use std::collections::HashMap;
use std::ffi::OsStr;
use std::io::{Read, Seek, SeekFrom, Write};
use std::path::{Path, PathBuf};
use std::time::{Duration, SystemTime, UNIX_EPOCH};
use fuser::{
BackgroundSession, FUSE_ROOT_ID, FileAttr, FileType, Filesystem as FuseFilesystem,
KernelConfig, MountOption, ReplyAttr, ReplyCreate, ReplyData, ReplyDirectory, ReplyEmpty,
ReplyEntry, ReplyOpen, ReplyStatfs, ReplyWrite, ReplyXattr, Request, TimeOrNow,
};
use crate::block::BlockDevice;
use crate::fs::{
DeviceKind, EntryKind, FileAttrs, FileMeta, Filesystem, MutationCapability, OpenFlags, SetAttrs,
};
const TTL: Duration = Duration::from_secs(1);
pub struct FstoolFs {
fs: Box<dyn Filesystem + Send>,
dev: Box<dyn BlockDevice + Send>,
fs_name: &'static str,
ino_to_path: HashMap<u64, PathBuf>,
path_to_ino: HashMap<PathBuf, u64>,
next_ino: u64,
writable: bool,
partial_writable: bool,
allow_other: bool,
}
impl FstoolFs {
pub fn new(
fs: Box<dyn Filesystem + Send>,
dev: Box<dyn BlockDevice + Send>,
fs_name: &'static str,
) -> Self {
let cap = fs.mutation_capability();
let writable = cap.supports_add_remove();
let partial_writable = cap.supports_partial_writes();
let mut ino_to_path = HashMap::new();
let mut path_to_ino = HashMap::new();
ino_to_path.insert(FUSE_ROOT_ID, PathBuf::from("/"));
path_to_ino.insert(PathBuf::from("/"), FUSE_ROOT_ID);
Self {
fs,
dev,
fs_name,
ino_to_path,
path_to_ino,
next_ino: 2,
writable,
partial_writable,
allow_other: false,
}
}
pub fn allow_other(mut self, yes: bool) -> Self {
self.allow_other = yes;
self
}
fn mount_options(&self) -> Vec<MountOption> {
let mut opts = vec![MountOption::FSName(self.fs_name.to_string())];
if self.allow_other {
opts.push(MountOption::AllowOther);
opts.push(MountOption::AutoUnmount);
}
if !self.writable {
opts.push(MountOption::RO);
}
opts
}
pub fn mount(self, mountpoint: &Path) -> std::io::Result<()> {
let opts = self.mount_options();
fuser::mount2(self, mountpoint, &opts)
}
pub fn spawn_mount(self, mountpoint: &Path) -> std::io::Result<BackgroundSession> {
let opts = self.mount_options();
fuser::spawn_mount2(self, mountpoint, &opts)
}
fn path_for(&self, ino: u64) -> Option<PathBuf> {
self.ino_to_path.get(&ino).cloned()
}
fn ino_for(&mut self, path: &Path) -> u64 {
if let Some(&id) = self.path_to_ino.get(path) {
return id;
}
let id = self.next_ino;
self.next_ino += 1;
self.ino_to_path.insert(id, path.to_path_buf());
self.path_to_ino.insert(path.to_path_buf(), id);
id
}
fn forget_path(&mut self, path: &Path) {
if let Some(id) = self.path_to_ino.remove(path) {
self.ino_to_path.remove(&id);
}
}
fn make_attr(&self, fuse_ino: u64, a: &FileAttrs) -> FileAttr {
let kind = entry_kind_to_file_type(a.kind);
let blksize = self.fs_block_size_hint().unwrap_or(4096);
FileAttr {
ino: fuse_ino,
size: a.size,
blocks: a.blocks,
atime: ts(a.atime),
mtime: ts(a.mtime),
ctime: ts(a.ctime),
crtime: ts(a.mtime),
kind,
perm: a.mode & 0o7777,
nlink: a.nlink,
uid: a.uid,
gid: a.gid,
rdev: a.rdev,
blksize,
flags: 0,
}
}
fn fs_block_size_hint(&self) -> Option<u32> {
Some(4096)
}
fn now_secs() -> u32 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_secs() as u32)
.unwrap_or(0)
}
fn child_path(parent: &Path, name: &OsStr) -> PathBuf {
let name_str = name.to_string_lossy();
if parent == Path::new("/") {
PathBuf::from(format!("/{name_str}"))
} else {
let mut p = parent.to_path_buf();
p.push(name_str.as_ref());
p
}
}
}
fn ts(secs: u32) -> SystemTime {
UNIX_EPOCH + Duration::from_secs(secs as u64)
}
fn time_or_now_secs(t: TimeOrNow) -> u32 {
match t {
TimeOrNow::SpecificTime(st) => st
.duration_since(UNIX_EPOCH)
.map(|d| d.as_secs() as u32)
.unwrap_or(0),
TimeOrNow::Now => SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_secs() as u32)
.unwrap_or(0),
}
}
fn fs_err_to_errno(e: &crate::Error) -> i32 {
match e {
crate::Error::InvalidArgument(_) => libc::ENOENT,
crate::Error::Unsupported(_) => libc::ENOSYS,
crate::Error::Immutable { .. } => libc::EROFS,
crate::Error::Io(_) => libc::EIO,
_ => libc::EIO,
}
}
fn entry_kind_to_file_type(k: EntryKind) -> FileType {
match k {
EntryKind::Regular => FileType::RegularFile,
EntryKind::Dir => FileType::Directory,
EntryKind::Symlink => FileType::Symlink,
EntryKind::Block => FileType::BlockDevice,
EntryKind::Char => FileType::CharDevice,
EntryKind::Fifo => FileType::NamedPipe,
EntryKind::Socket => FileType::Socket,
EntryKind::Unknown => FileType::RegularFile,
}
}
impl FuseFilesystem for FstoolFs {
fn init(&mut self, _req: &Request<'_>, _config: &mut KernelConfig) -> Result<(), libc::c_int> {
Ok(())
}
fn destroy(&mut self) {
if self.writable {
if let Err(e) = self.fs.flush(self.dev.as_mut()) {
eprintln!("fstool fuse: flush on unmount failed: {e}");
}
}
let _ = self.dev.sync();
}
fn lookup(&mut self, _req: &Request<'_>, parent: u64, name: &OsStr, reply: ReplyEntry) {
let parent_path = match self.path_for(parent) {
Some(p) => p,
None => return reply.error(libc::ENOENT),
};
let child = Self::child_path(&parent_path, name);
let attrs = match self.fs.getattr(self.dev.as_mut(), &child) {
Ok(a) => a,
Err(_) => return reply.error(libc::ENOENT),
};
let ino = self.ino_for(&child);
let attr = self.make_attr(ino, &attrs);
reply.entry(&TTL, &attr, 0);
}
fn getattr(&mut self, _req: &Request<'_>, ino: u64, _fh: Option<u64>, reply: ReplyAttr) {
let path = match self.path_for(ino) {
Some(p) => p,
None => return reply.error(libc::ENOENT),
};
match self.fs.getattr(self.dev.as_mut(), &path) {
Ok(attrs) => {
let attr = self.make_attr(ino, &attrs);
reply.attr(&TTL, &attr);
}
Err(_) => reply.error(libc::ENOENT),
}
}
#[allow(clippy::too_many_arguments)]
fn setattr(
&mut self,
_req: &Request<'_>,
ino: u64,
mode: Option<u32>,
uid: Option<u32>,
gid: Option<u32>,
size: Option<u64>,
atime: Option<TimeOrNow>,
mtime: Option<TimeOrNow>,
ctime: Option<SystemTime>,
_fh: Option<u64>,
_crtime: Option<SystemTime>,
_chgtime: Option<SystemTime>,
_bkuptime: Option<SystemTime>,
_flags: Option<u32>,
reply: ReplyAttr,
) {
if !self.writable {
return reply.error(libc::EROFS);
}
let path = match self.path_for(ino) {
Some(p) => p,
None => return reply.error(libc::ENOENT),
};
let attrs = SetAttrs {
mode: mode.map(|m| (m & 0o7777) as u16),
uid,
gid,
atime: atime.map(time_or_now_secs),
mtime: mtime.map(time_or_now_secs),
ctime: ctime.map(|t| {
t.duration_since(UNIX_EPOCH)
.map(|d| d.as_secs() as u32)
.unwrap_or(0)
}),
};
let asked_attrs = attrs.mode.is_some()
|| attrs.uid.is_some()
|| attrs.gid.is_some()
|| attrs.atime.is_some()
|| attrs.mtime.is_some()
|| attrs.ctime.is_some();
if asked_attrs {
if let Err(e) = self.fs.set_attrs(self.dev.as_mut(), &path, attrs) {
return reply.error(fs_err_to_errno(&e));
}
}
if let Some(sz) = size {
if let Err(e) = self.fs.truncate(self.dev.as_mut(), &path, sz) {
return reply.error(fs_err_to_errno(&e));
}
}
match self.fs.getattr(self.dev.as_mut(), &path) {
Ok(a) => {
let attr = self.make_attr(ino, &a);
reply.attr(&TTL, &attr);
}
Err(_) => reply.error(libc::EIO),
}
}
fn readlink(&mut self, _req: &Request<'_>, ino: u64, reply: ReplyData) {
let path = match self.path_for(ino) {
Some(p) => p,
None => return reply.error(libc::ENOENT),
};
match self.fs.read_symlink(self.dev.as_mut(), &path) {
Ok(target) => reply.data(target.as_os_str().as_encoded_bytes()),
Err(e) => reply.error(fs_err_to_errno(&e)),
}
}
fn mknod(
&mut self,
req: &Request<'_>,
parent: u64,
name: &OsStr,
mode: u32,
_umask: u32,
rdev: u32,
reply: ReplyEntry,
) {
if !self.writable {
return reply.error(libc::EROFS);
}
let parent_path = match self.path_for(parent) {
Some(p) => p,
None => return reply.error(libc::ENOENT),
};
let child = Self::child_path(&parent_path, name);
let mode_type = mode & 0o170000;
let meta = FileMeta {
mode: (mode & 0o7777) as u16,
uid: req.uid(),
gid: req.gid(),
mtime: Self::now_secs(),
atime: Self::now_secs(),
ctime: Self::now_secs(),
};
let res = match mode_type {
0o100000 => self.fs.create_file(
self.dev.as_mut(),
&child,
crate::fs::FileSource::Zero(0),
meta,
),
0o020000 => {
let major = (rdev >> 8) & 0xfff;
let minor = (rdev & 0xff) | ((rdev >> 12) & 0xfff00);
self.fs.create_device(
self.dev.as_mut(),
&child,
DeviceKind::Char,
major,
minor,
meta,
)
}
0o060000 => {
let major = (rdev >> 8) & 0xfff;
let minor = (rdev & 0xff) | ((rdev >> 12) & 0xfff00);
self.fs.create_device(
self.dev.as_mut(),
&child,
DeviceKind::Block,
major,
minor,
meta,
)
}
0o010000 => self.fs.create_device(
self.dev.as_mut(),
&child,
DeviceKind::Fifo,
0,
0,
meta,
),
0o140000 => self.fs.create_device(
self.dev.as_mut(),
&child,
DeviceKind::Socket,
0,
0,
meta,
),
_ => return reply.error(libc::ENOSYS),
};
match res {
Ok(()) => self.reply_with_new_entry(&child, reply),
Err(e) => reply.error(fs_err_to_errno(&e)),
}
}
fn mkdir(
&mut self,
req: &Request<'_>,
parent: u64,
name: &OsStr,
mode: u32,
_umask: u32,
reply: ReplyEntry,
) {
if !self.writable {
return reply.error(libc::EROFS);
}
let parent_path = match self.path_for(parent) {
Some(p) => p,
None => return reply.error(libc::ENOENT),
};
let child = Self::child_path(&parent_path, name);
let meta = FileMeta {
mode: (mode & 0o7777) as u16,
uid: req.uid(),
gid: req.gid(),
mtime: Self::now_secs(),
atime: Self::now_secs(),
ctime: Self::now_secs(),
};
match self.fs.create_dir(self.dev.as_mut(), &child, meta) {
Ok(()) => self.reply_with_new_entry(&child, reply),
Err(e) => reply.error(fs_err_to_errno(&e)),
}
}
fn unlink(&mut self, _req: &Request<'_>, parent: u64, name: &OsStr, reply: ReplyEmpty) {
if !self.writable {
return reply.error(libc::EROFS);
}
let parent_path = match self.path_for(parent) {
Some(p) => p,
None => return reply.error(libc::ENOENT),
};
let child = Self::child_path(&parent_path, name);
match self.fs.remove(self.dev.as_mut(), &child) {
Ok(()) => {
self.forget_path(&child);
reply.ok();
}
Err(e) => reply.error(fs_err_to_errno(&e)),
}
}
fn rmdir(&mut self, _req: &Request<'_>, parent: u64, name: &OsStr, reply: ReplyEmpty) {
self.unlink(_req, parent, name, reply)
}
fn symlink(
&mut self,
req: &Request<'_>,
parent: u64,
link_name: &OsStr,
target: &Path,
reply: ReplyEntry,
) {
if !self.writable {
return reply.error(libc::EROFS);
}
let parent_path = match self.path_for(parent) {
Some(p) => p,
None => return reply.error(libc::ENOENT),
};
let child = Self::child_path(&parent_path, link_name);
let meta = FileMeta {
mode: 0o777,
uid: req.uid(),
gid: req.gid(),
mtime: Self::now_secs(),
atime: Self::now_secs(),
ctime: Self::now_secs(),
};
match self
.fs
.create_symlink(self.dev.as_mut(), &child, target, meta)
{
Ok(()) => self.reply_with_new_entry(&child, reply),
Err(e) => reply.error(fs_err_to_errno(&e)),
}
}
fn rename(
&mut self,
_req: &Request<'_>,
parent: u64,
name: &OsStr,
newparent: u64,
newname: &OsStr,
_flags: u32,
reply: ReplyEmpty,
) {
if !self.writable {
return reply.error(libc::EROFS);
}
let parent_path = match self.path_for(parent) {
Some(p) => p,
None => return reply.error(libc::ENOENT),
};
let newparent_path = match self.path_for(newparent) {
Some(p) => p,
None => return reply.error(libc::ENOENT),
};
let old = Self::child_path(&parent_path, name);
let new = Self::child_path(&newparent_path, newname);
match self.fs.rename(self.dev.as_mut(), &old, &new) {
Ok(()) => {
if let Some(id) = self.path_to_ino.remove(&old) {
self.ino_to_path.insert(id, new.clone());
self.path_to_ino.insert(new, id);
}
reply.ok();
}
Err(e) => reply.error(fs_err_to_errno(&e)),
}
}
fn link(
&mut self,
_req: &Request<'_>,
ino: u64,
newparent: u64,
newname: &OsStr,
reply: ReplyEntry,
) {
if !self.writable {
return reply.error(libc::EROFS);
}
let target = match self.path_for(ino) {
Some(p) => p,
None => return reply.error(libc::ENOENT),
};
let newparent_path = match self.path_for(newparent) {
Some(p) => p,
None => return reply.error(libc::ENOENT),
};
let new = Self::child_path(&newparent_path, newname);
match self.fs.hardlink(self.dev.as_mut(), &target, &new) {
Ok(()) => self.reply_with_new_entry(&new, reply),
Err(e) => reply.error(fs_err_to_errno(&e)),
}
}
fn open(&mut self, _req: &Request<'_>, _ino: u64, _flags: i32, reply: ReplyOpen) {
reply.opened(0, 0);
}
fn read(
&mut self,
_req: &Request<'_>,
ino: u64,
_fh: u64,
offset: i64,
size: u32,
_flags: i32,
_lock_owner: Option<u64>,
reply: ReplyData,
) {
let path = match self.path_for(ino) {
Some(p) => p,
None => return reply.error(libc::ENOENT),
};
let mut handle = match self.fs.open_file_ro(self.dev.as_mut(), &path) {
Ok(h) => h,
Err(e) => return reply.error(fs_err_to_errno(&e)),
};
if handle.seek(SeekFrom::Start(offset as u64)).is_err() {
return reply.error(libc::EIO);
}
let mut buf = vec![0u8; size as usize];
let n = match handle.read(&mut buf) {
Ok(n) => n,
Err(_) => return reply.error(libc::EIO),
};
buf.truncate(n);
reply.data(&buf);
}
#[allow(clippy::too_many_arguments)]
fn write(
&mut self,
_req: &Request<'_>,
ino: u64,
_fh: u64,
offset: i64,
data: &[u8],
_write_flags: u32,
_flags: i32,
_lock_owner: Option<u64>,
reply: ReplyWrite,
) {
if !self.partial_writable {
return reply.error(libc::EROFS);
}
let path = match self.path_for(ino) {
Some(p) => p,
None => return reply.error(libc::ENOENT),
};
let res = (|| -> crate::Result<usize> {
let mut h =
self.fs
.open_file_rw(self.dev.as_mut(), &path, OpenFlags::default(), None)?;
h.seek(SeekFrom::Start(offset as u64))
.map_err(crate::Error::Io)?;
h.write_all(data).map_err(crate::Error::Io)?;
Ok(data.len())
})();
match res {
Ok(n) => reply.written(n as u32),
Err(e) => reply.error(fs_err_to_errno(&e)),
}
}
fn flush(
&mut self,
_req: &Request<'_>,
_ino: u64,
_fh: u64,
_lock_owner: u64,
reply: ReplyEmpty,
) {
if !self.writable {
return reply.ok();
}
match self.fs.flush(self.dev.as_mut()) {
Ok(()) => reply.ok(),
Err(e) => reply.error(fs_err_to_errno(&e)),
}
}
fn fsync(
&mut self,
_req: &Request<'_>,
_ino: u64,
_fh: u64,
_datasync: bool,
reply: ReplyEmpty,
) {
if !self.writable {
return reply.ok();
}
match self.fs.flush(self.dev.as_mut()) {
Ok(()) => reply.ok(),
Err(e) => reply.error(fs_err_to_errno(&e)),
}
}
fn release(
&mut self,
_req: &Request<'_>,
_ino: u64,
_fh: u64,
_flags: i32,
_lock_owner: Option<u64>,
_flush: bool,
reply: ReplyEmpty,
) {
reply.ok();
}
fn opendir(&mut self, _req: &Request<'_>, _ino: u64, _flags: i32, reply: ReplyOpen) {
reply.opened(0, 0);
}
fn readdir(
&mut self,
_req: &Request<'_>,
ino: u64,
_fh: u64,
offset: i64,
mut reply: ReplyDirectory,
) {
let path = match self.path_for(ino) {
Some(p) => p,
None => return reply.error(libc::ENOENT),
};
let entries = match self.fs.list(self.dev.as_mut(), &path) {
Ok(v) => v,
Err(e) => return reply.error(fs_err_to_errno(&e)),
};
let parent_path = path
.parent()
.map(|p| p.to_path_buf())
.unwrap_or_else(|| path.clone());
let parent_ino = if path == Path::new("/") {
FUSE_ROOT_ID
} else {
self.ino_for(&parent_path)
};
let mut all: Vec<(u64, FileType, String)> = Vec::with_capacity(entries.len() + 2);
all.push((ino, FileType::Directory, ".".to_string()));
all.push((parent_ino, FileType::Directory, "..".to_string()));
for e in entries {
if e.name == "." || e.name == ".." {
continue;
}
let child = Self::child_path(&path, OsStr::new(&e.name));
let child_ino = self.ino_for(&child);
let kind = entry_kind_to_file_type(e.kind);
all.push((child_ino, kind, e.name));
}
for (i, (child, kind, name)) in all.into_iter().enumerate().skip(offset as usize) {
if reply.add(child, (i + 1) as i64, kind, name) {
break;
}
}
reply.ok();
}
fn releasedir(
&mut self,
_req: &Request<'_>,
_ino: u64,
_fh: u64,
_flags: i32,
reply: ReplyEmpty,
) {
reply.ok();
}
fn statfs(&mut self, _req: &Request<'_>, _ino: u64, reply: ReplyStatfs) {
match self.fs.statfs(self.dev.as_mut()) {
Ok(s) => reply.statfs(
s.blocks,
s.blocks_free,
s.blocks_avail,
s.inodes,
s.inodes_free,
s.block_size,
s.name_max,
s.block_size,
),
Err(e) => reply.error(fs_err_to_errno(&e)),
}
}
fn create(
&mut self,
req: &Request<'_>,
parent: u64,
name: &OsStr,
mode: u32,
umask: u32,
_flags: i32,
reply: ReplyCreate,
) {
if !self.writable {
return reply.error(libc::EROFS);
}
let parent_path = match self.path_for(parent) {
Some(p) => p,
None => return reply.error(libc::ENOENT),
};
let child = Self::child_path(&parent_path, name);
let meta = FileMeta {
mode: (mode & 0o7777 & !(umask & 0o7777)) as u16,
uid: req.uid(),
gid: req.gid(),
mtime: Self::now_secs(),
atime: Self::now_secs(),
ctime: Self::now_secs(),
};
let res = self.fs.create_file(
self.dev.as_mut(),
&child,
crate::fs::FileSource::Zero(0),
meta,
);
match res {
Ok(()) => match self.fs.getattr(self.dev.as_mut(), &child) {
Ok(attrs) => {
let id = self.ino_for(&child);
let attr = self.make_attr(id, &attrs);
reply.created(&TTL, &attr, 0, 0, 0);
}
Err(_) => reply.error(libc::EIO),
},
Err(e) => reply.error(fs_err_to_errno(&e)),
}
}
fn access(&mut self, _req: &Request<'_>, _ino: u64, _mask: i32, reply: ReplyEmpty) {
reply.ok();
}
fn getxattr(
&mut self,
_req: &Request<'_>,
ino: u64,
name: &OsStr,
size: u32,
reply: ReplyXattr,
) {
let path = match self.path_for(ino) {
Some(p) => p,
None => return reply.error(libc::ENOENT),
};
let xattrs = match self.fs.list_xattrs(self.dev.as_mut(), &path) {
Ok(x) => x,
Err(e) => return reply.error(fs_err_to_errno(&e)),
};
let want = name.to_string_lossy();
let value = xattrs.into_iter().find(|x| x.name == want).map(|x| x.value);
match value {
Some(v) => {
if size == 0 {
reply.size(v.len() as u32);
} else if (size as usize) < v.len() {
reply.error(libc::ERANGE);
} else {
reply.data(&v);
}
}
None => reply.error(libc::ENODATA),
}
}
fn listxattr(&mut self, _req: &Request<'_>, ino: u64, size: u32, reply: ReplyXattr) {
let path = match self.path_for(ino) {
Some(p) => p,
None => return reply.error(libc::ENOENT),
};
let xattrs = match self.fs.list_xattrs(self.dev.as_mut(), &path) {
Ok(x) => x,
Err(e) => return reply.error(fs_err_to_errno(&e)),
};
let mut payload = Vec::new();
for x in &xattrs {
payload.extend_from_slice(x.name.as_bytes());
payload.push(0);
}
if size == 0 {
reply.size(payload.len() as u32);
} else if (size as usize) < payload.len() {
reply.error(libc::ERANGE);
} else {
reply.data(&payload);
}
}
fn setxattr(
&mut self,
_req: &Request<'_>,
ino: u64,
name: &OsStr,
value: &[u8],
_flags: i32,
_position: u32,
reply: ReplyEmpty,
) {
if !self.writable {
return reply.error(libc::EROFS);
}
let path = match self.path_for(ino) {
Some(p) => p,
None => return reply.error(libc::ENOENT),
};
let n = name.to_string_lossy();
match self
.fs
.set_xattr(self.dev.as_mut(), &path, n.as_ref(), value)
{
Ok(()) => reply.ok(),
Err(e) => reply.error(fs_err_to_errno(&e)),
}
}
fn removexattr(&mut self, _req: &Request<'_>, ino: u64, name: &OsStr, reply: ReplyEmpty) {
if !self.writable {
return reply.error(libc::EROFS);
}
let path = match self.path_for(ino) {
Some(p) => p,
None => return reply.error(libc::ENOENT),
};
let n = name.to_string_lossy();
match self.fs.remove_xattr(self.dev.as_mut(), &path, n.as_ref()) {
Ok(()) => reply.ok(),
Err(e) => reply.error(fs_err_to_errno(&e)),
}
}
}
impl FstoolFs {
fn reply_with_new_entry(&mut self, path: &Path, reply: ReplyEntry) {
match self.fs.getattr(self.dev.as_mut(), path) {
Ok(attrs) => {
let id = self.ino_for(path);
let attr = self.make_attr(id, &attrs);
reply.entry(&TTL, &attr, 0);
}
Err(_) => reply.error(libc::EIO),
}
}
}
#[allow(dead_code)]
fn _mutation_cap_touch(m: MutationCapability) -> bool {
m.supports_add_remove()
}