use crate::{
Error, MountOption, PathFilesystem, PathNodeFs, Session, SessionConfig, ThreadPoolConfig,
ThreadingMode,
};
use std::path::Path;
pub struct MountConfig {
pub fs_name: String,
pub subtype: Option<String>,
pub allow_other: bool,
pub allow_root: bool,
pub default_permissions: bool,
pub read_only: bool,
pub nonempty: bool,
pub volname: Option<String>,
pub extra_options: Vec<String>,
}
impl MountConfig {
pub fn new(fs_name: impl Into<String>) -> Self {
Self {
fs_name: fs_name.into(),
subtype: None,
allow_other: false,
allow_root: false,
default_permissions: true,
read_only: false,
nonempty: false,
volname: None,
extra_options: Vec::new(),
}
}
pub fn parse_option_words<'a>(&mut self, words: impl IntoIterator<Item = &'a str>) {
for word in words {
if let Some(name) = word.strip_prefix("fsname=") {
self.fs_name = name.to_string();
continue;
}
if let Some(name) = word.strip_prefix("subtype=") {
self.subtype = Some(name.to_string());
continue;
}
if let Some(name) = word.strip_prefix("volname=") {
self.volname = Some(name.to_string());
continue;
}
match word {
"allow_other" => self.allow_other = true,
"allow_root" => self.allow_root = true,
"nonempty" => self.nonempty = true,
"ro" => self.read_only = true,
"rw" => self.read_only = false,
"default_permissions" => self.default_permissions = true,
other => self.extra_options.push(other.to_string()),
}
}
}
}
pub fn build_mount_options(cfg: &MountConfig) -> Vec<MountOption> {
let mut options = vec![MountOption::FsName(cfg.fs_name.clone())];
if let Some(subtype) = &cfg.subtype {
options.push(MountOption::Subtype(subtype.clone()));
}
if cfg.allow_other {
options.push(MountOption::AllowOther);
}
if cfg.allow_root {
options.push(MountOption::AllowRoot);
}
if cfg.default_permissions {
options.push(MountOption::DefaultPermissions);
}
if cfg.read_only {
options.push(MountOption::ReadOnly);
}
if cfg.nonempty {
options.push(MountOption::NonEmpty);
}
if let Some(volname) = &cfg.volname {
options.push(MountOption::VolName(volname.clone()));
}
options.extend(cfg.extra_options.iter().cloned().map(MountOption::Custom));
let dropped = options
.iter()
.filter(|option| option.render().is_none())
.count();
if dropped > 0 {
log::debug!("{dropped} mount option(s) dropped as unsupported on this platform");
}
options
}
pub fn mount_blocking<FS>(
fs: FS,
mount_point: &Path,
cfg: &MountConfig,
single_thread: bool,
) -> Result<(), Error>
where
FS: PathFilesystem + 'static,
{
let adapter = PathNodeFs::new(fs);
let session_config = SessionConfig {
threading: if single_thread {
ThreadingMode::SingleThreaded
} else {
ThreadingMode::MultiThreaded(ThreadPoolConfig::default())
},
};
let options = build_mount_options(cfg);
let mut session = Session::new_with_config(adapter, &options, session_config)?;
session.mount(mount_point)?;
session.run()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn preserves_mount_policy_and_passthrough_options() {
let mut config = MountConfig {
fs_name: "encfs-test".to_string(),
subtype: Some("encfs".to_string()),
allow_other: true,
allow_root: true,
default_permissions: true,
read_only: true,
nonempty: true,
volname: Some("Encrypted Files".to_string()),
extra_options: Vec::new(),
};
config.parse_option_words(["noatime"]);
let options = build_mount_options(&config);
assert!(options.contains(&MountOption::FsName("encfs-test".to_string())));
assert!(options.contains(&MountOption::Subtype("encfs".to_string())));
assert!(options.contains(&MountOption::AllowOther));
assert!(options.contains(&MountOption::AllowRoot));
assert!(options.contains(&MountOption::DefaultPermissions));
assert!(options.contains(&MountOption::ReadOnly));
assert!(options.contains(&MountOption::NonEmpty));
assert!(options.contains(&MountOption::VolName("Encrypted Files".to_string())));
assert!(options.contains(&MountOption::Custom("noatime".to_string())));
assert!(!options.iter().any(|option| matches!(
option,
MountOption::Custom(value) if value.starts_with("uid=") || value.starts_with("gid=")
)));
}
#[test]
fn parse_option_words_recognizes_known_words_and_passes_through_others() {
let mut config = MountConfig::new("fs");
config.parse_option_words(["allow_other", "allow_root", "nonempty", "ro", "noatime"]);
assert!(config.allow_other);
assert!(config.allow_root);
assert!(config.nonempty);
assert!(config.read_only);
assert_eq!(config.extra_options, vec!["noatime".to_string()]);
}
#[test]
fn subtype_is_omitted_by_default() {
let options = build_mount_options(&MountConfig::new("fs"));
assert!(
!options
.iter()
.any(|option| matches!(option, MountOption::Subtype(_)))
);
}
#[test]
fn parse_option_words_replaces_named_values() {
let mut config = MountConfig::new("default");
config.parse_option_words(["fsname=/data/a,b", "subtype=myfs", "volname=Vol"]);
config.parse_option_words(["fsname=/data/c"]);
assert_eq!(config.fs_name, "/data/c");
assert_eq!(config.subtype.as_deref(), Some("myfs"));
assert_eq!(config.volname.as_deref(), Some("Vol"));
assert!(config.extra_options.is_empty());
let options = build_mount_options(&config);
let fs_names = options
.iter()
.filter(|option| matches!(option, MountOption::FsName(_)))
.count();
assert_eq!(fs_names, 1);
}
#[test]
fn parse_option_words_rw_toggles_read_only_back_off() {
let mut config = MountConfig::new("fs");
config.parse_option_words(["ro", "rw"]);
assert!(!config.read_only);
config.parse_option_words(["ro"]);
assert!(config.read_only);
assert!(config.extra_options.is_empty());
}
}