From af4af462bdbcaf626220bc5265c203064a7d8d3d Mon Sep 17 00:00:00 2001
From: rowan <rowan@kitsu.cafe>
Date: Mon, 3 Mar 2025 19:03:55 -0600
Subject: [PATCH] more unit testing,, aaa

---
 crates/char_enum_derive/src/lib.rs |   4 +-
 src/fs/id_mapping.rs               |  14 +-
 src/fs/permission.rs               |   4 +-
 src/fs/stackable/fuse_overlay.rs   | 487 +++++++++++++++++++----------
 src/fs/stackable/overlay.rs        |  13 +-
 5 files changed, 337 insertions(+), 185 deletions(-)

diff --git a/crates/char_enum_derive/src/lib.rs b/crates/char_enum_derive/src/lib.rs
index 499777c..027a39b 100644
--- a/crates/char_enum_derive/src/lib.rs
+++ b/crates/char_enum_derive/src/lib.rs
@@ -58,7 +58,7 @@ pub fn as_char_derive(input: TokenStream) -> TokenStream {
             fn from_char(c: char) -> Result<Self, Self::Err> {
                 match c {
                     #(#from_char_variants)*
-                    ch => Err(char_enum::FromCharError::new(format!("{ch} is not a valid variant. expected one of: #(#chars)*")))
+                    ch => Err(char_enum::FromCharError(ch))
                 }
             }
         }
@@ -69,7 +69,7 @@ pub fn as_char_derive(input: TokenStream) -> TokenStream {
             fn from_str(s: &str) -> Result<Self, Self::Err> {
                 match s.chars().next() {
                     Some(c) => Ok(Self::from_char(c)?),
-                    None => Err(char_enum::FromStrError::new(format!("{s} does not correspond to a valid #name variant"))),
+                    None => Err(char_enum::FromStrError(s.to_string())),
                 }
             }
         }
diff --git a/src/fs/id_mapping.rs b/src/fs/id_mapping.rs
index aa02dd8..d3c6ee4 100644
--- a/src/fs/id_mapping.rs
+++ b/src/fs/id_mapping.rs
@@ -1,4 +1,5 @@
 use char_enum::{char_enum_derive::FromChar, FromChar, FromStrError, ToChar};
+use std::ops::Deref;
 use std::{error::Error, fmt::Display, num::ParseIntError, str::FromStr};
 
 use itertools::{peek_nth, Itertools};
@@ -135,6 +136,12 @@ impl From<TypedIdRange> for UntypedIdRange {
     }
 }
 
+impl From<(usize, usize, usize)> for UntypedIdRange {
+    fn from(value: (usize, usize, usize)) -> Self {
+        Self(value.0, value.1, value.2)
+    }
+}
+
 #[derive(Debug, Clone, PartialEq, Eq)]
 pub struct ParseIdRangeError(String);
 
@@ -278,9 +285,10 @@ impl FromStr for IdMapping {
     fn from_str(s: &str) -> Result<Self, Self::Err> {
         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(),
-            )),
+            None => {
+                let s = std::slice::from_ref(&s).iter().map(Deref::deref);
+                Ok(Self::from_iter(s)?)
+            }
         }
     }
 }
diff --git a/src/fs/permission.rs b/src/fs/permission.rs
index 87d7532..9658628 100644
--- a/src/fs/permission.rs
+++ b/src/fs/permission.rs
@@ -219,9 +219,7 @@ impl FromStr for SymbolicArgs {
             Ok(perms) if perms.0.bits() > 0 => Ok(Self::Mode(perms)),
             Ok(_) | Err(_) => match Modes::<GroupId>::from_str(s) {
                 Ok(perms) if perms.0.bits() > 0 => Ok(Self::Group(perms)),
-                Ok(_) | Err(_) => {
-                    Err(FromStrError::new(format!("{} is not a valid argument", s)).into())
-                }
+                Ok(_) | Err(_) => Err(FromStrError(s.to_string()).into()),
             },
         }
     }
diff --git a/src/fs/stackable/fuse_overlay.rs b/src/fs/stackable/fuse_overlay.rs
index 8f633b6..4bfa876 100644
--- a/src/fs/stackable/fuse_overlay.rs
+++ b/src/fs/stackable/fuse_overlay.rs
@@ -6,11 +6,12 @@ use std::{
     ops::Deref,
     path::{Path, PathBuf},
     process::Command,
+    slice::Iter,
     str::FromStr,
 };
 
 use crate::fs::{
-    id_mapping::{IdMapping, ParseIdRangeError},
+    id_mapping::{IdMapping as OriginalIdMapping, IdRange, ParseIdRangeError},
     mount::MountOptions,
     AsIter, FileSystem, Mountpoint,
 };
@@ -18,8 +19,8 @@ use crate::macros::*;
 
 use super::Stack;
 
-macro_rules! impl_wrapper {
-    ($name:ident($inner:ty), $err:ty, $from_str:block) => {
+macro_rules! impl_deref {
+    ($name:ident($inner:ty)) => {
         impl Deref for $name {
             type Target = $inner;
 
@@ -27,16 +28,23 @@ macro_rules! impl_wrapper {
                 &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<Self, Self::Err> {
-                $from_str
-                //Ok(Self($target::from_str(s)?))
+                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)
@@ -45,12 +53,13 @@ macro_rules! impl_wrapper {
     };
 }
 
-#[derive(Clone, Debug, PartialEq, Eq)]
-pub struct Test(usize);
-
-impl_wrapper!(Test(usize), ParseIntError, {
-    Ok(Test(usize::from_str(s)?))
-});
+macro_rules! impl_wrapper_struct {
+    ($name:ident($inner:ty), $err:ty) => {
+        impl_deref!($name($inner));
+        impl_fromstr!($name($inner), $err);
+        impl_display!($name);
+    };
+}
 
 #[derive(Clone, Debug, PartialEq, Eq)]
 pub struct ParseMaxIdleThreadsError(ParseIntError);
@@ -85,9 +94,39 @@ 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::Item> {
+        self.inner.next().map(PathBuf::as_path)
+    }
+}
+
 #[derive(Clone, Debug, Default, PartialEq, Eq)]
 pub struct LowerDirs(Vec<PathBuf>);
 
+impl<A: Into<PathBuf>> FromIterator<A> for LowerDirs {
+    fn from_iter<T: IntoIterator<Item = A>>(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<PathBuf>;
 
@@ -98,14 +137,16 @@ impl Deref for LowerDirs {
 
 impl Display for LowerDirs {
     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
-        write!(
-            f,
-            "{}",
-            self.0
-                .iter()
-                .map(|v| v.to_string_lossy())
-                .fold(String::new(), |a, b| a + &b)
-        )
+        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(())
     }
 }
 
@@ -113,7 +154,12 @@ impl FromStr for LowerDirs {
     type Err = Infallible;
 
     fn from_str(s: &str) -> Result<Self, Self::Err> {
-        Ok(Self(s.split(',').map(Into::into).collect()))
+        Ok(Self(
+            s.split(',')
+                .filter(|s| !s.is_empty())
+                .map(Into::into)
+                .collect(),
+        ))
     }
 }
 
@@ -141,22 +187,16 @@ impl From<Vec<PathBuf>> for LowerDirs {
     }
 }
 
-#[derive(Clone, Copy, Debug, PartialEq, Eq)]
+#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
 pub struct MaxIdleThreads(Option<usize>);
 
-impl Default for MaxIdleThreads {
-    fn default() -> Self {
-        Self(None)
-    }
-}
-
 impl FromStr for MaxIdleThreads {
     type Err = ParseMaxIdleThreadsError;
 
     fn from_str(s: &str) -> Result<Self, Self::Err> {
         match s {
             "-1" => Ok(Self(None)),
-            s => Ok(usize::from_str_radix(s, 10).map(Some).map(Self)?),
+            s => Ok(s.parse::<usize>().map(Some).map(Self)?),
         }
     }
 }
@@ -170,6 +210,8 @@ impl Display for MaxIdleThreads {
     }
 }
 
+impl_deref!(MaxIdleThreads(Option<usize>));
+
 #[derive(Clone, Copy, Debug, PartialEq, Eq)]
 pub struct MaxThreads(usize);
 
@@ -187,127 +229,59 @@ impl FromStr for MaxThreads {
     }
 }
 
-impl Display for MaxThreads {
-    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
-        write!(f, "{}", self.0)
-    }
-}
+impl_deref!(MaxThreads(usize));
+impl_display!(MaxThreads);
 
 #[derive(Clone, Copy, Debug, PartialEq, Eq)]
 pub struct SquashToId(usize);
 
+impl_deref!(SquashToId(usize));
+impl_display!(SquashToId);
+
 impl FromStr for SquashToId {
     type Err = ParseSquashToIdError;
 
     fn from_str(s: &str) -> Result<Self, Self::Err> {
-        Ok(usize::from_str_radix(s, 10).map(Self)?)
-    }
-}
-
-impl Display for SquashToId {
-    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
-        write!(f, "{}", self.0)
+        Ok(Self(s.parse::<usize>()?))
     }
 }
 
 #[derive(Clone, Copy, Debug, PartialEq, Eq)]
 pub struct SquashToUid(SquashToId);
 
-impl Deref for SquashToUid {
-    type Target = SquashToId;
-
-    fn deref(&self) -> &Self::Target {
-        &self.0
-    }
-}
-
-impl FromStr for SquashToUid {
-    type Err = ParseSquashToUidError;
-
-    fn from_str(s: &str) -> Result<Self, Self::Err> {
-        Ok(Self(SquashToId::from_str(s)?))
-    }
-}
-
-impl Display for SquashToUid {
-    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
-        write!(f, "{}", self.0)
-    }
-}
+impl_wrapper_struct!(SquashToUid(SquashToId), ParseSquashToUidError);
 
 #[derive(Clone, Copy, Debug, PartialEq, Eq)]
 pub struct SquashToGid(SquashToId);
 
-impl Deref for SquashToGid {
-    type Target = SquashToId;
+impl_wrapper_struct!(SquashToGid(SquashToId), ParseSquashToGidError);
 
-    fn deref(&self) -> &Self::Target {
-        &self.0
-    }
-}
-
-impl FromStr for SquashToGid {
-    type Err = ParseSquashToGidError;
-
-    fn from_str(s: &str) -> Result<Self, Self::Err> {
-        Ok(Self(SquashToId::from_str(s)?))
-    }
-}
-
-impl Display for SquashToGid {
-    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
-        write!(f, "{}", self.0)
-    }
-}
 #[derive(Clone, Debug, PartialEq, Eq)]
-pub struct GidMapping(IdMapping);
+pub struct IdMapping(OriginalIdMapping);
 
-impl Deref for GidMapping {
-    type Target = IdMapping;
-
-    fn deref(&self) -> &Self::Target {
-        &self.0
+impl IdMapping {
+    pub fn new(ranges: impl IntoIterator<Item = impl Into<IdRange>>) -> Self {
+        Self(OriginalIdMapping::new(ranges.into_iter(), ','))
     }
 }
 
-impl FromStr for GidMapping {
-    type Err = ParseGidMappingError;
-
-    fn from_str(s: &str) -> Result<Self, Self::Err> {
-        Ok(Self(IdMapping::from_str(s)?))
+impl From<OriginalIdMapping> for IdMapping {
+    fn from(value: OriginalIdMapping) -> Self {
+        Self(value)
     }
 }
 
-impl Display for GidMapping {
-    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
-        write!(f, "{}", self.0)
-    }
-}
+impl_wrapper_struct!(IdMapping(OriginalIdMapping), ParseIdRangeError);
+
+#[derive(Clone, Debug, PartialEq, Eq)]
+pub struct UidMapping(IdMapping);
+
+impl_wrapper_struct!(UidMapping(IdMapping), ParseUidMappingError);
 
 #[derive(Clone, Debug, PartialEq, Eq)]
 pub struct GidMapping(IdMapping);
 
-impl Deref for GidMapping {
-    type Target = IdMapping;
-
-    fn deref(&self) -> &Self::Target {
-        &self.0
-    }
-}
-
-impl FromStr for GidMapping {
-    type Err = ParseGidMappingError;
-
-    fn from_str(s: &str) -> Result<Self, Self::Err> {
-        Ok(Self(IdMapping::from_str(s)?))
-    }
-}
-
-impl Display for GidMapping {
-    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
-        write!(f, "{}", self.0)
-    }
-}
+impl_wrapper_struct!(GidMapping(IdMapping), ParseGidMappingError);
 
 #[derive(Clone, Debug, PartialEq, Eq)]
 pub enum ParseFuseOverlayOptionError {
@@ -317,6 +291,7 @@ pub enum ParseFuseOverlayOptionError {
     SquashToGid(ParseSquashToGidError),
     UidMapping(ParseUidMappingError),
     GidMapping(ParseGidMappingError),
+    UnknownOption(String),
 }
 
 impl Display for ParseFuseOverlayOptionError {
@@ -328,6 +303,7 @@ impl Display for ParseFuseOverlayOptionError {
             Self::SquashToGid(e) => write!(f, "{e}"),
             Self::UidMapping(e) => write!(f, "{e}"),
             Self::GidMapping(e) => write!(f, "{e}"),
+            Self::UnknownOption(opt) => write!(f, "unknown option: {opt}"),
         }
     }
 }
@@ -337,7 +313,11 @@ impl Error for ParseFuseOverlayOptionError {}
 impl_from_variants!(
     ParseFuseOverlayOptionError,
     MaxIdleThreads(ParseMaxIdleThreadsError),
-    MaxThreads(ParseMaxThreadsError)
+    MaxThreads(ParseMaxThreadsError),
+    SquashToUid(ParseSquashToUidError),
+    SquashToGid(ParseSquashToGidError),
+    UidMapping(ParseUidMappingError),
+    GidMapping(ParseGidMappingError)
 );
 
 #[derive(Clone, Debug, PartialEq, Eq)]
@@ -355,8 +335,8 @@ pub enum FuseOverlayOption {
     SquashToGid(SquashToGid),
     StaticNLink,
     NoAcl,
-    UidMapping(IdMapping),
-    GidMapping(IdMapping),
+    UidMapping(UidMapping),
+    GidMapping(GidMapping),
 }
 
 impl_from_variants!(
@@ -365,7 +345,9 @@ impl_from_variants!(
     MaxIdleThreads(MaxIdleThreads),
     MaxThreads(MaxThreads),
     SquashToUid(SquashToUid),
-    SquashToGid(SquashToGid)
+    SquashToGid(SquashToGid),
+    UidMapping(UidMapping),
+    GidMapping(GidMapping)
 );
 
 impl FromStr for FuseOverlayOption {
@@ -374,7 +356,7 @@ impl FromStr for FuseOverlayOption {
     fn from_str(s: &str) -> Result<Self, Self::Err> {
         let s = s.trim().trim_start_matches('-');
 
-        let option = match s.split_once(|delim| delim == ' ' || delim == '=') {
+        let option = match s.split_once([' ', '=']) {
             Some((option, value)) => (option, Some(value)),
             None => (s, None),
         };
@@ -384,17 +366,18 @@ impl FromStr for FuseOverlayOption {
             ("upperdir", Some(args)) => Ok(Self::UpperDir(PathBuf::from(args))),
             ("workdir", Some(args)) => Ok(Self::WorkDir(PathBuf::from(args))),
             ("clonefd", None) => Ok(Self::CloneFd),
-            ("max_idle_threads", Some(args)) => Ok(MaxIdleThreads::from_str(args).into()?),
-            ("max_threads", Some(args)) => Ok(MaxThreads::from_str(args).into()?),
+            ("max_idle_threads", Some(args)) => Ok(MaxIdleThreads::from_str(args)?.into()),
+            ("max_threads", Some(args)) => Ok(MaxThreads::from_str(args)?.into()),
             ("allow_other", None) => Ok(Self::AllowOther),
             ("allow_root", None) => Ok(Self::AllowRoot),
             ("squash_to_root", None) => Ok(Self::SquashToRoot),
-            ("squash_to_uid", Some(uid)) => Ok(SquashToUid::from_str(uid).into()?),
-            ("squash_to_gid", Some(gid)) => Ok(SquashToGid::from_str(gid).into()?),
+            ("squash_to_uid", Some(uid)) => Ok(SquashToUid::from_str(uid)?.into()),
+            ("squash_to_gid", Some(gid)) => Ok(SquashToGid::from_str(gid)?.into()),
             ("static_nlink", None) => Ok(Self::StaticNLink),
             ("noacl", None) => Ok(Self::NoAcl),
-            ("uidmapping", Some(ids)) => Ok(Self::UidMapping(IdMapping::from_str(ids)?)),
-            ("gidmapping", Some(ids)) => Ok(Self::GidMapping(IdMapping::from_str(ids)?)),
+            ("uidmapping", Some(ids)) => Ok(UidMapping::from_str(ids)?.into()),
+            ("gidmapping", Some(ids)) => Ok(GidMapping::from_str(ids)?.into()),
+            (opt, _) => Err(ParseFuseOverlayOptionError::UnknownOption(opt.to_string())),
         }
     }
 }
@@ -404,9 +387,9 @@ impl Display for FuseOverlayOption {
         write!(f, "-o ")?;
 
         match self {
-            FuseOverlayOption::LowerDir(lower) => write!(f, "{lower}"),
-            FuseOverlayOption::UpperDir(path) => write!(f, "{}", path.to_string_lossy()),
-            FuseOverlayOption::WorkDir(path) => write!(f, "{}", path.to_string_lossy()),
+            FuseOverlayOption::LowerDir(lower) => write!(f, "lowerdir={lower}"),
+            FuseOverlayOption::UpperDir(path) => write!(f, "upperdir={}", path.to_string_lossy()),
+            FuseOverlayOption::WorkDir(path) => write!(f, "workdir={}", path.to_string_lossy()),
             FuseOverlayOption::CloneFd => write!(f, "clone_fd"),
             FuseOverlayOption::MaxIdleThreads(n) => write!(f, "max_idle_threads={n}"),
             FuseOverlayOption::MaxThreads(n) => write!(f, "max_threads={n}"),
@@ -425,47 +408,59 @@ impl Display for FuseOverlayOption {
 
 #[derive(Debug)]
 pub struct FuseOverlay {
-    lower_dir: usize,
-    upper_dir: Option<usize>,
-    work_dir: Option<usize>,
+    mount_target: PathBuf,
     options: MountOptions<FuseOverlayOption>,
 }
 
 impl FuseOverlay {
-    pub fn new(lowerdir: impl Into<LowerDirs>) -> Self {
+    pub fn new(target: impl Into<PathBuf>) -> Self {
         Self {
-            lower_dir: 0,
-            upper_dir: None,
-            work_dir: None,
-            options: MountOptions(vec![FuseOverlayOption::LowerDir(lowerdir.into())]),
+            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<Item = FuseOverlayOption>) {
+        self.options.extend(options)
+    }
+
+    fn find_option<'a, T, F>(&'a self, f: F) -> Option<T>
+    where
+        F: Fn(&'a FuseOverlayOption) -> Option<T>,
+    {
+        self.options.iter().filter_map(f).take(1).next()
+    }
 }
 
 impl Stack for FuseOverlay {
     fn lower_dirs(&self) -> impl Iterator<Item = &std::path::Path> {
-        match self.options.get(self.lower_dir) {
-            Some(FuseOverlayOption::LowerDir(lower)) => lower.as_iter().map(AsRef::as_ref),
-            _ => panic!("invalid lowerdir option"),
+        let result = self.find_option(|opt| match opt {
+            FuseOverlayOption::LowerDir(dirs) => Some(dirs),
+            _ => None,
+        });
+
+        match result {
+            Some(dirs) => dirs.into_iter(),
+            None => DirIter::default(),
         }
     }
 
     fn upper_dir(&self) -> Option<&std::path::Path> {
-        let upper = self.upper_dir.and_then(|i| self.options.get(i));
-
-        match upper {
-            Some(FuseOverlayOption::UpperDir(upper)) => Some(upper),
+        self.find_option(|opt| match opt {
+            FuseOverlayOption::UpperDir(dir) => Some(dir.as_path()),
             _ => None,
-        }
+        })
     }
 
     fn work_dir(&self) -> Option<&std::path::Path> {
-        let work = self.work_dir.and_then(|i| self.options.get(i));
-
-        match work {
-            Some(FuseOverlayOption::WorkDir(work)) => Some(work),
+        self.find_option(|opt| match opt {
+            FuseOverlayOption::WorkDir(dir) => Some(dir.as_path()),
             _ => None,
-        }
+        })
     }
 }
 
@@ -477,29 +472,18 @@ impl Mountpoint for FuseOverlay {
 
 impl FileSystem for FuseOverlay {
     fn mount(&mut self) -> std::io::Result<()> {
-        let mut cmd = Command::new("fuse-overlayfs").arg(self.options.to_string());
-
-        if self.fs.lower.len() > 0 {
-            cmd.arg(format!("-o lowerdir={}", self.fs.lower));
-        };
-
-        if let Some(upper) = &self.fs.upper {
-            cmd.arg(format!("-o upperdir={}", upper.to_string_lossy()));
-        };
-
-        if let Some(work) = &self.fs.work {
-            cmd.arg(format!("-o workdir={}", work.to_string_lossy()));
-        };
-
-        cmd.output()?;
+        Command::new("fuse-overlayfs")
+            .arg(self.options.to_string())
+            .output()?;
 
         Ok(())
     }
 
     fn unmount(&mut self) -> std::io::Result<()> {
-        if let Some(upper) = &self.fs.upper {
-            Command::new("fusermount").arg("-u").arg(upper).output()?;
-        }
+        Command::new("fusermount")
+            .arg("-u")
+            .arg(&self.mount_target)
+            .output()?;
 
         Ok(())
     }
@@ -507,6 +491,165 @@ impl FileSystem for FuseOverlay {
 
 impl Display for FuseOverlay {
     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
-        write!(f, "fuse-overlayfs")
+        write!(f, "fuse-overlayfs")?;
+
+        // INFO: this is to get rid of the extraneous space if
+        // self.options is empty
+        if self.options.len() > 0 {
+            write!(f, " {}", self.options)?;
+        }
+
+        write!(f, " {}", self.mount_target.to_string_lossy())
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use std::path::Path;
+    use std::str::FromStr;
+
+    use super::*;
+    use crate::fs::id_mapping::UntypedIdRange;
+    use crate::fs::stackable::Stack;
+    use crate::fs::Mountpoint;
+
+    #[test]
+    fn lower_dirs() {
+        let empty = LowerDirs::default();
+        assert_eq!(empty.to_string(), "");
+
+        let one = LowerDirs::from_iter(["/lower"]);
+        assert_eq!(one.to_string(), "/lower");
+
+        let two = LowerDirs::from_iter(["/first", "/second"]);
+        assert_eq!(two.to_string(), "/first,/second");
+
+        let from_str_none = LowerDirs::from_str("").unwrap();
+        assert_eq!(from_str_none, empty);
+
+        let from_str_one = LowerDirs::from_str("/lower").unwrap();
+        assert_eq!(from_str_one, one);
+
+        let from_str_two = LowerDirs::from_str("/first,/second").unwrap();
+        assert_eq!(from_str_two, two);
+    }
+
+    #[test]
+    fn max_threads() {
+        let default_threads = MaxThreads::default();
+        assert_eq!(*default_threads, 10);
+        assert_eq!(default_threads.to_string(), "10");
+
+        assert_eq!(MaxThreads::from_str("10").unwrap(), default_threads);
+        let err = MaxThreads::from_str("");
+        assert!(matches!(err, Err(ParseMaxThreadsError(_))));
+        assert_eq!(MaxThreads::from_str("69").unwrap(), MaxThreads(69));
+    }
+
+    #[test]
+    fn max_idle_threads() {
+        let default_threads = MaxIdleThreads::default();
+        assert_eq!(*default_threads, None);
+        assert_eq!(default_threads.to_string(), "-1");
+        assert_eq!(MaxIdleThreads::from_str("-1").unwrap(), default_threads);
+        assert_eq!(
+            MaxIdleThreads::from_str("69").unwrap(),
+            MaxIdleThreads(Some(69))
+        );
+        let err = MaxIdleThreads::from_str("");
+        assert!(matches!(err, Err(ParseMaxIdleThreadsError(_))));
+    }
+
+    #[test]
+    fn squash_to_id() {
+        let squash = SquashToId(1);
+        assert_eq!(squash.to_string(), "1");
+        assert_eq!(SquashToId::from_str("1").unwrap(), squash);
+        let err = SquashToId::from_str("");
+        assert!(matches!(err, Err(ParseSquashToIdError(_))));
+
+        let uid = SquashToUid(SquashToId(2));
+        let gid = SquashToGid(SquashToId(4));
+
+        assert_eq!(uid.to_string(), "2");
+        assert_eq!(gid.to_string(), "4");
+
+        assert_eq!(SquashToUid::from_str("2").unwrap(), uid);
+        assert_eq!(SquashToGid::from_str("4").unwrap(), gid);
+
+        let uid_err = SquashToUid::from_str("");
+        let gid_err = SquashToGid::from_str("");
+        assert!(matches!(uid_err, Err(ParseSquashToUidError(_))));
+        assert!(matches!(gid_err, Err(ParseSquashToGidError(_))));
+    }
+
+    #[test]
+    fn id_mapping() {
+        let id_range = IdRange::Untyped(UntypedIdRange(1, 1000, 1));
+        let uid = UidMapping(IdMapping::new([id_range.clone()]));
+        let gid = GidMapping(IdMapping::new([id_range.clone()]));
+
+        assert_eq!(uid.to_string(), "1:1000:1");
+        assert_eq!(gid.to_string(), "1:1000:1");
+
+        assert_eq!(UidMapping::from_str("1:1000:1").unwrap(), uid);
+        assert_eq!(GidMapping::from_str("1:1000:1").unwrap(), gid);
+    }
+
+    #[test]
+    fn command_output() {
+        let basic = FuseOverlay::new("/mnt");
+        assert_eq!(basic.to_string(), "fuse-overlayfs /mnt");
+
+        let mut with_options = FuseOverlay::new("~/.config");
+        with_options.push_options([
+            FuseOverlayOption::NoAcl,
+            LowerDirs::from_iter(["~/.themes", "~/.colors"]).into(),
+        ]);
+
+        assert_eq!(
+            with_options.to_string(),
+            "fuse-overlayfs -o noacl -o lowerdir=~/.themes,~/.colors ~/.config"
+        );
+    }
+
+    #[test]
+    fn mountpoint_trait_impl() {
+        let mut all = FuseOverlay::new("/mnt");
+
+        all.push_options([
+            LowerDirs::from_iter(["/doesnt", "/matter"]).into(),
+            FuseOverlayOption::UpperDir("/upper".into()),
+            FuseOverlayOption::WorkDir("/work".into()),
+        ]);
+
+        let opts: Vec<String> = all.options().map(|x| x.to_string()).collect();
+
+        assert_eq!(
+            opts,
+            [
+                "-o lowerdir=/doesnt,/matter",
+                "-o upperdir=/upper",
+                "-o workdir=/work"
+            ]
+        );
+    }
+
+    #[test]
+    fn stack_trait_impl() {
+        let mut all = FuseOverlay::new("/mnt");
+
+        assert_eq!(all.lower_dirs().next(), None);
+        assert_eq!(all.upper_dir(), None);
+        assert_eq!(all.work_dir(), None);
+
+        all.push_option(LowerDirs::from_iter(["/lower"]).into());
+        assert_eq!(all.lower_dirs().next(), Some(Path::new("/lower")));
+
+        all.push_option(FuseOverlayOption::UpperDir("/upper".into()));
+        assert_eq!(all.upper_dir(), Some(Path::new("/upper")));
+
+        all.push_option(FuseOverlayOption::WorkDir("/work".into()));
+        assert_eq!(all.work_dir(), Some(Path::new("/work")));
     }
 }
diff --git a/src/fs/stackable/overlay.rs b/src/fs/stackable/overlay.rs
index b6a7b36..e980091 100644
--- a/src/fs/stackable/overlay.rs
+++ b/src/fs/stackable/overlay.rs
@@ -1,8 +1,10 @@
 use std::fmt::Display;
+use std::iter;
+use std::path::PathBuf;
 
 use crate::fs::{AsIter, Mountpoint};
 
-use super::{Stack, Stackable};
+use super::Stack;
 
 #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
 pub enum RedirectDir {
@@ -96,21 +98,22 @@ impl Display for OverlayOptions {
 
 #[derive(Debug)]
 pub struct Overlay {
-    fs: Stackable,
+    mount_target: PathBuf,
     options: Vec<OverlayOptions>,
 }
 
 impl Stack for Overlay {
     fn lower_dirs(&self) -> impl Iterator<Item = &std::path::Path> {
-        self.fs.lower_dirs()
+        todo!();
+        iter::empty()
     }
 
     fn upper_dir(&self) -> Option<&std::path::Path> {
-        self.fs.upper_dir()
+        todo!()
     }
 
     fn work_dir(&self) -> Option<&std::path::Path> {
-        self.fs.work_dir()
+        todo!()
     }
 }