use std::cell::{Cell, RefCell};
use std::cmp::Reverse;
use std::collections::{BTreeMap, BTreeSet, BinaryHeap, HashMap, HashSet, VecDeque};
use std::ffi::OsStr;
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
use std::sync::{Arc, Condvar, Mutex, MutexGuard, OnceLock, PoisonError, Weak};
use std::time::{Duration, Instant, SystemTime};
use bytes::Bytes;
#[cfg(fuse)]
use fuser::{
CopyFileRangeFlags, FileHandle, Filesystem, FopenFlags, Generation, InitFlags, KernelConfig, LockOwner, OpenFlags,
ReplyAttr, ReplyCreate, ReplyData, ReplyDirectory, ReplyDirectoryPlus, ReplyEmpty, ReplyEntry, ReplyOpen,
ReplyStatfs, ReplyWrite, Request, WriteFlags,
};
use fuser::{Errno, FileAttr, FileType, INodeNo, RenameFlags, TimeOrNow};
use sandfs_core::{Error, Kind, VPath, Workspace};
use crate::persist::{Draft, Drafts, Saved, Snapshot};
use crate::store::subtree_keys;
#[cfg(fuse)]
fn missing() -> FileAttr {
let never = SystemTime::UNIX_EPOCH;
FileAttr {
ino: INodeNo(0),
size: 0,
blocks: 0,
atime: never,
mtime: never,
ctime: never,
crtime: never,
kind: FileType::RegularFile,
perm: 0,
nlink: 0,
uid: 0,
gid: 0,
rdev: 0,
blksize: 0,
flags: 0,
}
}
#[cfg(any(fuse, test))]
const TTL: Duration = Duration::from_secs(60);
pub const ROOT_INO: u64 = 1;
pub const MAX_FILE_SIZE: usize = i64::MAX as usize;
#[cfg_attr(not(fuse), allow(dead_code))] const COPY_PIECE: u64 = 8 << 20;
const MAX_MEMORY_FILE: u64 = 4 << 30;
fn since_start() -> u64 {
static START: OnceLock<Instant> = OnceLock::new();
START.get_or_init(Instant::now).elapsed().as_millis() as u64 + 1
}
const RECENT_FILES: usize = 16;
const RECENT_BYTES: usize = 64 << 20;
pub const SETTLE: Duration = Duration::from_millis(250);
#[cfg(any(fuse, test))]
const LISTINGS_KEPT: usize = 64;
#[cfg(target_os = "linux")]
const RENAME_NOREPLACE: u32 = libc::RENAME_NOREPLACE;
#[cfg(target_os = "linux")]
fn rename_unsupported(flags: RenameFlags) -> bool {
flags.bits() & (libc::RENAME_EXCHANGE | libc::RENAME_WHITEOUT) != 0
}
#[cfg(not(target_os = "linux"))]
fn rename_unsupported(_: RenameFlags) -> bool {
false
}
#[cfg(target_os = "linux")]
fn rename_noreplace(flags: RenameFlags) -> bool {
flags.bits() & RENAME_NOREPLACE != 0
}
#[cfg(not(target_os = "linux"))]
fn rename_noreplace(_: RenameFlags) -> bool {
false
}
pub type FsResult<T> = Result<T, Errno>;
pub fn errno(err: &Error) -> Errno {
match err {
Error::NotFound(_) => Errno::ENOENT,
Error::AlreadyExists(_) => Errno::EEXIST,
Error::NotADirectory(_) => Errno::ENOTDIR,
Error::IsADirectory(_) => Errno::EISDIR,
Error::DirectoryNotEmpty(_) => Errno::ENOTEMPTY,
Error::ReadOnly(_) => Errno::EROFS,
Error::InvalidPath(_) => Errno::EINVAL,
Error::Unsupported(_, reason) if reason.contains("symbolic link") => Errno::ELOOP,
Error::Unsupported(..) => Errno::EPERM,
other => {
eprintln!("sandfs: {other}");
Errno::EIO
}
}
}
pub enum Reading {
Read(Bytes),
Stored(sandfs_core::Stored),
}
impl Reading {
pub fn read(self, offset: u64, size: u32) -> FsResult<Bytes> {
match self {
Reading::Read(data) => Ok(data),
Reading::Stored(stored) => stored.source.read_range(&stored.key, offset, size.into()).fs(),
}
}
}
trait OrErrno<T> {
fn fs(self) -> FsResult<T>;
}
impl<T> OrErrno<T> for sandfs_core::Result<T> {
fn fs(self) -> FsResult<T> {
self.map_err(|err| errno(&err))
}
}
struct Node {
path: VPath,
lookups: u64,
linked: bool,
sized: Cell<bool>,
removed_from: Option<Box<Workspace>>,
}
struct DirHandle {
ino: u64,
entries: Vec<(u64, FileType, String)>,
#[cfg_attr(not(any(fuse, nfs, test)), allow(dead_code))]
sizeless: HashMap<String, bool>,
}
struct Buffer {
body: Body,
size: u64,
written: Instant,
drafts: Option<Drafts>,
}
enum Body {
Memory(Vec<u8>),
Draft(Draft),
}
impl Buffer {
fn new(body: Body, size: u64, drafts: Option<&Drafts>) -> Self {
Buffer { body, size, written: Instant::now(), drafts: drafts.cloned() }
}
fn make_room(&mut self, size: u64) -> FsResult<()> {
let Some(drafts) = self.drafts.as_ref().filter(|drafts| !drafts.in_memory(size)) else { return Ok(()) };
if let Body::Memory(data) = &self.body {
let mut draft = drafts.empty().map_err(draft_errno)?;
let written = draft.write_at(0, data).map_err(io_errno)?;
if written < data.len() {
return Err(Errno::ENOSPC);
}
self.body = Body::Draft(draft);
}
Ok(())
}
fn read(&self, offset: u64, len: u64) -> FsResult<Bytes> {
match &self.body {
Body::Memory(data) => {
let start = usize::try_from(offset).unwrap_or(usize::MAX).min(data.len());
let end = start.saturating_add(usize::try_from(len).unwrap_or(usize::MAX)).min(data.len());
Ok(Bytes::copy_from_slice(&data[start..end]))
}
Body::Draft(draft) => draft.read_at(offset, len).map_err(io_errno),
}
}
fn write(&mut self, offset: u64, data: &[u8]) -> FsResult<usize> {
let end = offset.checked_add(data.len() as u64).ok_or(Errno::EFBIG)?;
self.make_room(end)?;
let written = match &mut self.body {
Body::Memory(buffer) => {
let end = usize::try_from(end).ok().filter(|end| *end as u64 <= MAX_MEMORY_FILE).ok_or(Errno::EFBIG)?;
if buffer.len() < end {
buffer.resize(end, 0);
}
buffer[end - data.len()..end].copy_from_slice(data);
data.len()
}
Body::Draft(draft) => draft.write_at(offset, data).map_err(io_errno)?,
};
self.size = self.size.max(offset + written as u64);
Ok(written)
}
fn set_len(&mut self, size: u64) -> FsResult<()> {
self.make_room(size)?;
match &mut self.body {
Body::Memory(data) => {
let size = usize::try_from(size).ok().filter(|size| *size as u64 <= MAX_MEMORY_FILE);
data.resize(size.ok_or(Errno::EFBIG)?, 0);
}
Body::Draft(draft) => draft.set_len(size).map_err(io_errno)?,
}
self.size = size;
Ok(())
}
fn commit(self, ws: &mut Workspace, path: &str) -> FsResult<u64> {
match self.body {
Body::Memory(data) => ws.write(path, Bytes::from(data)).fs(),
Body::Draft(draft) => ws.write_stored(path, draft.finish().map_err(io_errno)?).fs(),
}
}
}
fn io_errno(err: std::io::Error) -> Errno {
eprintln!("sandfs: writing a file failed: {err}");
Errno::from(err)
}
fn draft_errno(err: anyhow::Error) -> Errno {
match err.downcast::<std::io::Error>() {
Ok(err) => io_errno(err),
Err(err) => {
eprintln!("sandfs: writing a file failed: {err:#}");
Errno::EIO
}
}
}
pub struct Replacement {
pub ws: Workspace,
pub saved: Saved,
pub times: BTreeMap<String, SystemTime>,
pub dirs: BTreeSet<String>,
pub changed: Option<BTreeSet<String>>,
pub whole: Vec<VPath>,
}
#[derive(Debug, PartialEq)]
#[cfg_attr(not(fuse), allow(dead_code))] pub enum Stale {
Inode(u64),
Entry(u64, String),
}
pub struct Inner {
ws: Workspace,
nodes: HashMap<u64, Node>,
inos: BTreeMap<VPath, u64>,
next_ino: u64,
handles: HashMap<u64, DirHandle>,
next_fh: u64,
#[cfg(any(fuse, test))]
listings: HashMap<u64, u64>,
#[cfg(any(fuse, test))]
pub keep_listed: bool,
#[cfg(any(fuse, test))]
stale_listings: Vec<u64>,
open: HashMap<u64, u32>,
buffers: HashMap<u64, Buffer>,
pub drafts: Option<Drafts>,
times: BTreeMap<String, SystemTime>,
dirs: BTreeSet<String>,
base_time: SystemTime,
uid: u32,
gid: u32,
unsaved: bool,
writing: bool,
pub ranged_reads: bool,
recent: RefCell<VecDeque<(u64, u64, Bytes)>>,
pub keep_removed: bool,
pub renumber_changed: bool,
moved_off: Vec<u64>,
pub skip_bookkeeping: bool,
#[cfg(any(nfs, test))]
mounted: Workspace,
}
impl Inner {
pub fn new(
ws: Workspace,
root: &VPath,
times: BTreeMap<String, SystemTime>,
dirs: BTreeSet<String>,
base_time: SystemTime,
(uid, gid): (u32, u32),
) -> Self {
let mut inner = Inner {
#[cfg(any(nfs, test))]
mounted: ws.fork(),
ws,
nodes: HashMap::new(),
inos: BTreeMap::new(),
next_ino: ROOT_INO + 1,
handles: HashMap::new(),
#[cfg(any(fuse, test))]
listings: HashMap::new(),
#[cfg(any(fuse, test))]
keep_listed: false,
#[cfg(any(fuse, test))]
stale_listings: Vec::new(),
next_fh: 1,
open: HashMap::new(),
buffers: HashMap::new(),
drafts: None,
times,
dirs,
base_time,
uid,
gid,
unsaved: false,
writing: false,
ranged_reads: false,
recent: RefCell::default(),
keep_removed: true,
renumber_changed: false,
moved_off: Vec::new(),
skip_bookkeeping: false,
};
inner.inos.insert(root.clone(), ROOT_INO);
inner.nodes.insert(
ROOT_INO,
Node { path: root.clone(), lookups: 0, linked: true, sized: Cell::new(false), removed_from: None },
);
inner
}
pub fn trim(&mut self) {
self.ws.trim();
let recent = std::mem::take(&mut *self.recent.borrow_mut());
drop(recent);
self.nodes.shrink_to_fit();
self.handles.shrink_to_fit();
self.open.shrink_to_fit();
self.buffers.shrink_to_fit();
}
pub fn snapshot(&mut self, settle: Duration) -> Snapshot {
self.commit_written(settle);
self.unsaved = false;
Snapshot { ws: self.ws.fork(), times: self.times.clone(), dirs: self.dirs.clone() }
}
pub fn replace(
&mut self,
ws: Workspace,
times: BTreeMap<String, SystemTime>,
dirs: BTreeSet<String>,
changed: Option<&BTreeSet<String>>,
whole: &[VPath],
) -> Vec<Stale> {
let old = std::mem::replace(&mut self.ws, ws);
#[cfg(any(nfs, test))]
{
self.mounted = self.ws.fork();
}
for ino in std::mem::take(&mut self.moved_off) {
if let Some(node) = self.nodes.get_mut(&ino) {
node.lookups = 0;
node.removed_from = None;
}
self.maybe_drop(ino);
}
self.times = times;
self.dirs = dirs;
self.unsaved = false;
self.recent.borrow_mut().clear();
let Some(root) = self.nodes.get(&ROOT_INO).map(|node| node.path.clone()) else { return Vec::new() };
let mut paths = changed.cloned().unwrap_or_default();
let in_whole = |path: &VPath| changed.is_none() || whole.iter().any(|folder| path.starts_with(folder));
let known: Vec<VPath> = self.inos.keys().cloned().collect();
for path in known.iter().filter(|path| in_whole(path)) {
paths.insert(path.to_string());
for ws in [&old, &self.ws] {
for entry in ws.list(path.as_str()).unwrap_or_default() {
paths.extend(path.join(&entry.name).ok().map(|child| child.to_string()));
}
}
}
let shape = |ws: &Workspace, path: &VPath| ws.stat(path.as_str()).ok().map(|meta| file_type(meta.kind).0);
let now = SystemTime::now();
let mut stale = Vec::new();
let mut looked: HashMap<VPath, bool> = HashMap::new();
let mut moved: HashSet<u64> = HashSet::new();
for path in paths.iter().filter_map(|path| VPath::parse(path).ok()) {
if !path.starts_with(&root) || path == root {
continue;
}
let below_root = path.ancestors().into_iter().filter(|above| above.starts_with(&root) && *above != root);
for name in below_root.chain([path.clone()]) {
let parent = name.parent().and_then(|parent| self.inos.get(parent.as_str()).copied());
let Some(parent) = parent else { break };
let went = match looked.get(&name) {
Some(went) => *went,
None => {
let (before, now_is) = (shape(&old, &name), shape(&self.ws, &name));
let renumbered = self.renumber_changed
&& name == path
&& before.is_some_and(|kind| kind != FileType::Directory)
&& self.inos.contains_key(name.as_str());
let went = before != now_is || renumbered;
looked.insert(name.clone(), went);
if went {
stale.push(Stale::Entry(parent, name.name().unwrap_or_default().to_owned()));
stale.push(Stale::Inode(parent));
self.times.extend(name.parent().map(|parent| (parent.to_string(), now)));
for gone in subtree_keys(&self.inos, &name) {
let Some(ino) = self.inos.get(gone.as_str()).copied() else { continue };
let others = self.other_names(&gone);
let keep_removed = std::mem::replace(&mut self.keep_removed, true);
self.unlink_path(&gone, Some(old.fork()), &others);
self.keep_removed = keep_removed;
moved.insert(ino);
if !keep_removed && self.nodes.get(&ino).is_some_and(|node| !node.linked) {
self.moved_off.push(ino);
}
}
} else if name == path
&& let Some(&ino) = self.inos.get(name.as_str())
{
stale.push(Stale::Inode(ino));
}
went
}
};
if went {
break;
}
}
}
let unlinked = |ino: &u64| moved.contains(ino) && self.nodes.get(ino).is_none_or(|node| !node.linked);
let dangling: Vec<VPath> =
self.inos.iter().filter(|(_, ino)| unlinked(ino)).map(|(path, _)| path.clone()).collect();
for name in dangling {
self.inos.remove(name.as_str());
let parent = name.parent().and_then(|parent| self.inos.get(parent.as_str()).copied());
if let Some(parent) = parent {
stale.push(Stale::Entry(parent, name.name().unwrap_or_default().to_owned()));
stale.push(Stale::Inode(parent));
self.times.extend(name.parent().map(|parent| (parent.to_string(), now)));
}
}
#[cfg(any(fuse, test))]
stale.extend(self.take_stale_listings().into_iter().map(Stale::Inode));
stale
}
#[cfg(any(nfs, test))]
pub fn is_added_appledouble(&self, parent: u64, name: &str, kind: FileType) -> bool {
self.skip_bookkeeping
&& kind == FileType::RegularFile
&& name.starts_with("._")
&& self.child(parent, OsStr::new(name)).is_ok_and(|path| !self.mounted.exists(path.as_str()))
}
#[cfg(any(nfs, test))]
pub fn clear_appledouble(&mut self, ino: u64) -> FsResult<()> {
let path = self.path(ino)?;
let entries = self.ws.list(path.as_str()).fs()?;
let all_added = entries.iter().all(|entry| {
matches!(entry.kind, Kind::File { .. })
&& self.is_added_appledouble(ino, &entry.name, FileType::RegularFile)
});
if all_added {
for entry in entries {
self.unlink(ino, OsStr::new(&entry.name))?;
}
}
Ok(())
}
#[cfg(any(nfs, test))]
pub fn parent_of(&mut self, ino: u64) -> Option<u64> {
if ino == ROOT_INO {
return Some(ROOT_INO);
}
let parent = self.path(ino).ok()?.parent()?;
Some(self.ino_for(&parent))
}
#[cfg(test)]
pub fn node_count(&self) -> usize {
self.nodes.len()
}
#[cfg(any(nfs, test))]
pub fn last_name_of(&self, ino: u64) -> Option<String> {
self.nodes.get(&ino)?.path.name().map(str::to_owned)
}
#[cfg(any(nfs, test))]
pub fn hold(&mut self, ino: u64) {
if let Some(node) = self.nodes.get_mut(&ino) {
node.lookups += 1;
}
}
fn check_writable(&self, ino: u64) -> FsResult<()> {
match self.nodes.get(&ino) {
Some(node) if node.linked && self.ws.is_read_only(node.path.as_str()) => Err(Errno::EROFS),
_ => Ok(()),
}
}
fn path(&self, ino: u64) -> FsResult<VPath> {
match self.nodes.get(&ino) {
Some(node) if node.linked => Ok(node.path.clone()),
_ => Err(Errno::ENOENT),
}
}
fn child(&self, parent: u64, name: &OsStr) -> FsResult<VPath> {
let name = name.to_str().ok_or(Errno::EINVAL)?;
self.path(parent)?.join(name).map_err(|_| Errno::EINVAL)
}
fn ino_for(&mut self, path: &VPath) -> u64 {
if let Some(&ino) = self.inos.get(path.as_str()) {
return ino;
}
if let Some(ino) = self.linked_ino(path) {
self.inos.insert(path.clone(), ino);
return ino;
}
let ino = self.next_ino;
self.next_ino += 1;
self.inos.insert(path.clone(), ino);
self.nodes.insert(
ino,
Node { path: path.clone(), lookups: 0, linked: true, sized: Cell::new(false), removed_from: None },
);
ino
}
fn linked_ino(&self, path: &VPath) -> Option<u64> {
if !self.ws.has_hard_links() {
return None;
}
let names = self.ws.hard_links(path.as_str()).ok()?;
names.iter().find_map(|name| self.inos.get(name.as_str()).copied())
}
fn other_names(&self, path: &VPath) -> Vec<VPath> {
if !self.ws.has_hard_links() {
return Vec::new();
}
let mut names = self.ws.hard_links(path.as_str()).unwrap_or_default();
names.retain(|name| name != path);
names
}
fn entry(&mut self, path: &VPath) -> FsResult<FileAttr> {
let ino = self.ino_for(path);
let attr = self.attr(ino)?;
if let Some(node) = self.nodes.get_mut(&ino) {
node.lookups += 1;
}
Ok(attr)
}
pub fn attr(&self, ino: u64) -> FsResult<FileAttr> {
let node = self.nodes.get(&ino).ok_or(Errno::ENOENT)?;
if !node.linked {
let (kind, perm, size, modified) = match (self.buffers.get(&ino), &node.removed_from) {
(Some(buffer), _) => (FileType::RegularFile, 0o644, buffer.size, None),
(None, Some(before)) => {
let meta = before.stat(node.path.as_str()).fs()?;
let (kind, perm) = file_type(meta.kind);
(kind, perm, meta.size, meta.modified)
}
(None, None) => return Err(Errno::ENOENT),
};
node.sized.set(true);
return Ok(self.make_attr(ino, kind, perm, size, modified.unwrap_or(self.base_time)));
}
let path = node.path.as_str();
let meta = self.ws.stat(path).fs()?;
node.sized.set(true);
let size = match self.buffers.get(&ino) {
Some(buffer) => buffer.size,
None if meta.kind.is_dir() => 0,
None => meta.size,
};
let (kind, mut perm) = file_type(meta.kind);
if kind != FileType::Symlink && self.ws.is_read_only(path) {
perm &= !0o222;
}
let mtime = self.times.get(path).copied().or(meta.modified).unwrap_or(self.base_time);
let mut attr = self.make_attr(ino, kind, perm, size, mtime);
if kind != FileType::Directory {
attr.nlink = self.nlink(path);
}
Ok(attr)
}
fn nlink(&self, path: &str) -> u32 {
if !self.ws.has_hard_links() {
return 1;
}
self.ws.hard_links(path).map_or(1, |names| names.len() as u32)
}
fn set_time(&mut self, path: &VPath, time: SystemTime) {
for other in self.other_names(path) {
self.times.insert(other.to_string(), time);
}
self.times.insert(path.to_string(), time);
}
fn keep_source_time(&mut self, path: &VPath) {
if !self.times.contains_key(path.as_str())
&& let Ok(Some(time)) = self.ws.modified(path.as_str())
{
self.times.insert(path.to_string(), time);
}
}
#[cfg(any(fuse, test))]
fn sizeless_attr(&self, ino: u64, executable: bool) -> FsResult<FileAttr> {
let node = self.nodes.get(&ino).ok_or(Errno::ENOENT)?;
let path = node.path.as_str();
if !node.linked || !self.ws.exists(path) {
return Err(Errno::ENOENT);
}
let mtime = self.times.get(path).copied().unwrap_or(self.base_time);
let mut attr = self.make_attr(ino, FileType::RegularFile, if executable { 0o755 } else { 0o644 }, 0, mtime);
attr.nlink = self.nlink(path);
Ok(attr)
}
fn make_attr(&self, ino: u64, kind: FileType, perm: u16, size: u64, mtime: SystemTime) -> FileAttr {
FileAttr {
ino: INodeNo(ino),
size,
blocks: size.div_ceil(512),
atime: mtime,
mtime,
ctime: mtime,
crtime: mtime,
kind,
perm,
nlink: if kind == FileType::Directory { 2 } else { 1 },
uid: self.uid,
gid: self.gid,
rdev: 0,
blksize: 4096,
flags: 0,
}
}
pub fn lookup(&mut self, parent: u64, name: &OsStr) -> FsResult<FileAttr> {
let path = self.child(parent, name)?;
self.ws.stat(path.as_str()).fs()?;
self.entry(&path)
}
#[cfg(any(fuse, test))]
pub fn take_stale_listings(&mut self) -> Vec<u64> {
let mut folders = std::mem::take(&mut self.stale_listings);
folders.retain(|folder| self.nodes.contains_key(folder));
folders.sort_unstable();
folders.dedup();
folders
}
#[cfg(any(fuse, test))]
pub fn forget(&mut self, ino: u64, count: u64) {
if ino == ROOT_INO {
return;
}
if let Some(node) = self.nodes.get_mut(&ino) {
node.lookups = node.lookups.saturating_sub(count);
}
self.maybe_drop(ino);
}
fn is_open(&self, ino: u64) -> bool {
self.open.get(&ino).is_some_and(|count| *count > 0)
}
fn maybe_drop(&mut self, ino: u64) {
let unused = self.nodes.get(&ino).is_some_and(|node| node.lookups == 0) && !self.is_open(ino);
if unused && ino != ROOT_INO {
if let Some(node) = self.nodes.remove(&ino) {
if node.linked
&& let Some(buffer) = self.buffers.remove(&ino)
{
let _ = buffer.commit(&mut self.ws, node.path.as_str());
self.unsaved = true;
}
#[cfg(any(fuse, test))]
if self.keep_listed && node.linked {
let parent = node.path.parent().and_then(|parent| self.inos.get(parent.as_str()));
self.stale_listings.extend(parent.filter(|parent| self.nodes.contains_key(parent)));
}
let others = if node.linked { self.other_names(&node.path) } else { Vec::new() };
for name in others.iter().chain([&node.path]) {
if self.inos.get(name.as_str()) == Some(&ino) {
self.inos.remove(name.as_str());
}
}
}
self.buffers.remove(&ino);
if self.nodes.capacity() > 4096 && self.nodes.len() < self.nodes.capacity() / 4 {
self.nodes.shrink_to(self.nodes.len() * 2);
}
}
}
fn unlink_path(&mut self, path: &VPath, before: Option<Workspace>, others: &[VPath]) {
let Some(ino) = self.inos.remove(path.as_str()) else { return };
if let Some(other) = others.first() {
if let Some(node) = self.nodes.get_mut(&ino)
&& node.path == *path
{
node.path = other.clone();
}
self.inos.entry(other.clone()).or_insert(ino);
return;
}
if let Some(node) = self.nodes.get_mut(&ino) {
node.linked = false;
node.removed_from = before.filter(|_| self.keep_removed).map(Box::new);
if !self.keep_removed {
node.lookups = 0;
}
}
if !self.keep_removed {
self.buffers.remove(&ino);
}
self.maybe_drop(ino);
}
fn touch(&mut self, path: &VPath) {
self.times.insert(path.to_string(), SystemTime::now());
if let Some(parent) = path.parent() {
self.times.insert(parent.to_string(), SystemTime::now());
}
self.unsaved = true;
}
pub fn setattr(
&mut self,
ino: u64,
mode: Option<u32>,
size: Option<u64>,
mtime: Option<TimeOrNow>,
) -> FsResult<FileAttr> {
if size.is_some() || mtime.is_some() {
self.check_writable(ino)?;
}
if let Some(size) = size {
if size > MAX_FILE_SIZE as u64 {
return Err(Errno::EFBIG);
}
let (ws, path) = self.contents(ino)?;
match ws.read_link(path.as_str()) {
Ok(_) => return Err(Errno::EINVAL),
Err(Error::Unsupported(..)) => {}
Err(err) => return Err(errno(&err)),
}
if size == 0 {
let empty = self.empty_buffer()?;
self.buffers.insert(ino, empty);
self.writing = true;
} else {
self.change_buffer(ino, |buffer| buffer.set_len(size))?;
}
if !self.nodes.get(&ino).is_some_and(|node| node.linked) {
return self.attr(ino);
}
let path = self.path(ino)?;
self.touch(&path);
}
if let Some(mode) = mode {
let path = self.path(ino)?;
if let Kind::File { .. } = self.ws.stat(path.as_str()).fs()?.kind {
self.keep_source_time(&path);
self.ws.set_executable(path.as_str(), mode & 0o111 != 0).fs()?;
self.unsaved = true;
}
}
if let Some(mtime) = mtime {
let path = self.path(ino)?;
let time = match mtime {
TimeOrNow::Now => SystemTime::now(),
TimeOrNow::SpecificTime(time) => time,
};
self.set_time(&path, time);
self.unsaved = true;
}
self.attr(ino)
}
pub fn mkdir(&mut self, parent: u64, name: &OsStr) -> FsResult<FileAttr> {
let path = self.child(parent, name)?;
self.ws.mkdir(path.as_str(), false).fs()?;
self.dirs.insert(path.to_string());
self.touch(&path);
self.entry(&path)
}
pub fn symlink(&mut self, parent: u64, name: &OsStr, target: &OsStr) -> FsResult<FileAttr> {
let path = self.child(parent, name)?;
let target = target.to_str().ok_or(Errno::EINVAL)?;
self.ws.symlink(path.as_str(), target).fs()?;
self.touch(&path);
self.entry(&path)
}
pub fn readlink(&self, ino: u64) -> FsResult<String> {
let (ws, path) = self.contents(ino)?;
ws.read_link(path.as_str()).map_err(|err| match err {
Error::Unsupported(..) => Errno::EINVAL,
err => errno(&err),
})
}
pub fn link(&mut self, ino: u64, new_parent: u64, new_name: &OsStr) -> FsResult<FileAttr> {
let from = self.path(ino)?;
let to = self.child(new_parent, new_name)?;
self.ws.hard_link(from.as_str(), to.as_str()).fs()?;
self.inos.insert(to.clone(), ino);
self.keep_source_time(&from);
match self.times.get(from.as_str()).copied() {
Some(time) => self.times.insert(to.to_string(), time),
None => self.times.remove(to.as_str()),
};
self.touch_parent(&to);
self.entry(&to)
}
pub fn unlink(&mut self, parent: u64, name: &OsStr) -> FsResult<()> {
let path = self.child(parent, name)?;
if self.ws.stat(path.as_str()).fs()?.kind.is_dir() {
return Err(Errno::EISDIR);
}
let before = self.ws.fork();
let others = self.other_names(&path);
self.ws.remove(path.as_str(), false).fs()?;
self.unlink_path(&path, Some(before), &others);
self.times.remove(path.as_str());
self.touch_parent(&path);
Ok(())
}
pub fn rmdir(&mut self, parent: u64, name: &OsStr) -> FsResult<()> {
let path = self.child(parent, name)?;
if !self.ws.stat(path.as_str()).fs()?.kind.is_dir() {
return Err(Errno::ENOTDIR);
}
self.ws.remove(path.as_str(), false).fs()?;
self.unlink_path(&path, None, &[]);
self.times.remove(path.as_str());
self.dirs.insert(path.to_string());
self.touch_parent(&path);
Ok(())
}
fn touch_parent(&mut self, path: &VPath) {
if let Some(parent) = path.parent() {
self.times.insert(parent.to_string(), SystemTime::now());
}
self.unsaved = true;
}
pub fn rename(
&mut self,
parent: u64,
name: &OsStr,
new_parent: u64,
new_name: &OsStr,
flags: RenameFlags,
) -> FsResult<()> {
if rename_unsupported(flags) {
return Err(Errno::EINVAL);
}
let from = self.child(parent, name)?;
let to = self.child(new_parent, new_name)?;
let moving_dir = self.ws.stat(from.as_str()).fs()?.kind.is_dir();
if from == to {
return Ok(());
}
if to.starts_with(&from) {
return Err(Errno::EINVAL);
}
let replacing = match self.ws.stat(to.as_str()) {
Ok(meta) => {
if rename_noreplace(flags) {
return Err(Errno::EEXIST);
}
if !moving_dir && self.other_names(&to).contains(&from) {
return Ok(());
}
match (moving_dir, meta.kind.is_dir()) {
(true, false) => return Err(Errno::ENOTDIR),
(false, true) => return Err(Errno::EISDIR),
(true, true) if !self.ws.list(to.as_str()).fs()?.is_empty() => return Err(Errno::ENOTEMPTY),
_ => true,
}
}
Err(Error::NotFound(_)) => false,
Err(err) => return Err(errno(&err)),
};
let mut next = self.ws.fork();
let others = if replacing && !moving_dir { self.other_names(&to) } else { Vec::new() };
if replacing {
next.remove(to.as_str(), true).fs()?;
}
next.rename(from.as_str(), to.as_str()).fs()?;
if replacing {
let before = (!moving_dir).then(|| self.ws.fork());
self.unlink_path(&to, before, &others);
self.times.remove(to.as_str());
}
self.ws = next;
self.move_subtree(&from, &to);
if moving_dir {
let mut stack = vec![to.clone()];
while let Some(dir) = stack.pop() {
for entry in self.ws.list(dir.as_str()).fs()? {
let child = dir.join(&entry.name).map_err(|_| Errno::EINVAL)?;
if entry.kind.is_dir() {
stack.push(child);
} else {
self.keep_source_time(&child);
}
}
self.dirs.insert(dir.to_string());
}
self.dirs.insert(from.to_string());
} else {
self.keep_source_time(&to);
}
self.touch_parent(&from);
self.touch_parent(&to);
Ok(())
}
fn move_subtree(&mut self, from: &VPath, to: &VPath) {
let moved = |map_keys: Vec<String>| -> Vec<(String, String)> {
map_keys.into_iter().map(|key| (key.clone(), format!("{to}{}", &key[from.as_str().len()..]))).collect()
};
for old in subtree_keys(&self.inos, from) {
let Ok(new) = VPath::parse(&format!("{to}{}", &old.as_str()[from.as_str().len()..])) else { continue };
if let Some(ino) = self.inos.remove(&old) {
if let Some(node) = self.nodes.get_mut(&ino) {
node.path = new.clone();
}
self.inos.insert(new, ino);
}
}
let time_keys = moved(subtree_keys(&self.times, from));
for (old, new) in time_keys {
if let Some(time) = self.times.remove(&old) {
self.times.insert(new, time);
}
}
}
pub fn create(
&mut self,
parent: u64,
name: &OsStr,
mode: u32,
exclusive: bool,
truncate: bool,
) -> FsResult<FileAttr> {
let path = self.child(parent, name)?;
match self.ws.stat(path.as_str()) {
Ok(_) if exclusive => return Err(Errno::EEXIST),
Ok(meta) if meta.kind.is_dir() => return Err(Errno::EISDIR),
Ok(meta) if truncate && meta.kind == Kind::Symlink => return Err(Errno::ELOOP),
Ok(_) => {
if truncate {
if self.ws.is_read_only(path.as_str()) {
return Err(Errno::EROFS);
}
let ino = self.ino_for(&path);
let empty = self.empty_buffer()?;
self.buffers.insert(ino, empty);
self.writing = true;
self.touch(&path);
}
}
Err(Error::NotFound(_)) => {
self.ws.write(path.as_str(), Bytes::new()).fs()?;
if mode & 0o111 != 0 {
self.ws.set_executable(path.as_str(), true).fs()?;
}
self.touch(&path);
}
Err(err) => return Err(errno(&err)),
}
self.entry(&path)
}
pub fn recheck(&mut self, ino: u64) -> FsResult<bool> {
if self.buffers.contains_key(&ino) {
return Ok(false);
}
let Some(Node { linked: true, path, .. }) = self.nodes.get(&ino) else { return Ok(false) };
let path = path.clone();
let changed = self.ws.recheck(path.as_str()).fs()?;
if changed && self.renumber_changed {
let now = SystemTime::now();
for name in self.other_names(&path).iter().chain([&path]) {
if self.inos.get(name.as_str()) == Some(&ino) {
self.inos.remove(name.as_str());
}
self.times.extend(name.parent().map(|parent| (parent.to_string(), now)));
}
if let Some(node) = self.nodes.get_mut(&ino) {
(node.linked, node.lookups) = (false, 0);
}
self.maybe_drop(ino);
}
Ok(changed)
}
fn contents(&self, ino: u64) -> FsResult<(&Workspace, &VPath)> {
match self.nodes.get(&ino) {
Some(Node { linked: true, path, .. }) => Ok((&self.ws, path)),
Some(Node { removed_from: Some(before), path, .. }) => Ok((&**before, path)),
_ => Err(Errno::ENOENT),
}
}
#[cfg(test)]
pub fn read(&self, ino: u64, offset: u64, size: u32) -> FsResult<Bytes> {
self.reading(ino, offset, size)?.read(offset, size)
}
pub fn reading(&self, ino: u64, offset: u64, size: u32) -> FsResult<Reading> {
let range = |len: usize| {
let start = usize::try_from(offset).unwrap_or(usize::MAX).min(len);
start..start.saturating_add(size as usize).min(len)
};
if let Some(buffer) = self.buffers.get(&ino) {
return buffer.read(offset, size.into()).map(Reading::Read);
}
let (ws, path) = self.contents(ino)?;
if let Some(stored) = ws.stored(path.as_str()).fs()? {
return Ok(Reading::Stored(stored));
}
if self.ranged_reads {
return ws.read_range(path.as_str(), offset, size.into()).fs().map(Reading::Read);
}
let meta = ws.stat(path.as_str()).fs()?;
let data = match self.kept(ino, meta.version) {
Some(data) => data,
None => {
let data = ws.read_range(path.as_str(), 0, meta.size).fs()?;
self.remember(ino, meta.version, data.clone());
data
}
};
Ok(Reading::Read(data.slice(range(data.len()))))
}
fn kept(&self, ino: u64, version: u64) -> Option<Bytes> {
let mut recent = self.recent.borrow_mut();
let at = recent.iter().position(|(i, v, _)| (*i, *v) == (ino, version))?;
let entry = recent.remove(at)?;
let data = entry.2.clone();
recent.push_back(entry);
Some(data)
}
fn remember(&self, ino: u64, version: u64, data: Bytes) {
let mut recent = self.recent.borrow_mut();
recent.retain(|(i, _, _)| *i != ino);
recent.push_back((ino, version, data));
let mut total: usize = recent.iter().map(|(_, _, data)| data.len()).sum();
while recent.len() > 1 && (recent.len() > RECENT_FILES || total > RECENT_BYTES) {
total -= recent.pop_front().map_or(0, |(_, _, data)| data.len());
}
}
fn empty_buffer(&self) -> FsResult<Buffer> {
let body = match &self.drafts {
Some(drafts) if !drafts.in_memory(0) => Body::Draft(drafts.empty().map_err(draft_errno)?),
_ => Body::Memory(Vec::new()),
};
Ok(Buffer::new(body, 0, self.drafts.as_ref()))
}
fn buffer(&mut self, ino: u64) -> FsResult<&mut Buffer> {
if !self.buffers.contains_key(&ino) {
self.check_writable(ino)?;
let (ws, path) = self.contents(ino)?;
let size = ws.stat(path.as_str()).fs()?.size;
let body = match &self.drafts {
Some(drafts) if !drafts.in_memory(size) => {
Body::Draft(drafts.copy(ws, path.as_str(), self.ranged_reads).map_err(draft_errno)?)
}
Some(_) => Body::Memory(ws.read_range(path.as_str(), 0, size).fs()?.to_vec()),
None if size > MAX_MEMORY_FILE => return Err(Errno::EFBIG),
None => Body::Memory(ws.read_range(path.as_str(), 0, size).fs()?.to_vec()),
};
self.buffers.insert(ino, Buffer::new(body, size, self.drafts.as_ref()));
}
self.writing = true;
let buffer = self.buffers.get_mut(&ino).ok_or(Errno::EIO)?;
buffer.written = Instant::now();
Ok(buffer)
}
fn change_buffer<T>(&mut self, ino: u64, change: impl FnOnce(&mut Buffer) -> FsResult<T>) -> FsResult<T> {
let new = !self.buffers.contains_key(&ino);
let result = change(self.buffer(ino)?);
if result.is_err() && new {
self.buffers.remove(&ino);
}
result
}
pub fn write(&mut self, ino: u64, offset: u64, data: &[u8]) -> FsResult<u32> {
if offset.checked_add(data.len() as u64).is_none_or(|end| end > MAX_FILE_SIZE as u64) {
return Err(Errno::EFBIG);
}
let written = self.change_buffer(ino, |buffer| buffer.write(offset, data))?;
if let Ok(path) = self.path(ino) {
self.set_time(&path, SystemTime::now());
}
Ok(u32::try_from(written).unwrap_or(u32::MAX))
}
#[cfg_attr(not(fuse), allow(dead_code))]
pub fn copy_range(&mut self, src: u64, offset_in: u64, dst: u64, offset_out: u64, len: u64) -> FsResult<u32> {
self.check_writable(dst)?;
let size = match self.buffers.get(&src) {
Some(buffer) => buffer.size,
None => {
let (ws, path) = self.contents(src)?;
ws.stat(path.as_str()).fs()?.size
}
};
let linked = |ino| matches!(self.nodes.get(&ino), Some(Node { linked: true, .. }));
if offset_in == 0 && offset_out == 0 && len >= size && linked(src) && linked(dst) && self.attr(dst)?.size == 0 {
let (from, to) = (self.path(src)?, self.path(dst)?);
if let Some(buffer) = self.buffers.remove(&src) {
buffer.commit(&mut self.ws, from.as_str())?;
}
let executable = matches!(self.ws.stat(to.as_str()).fs()?.kind, Kind::File { executable: true });
let stored = match (&self.drafts, self.ws.stored(from.as_str()).fs()?) {
(Some(drafts), None) if !drafts.in_memory(size) => {
let draft = drafts.copy(&self.ws, from.as_str(), self.ranged_reads).map_err(draft_errno)?;
Some(draft.finish().map_err(io_errno)?)
}
_ => None,
};
self.buffers.remove(&dst);
match stored {
Some(stored) => {
self.ws.write_stored(to.as_str(), stored).fs()?;
}
None => {
self.ws.remove(to.as_str(), false).fs()?;
self.ws.copy(from.as_str(), to.as_str(), false).fs()?;
}
}
self.ws.set_executable(to.as_str(), executable).fs()?;
self.times.insert(to.to_string(), SystemTime::now());
self.unsaved = true;
return Ok(u32::try_from(size).unwrap_or(u32::MAX));
}
let len = len.min(COPY_PIECE);
let piece = match self.buffers.get(&src) {
Some(buffer) => buffer.read(offset_in, len)?,
None => {
let (ws, path) = self.contents(src)?;
ws.read_range(path.as_str(), offset_in, len).fs()?
}
};
if piece.is_empty() {
return Ok(0);
}
self.write(dst, offset_out, &piece)
}
pub fn commit_written(&mut self, settle: Duration) {
let now = Instant::now();
let mut waiting = false;
let inos: Vec<u64> = self.buffers.keys().copied().collect();
for ino in inos {
let Ok(path) = self.path(ino) else { continue };
if now.duration_since(self.buffers[&ino].written) < settle {
waiting = true;
continue;
}
let Some(buffer) = self.buffers.remove(&ino) else { continue };
let _ = buffer.commit(&mut self.ws, path.as_str());
self.unsaved = true;
}
self.writing = waiting;
}
pub fn opendir(&mut self, ino: u64) -> FsResult<u64> {
let path = self.path(ino)?;
let listing = self.ws.list(path.as_str()).fs()?;
let parent = match path.parent() {
Some(parent) if ino != ROOT_INO => self.ino_for(&parent),
_ => ROOT_INO,
};
let mut entries =
vec![(ino, FileType::Directory, ".".to_owned()), (parent, FileType::Directory, "..".to_owned())];
let mut sizeless = HashMap::new();
for entry in listing {
if let (Kind::File { executable }, None) = (entry.kind, entry.size) {
sizeless.insert(entry.name.clone(), executable);
}
let child = path.join(&entry.name).map_err(|_| Errno::EIO)?;
let kind = match entry.kind {
Kind::Dir => FileType::Directory,
Kind::File { .. } => FileType::RegularFile,
Kind::Symlink => FileType::Symlink,
};
entries.push((self.ino_for(&child), kind, entry.name));
}
let fh = self.next_fh;
self.next_fh += 1;
self.handles.insert(fh, DirHandle { ino, entries, sizeless });
*self.open.entry(ino).or_default() += 1;
Ok(fh)
}
pub fn readdir(&self, fh: u64, offset: u64) -> FsResult<&[(u64, FileType, String)]> {
let handle = self.handles.get(&fh).ok_or(Errno::EBADF)?;
Ok(handle.entries.get(usize::try_from(offset).unwrap_or(usize::MAX)..).unwrap_or_default())
}
#[cfg(any(nfs, test))]
pub fn sizeless(&self, fh: u64) -> FsResult<HashSet<u64>> {
let handle = self.handles.get(&fh).ok_or(Errno::EBADF)?;
let sizeless = handle.entries.iter().filter(|(ino, _, name)| {
handle.sizeless.contains_key(name)
&& !self.buffers.contains_key(ino)
&& !self.nodes.get(ino).is_some_and(|node| node.sized.get())
});
Ok(sizeless.map(|(ino, ..)| *ino).collect())
}
#[cfg(any(fuse, test))]
pub fn readdirplus(
&mut self,
fh: u64,
offset: u64,
mut add: impl FnMut(u64, u64, &str, &FileAttr, Duration) -> bool,
) -> FsResult<()> {
let mut position = usize::try_from(offset).unwrap_or(usize::MAX);
loop {
let Some(DirHandle { ino: dir, entries, sizeless }) = self.handles.get(&fh) else {
return Err(Errno::EBADF);
};
let dir = *dir;
let Some((saved, _, name)) = entries.get(position).cloned() else { return Ok(()) };
let executable = sizeless.get(&name).copied();
position += 1;
let dots = name == "." || name == "..";
let ino = if dots {
saved
} else {
let Ok(path) = self.path(dir).and_then(|path| path.join(&name).map_err(|_| Errno::EIO)) else {
continue;
};
self.ino_for(&path)
};
let sized = self.nodes.get(&ino).is_some_and(|node| node.sized.get());
let attr = match executable.filter(|_| !sized && !self.buffers.contains_key(&ino)) {
Some(executable) => self.sizeless_attr(ino, executable).map(|attr| (attr, Duration::ZERO)),
None => self.attr(ino).map(|attr| (attr, TTL)),
};
let Ok((attr, ttl)) = attr else {
self.maybe_drop(ino);
continue;
};
if add(ino, position as u64, &name, &attr, ttl) {
if !dots {
self.maybe_drop(ino);
}
return Ok(());
}
if !dots && let Some(node) = self.nodes.get_mut(&ino) {
node.lookups += 1;
}
}
}
#[cfg(any(fuse, test))]
pub fn listing(&mut self, ino: u64, offset: u64) -> FsResult<u64> {
match self.listings.get(&ino) {
Some(&fh) if offset > 0 => Ok(fh),
_ => {
self.end_listing(ino);
let fh = self.opendir(ino)?;
self.listings.insert(ino, fh);
while self.listings.len() > LISTINGS_KEPT {
let Some(oldest) = self.listings.iter().min_by_key(|(_, fh)| **fh).map(|(&ino, _)| ino) else {
break;
};
self.end_listing(oldest);
}
Ok(fh)
}
}
}
#[cfg(any(fuse, test))]
pub fn end_listing(&mut self, ino: u64) {
if let Some(fh) = self.listings.remove(&ino) {
self.releasedir(fh);
}
}
pub fn releasedir(&mut self, fh: u64) {
let Some(DirHandle { ino, .. }) = self.handles.remove(&fh) else { return };
if let Some(count) = self.open.get_mut(&ino) {
*count = count.saturating_sub(1);
if *count == 0 {
self.open.remove(&ino);
}
}
self.maybe_drop(ino);
}
}
pub struct Shared {
inner: Mutex<Inner>,
saved: Mutex<Saved>,
root: VPath,
me: Weak<Shared>,
scheduled: AtomicBool,
stop: AtomicBool,
used: AtomicU64,
trimmed: AtomicU64,
#[cfg(fuse)]
no_open: AtomicBool,
#[cfg(fuse)]
no_opendir: AtomicBool,
#[cfg(fuse)]
notifier: Mutex<Option<fuser::Notifier>>,
}
impl Shared {
pub fn new(inner: Inner, saved: Saved, root: VPath) -> Arc<Self> {
Arc::new_cyclic(|me| Shared {
inner: Mutex::new(inner),
saved: Mutex::new(saved),
root,
me: me.clone(),
scheduled: AtomicBool::new(false),
stop: AtomicBool::new(false),
used: AtomicU64::new(since_start()),
trimmed: AtomicU64::new(0),
#[cfg(fuse)]
no_open: AtomicBool::new(false),
#[cfg(fuse)]
no_opendir: AtomicBool::new(false),
#[cfg(fuse)]
notifier: Mutex::new(None),
})
}
pub fn lock(&self) -> MutexGuard<'_, Inner> {
self.used.store(since_start(), Ordering::Relaxed);
self.inner.lock().unwrap_or_else(PoisonError::into_inner)
}
pub fn trim(&self) -> anyhow::Result<()> {
self.save(false)?;
self.inner.lock().unwrap_or_else(PoisonError::into_inner).trim();
self.trimmed.store(since_start(), Ordering::Relaxed);
Ok(())
}
pub fn idle_untrimmed(&self, idle: Duration) -> bool {
let used = self.used.load(Ordering::Relaxed);
used > self.trimmed.load(Ordering::Relaxed) && since_start().saturating_sub(used) >= idle.as_millis() as u64
}
pub fn save(&self, durable: bool) -> anyhow::Result<()> {
self.save_settled(Duration::ZERO, durable)
}
fn save_settled(&self, settle: Duration, durable: bool) -> anyhow::Result<()> {
let mut saved = self.saved.lock().unwrap_or_else(PoisonError::into_inner);
self.save_locked(&mut saved, settle, durable)
}
fn save_locked(&self, saved: &mut Saved, settle: Duration, durable: bool) -> anyhow::Result<()> {
let (snapshot, skip_bookkeeping) = {
let mut inner = self.lock();
inner.commit_written(settle);
if !inner.unsaved && !inner.writing && !saved.deleting_later() {
drop(inner);
return if durable { saved.flush() } else { Ok(()) };
}
(inner.snapshot(settle), inner.skip_bookkeeping)
};
saved.skip_bookkeeping = skip_bookkeeping;
let result = saved.save(&snapshot, &self.root, durable);
let mut inner = self.lock();
for (path, version, stored) in saved.take_written() {
let _ = inner.ws.store(path.as_str(), version, stored);
}
if result.is_err() {
inner.unsaved = true;
}
result
}
pub fn replace(&self, change: impl FnOnce() -> anyhow::Result<Option<Replacement>>) -> anyhow::Result<()> {
let mut saved = self.saved.lock().unwrap_or_else(PoisonError::into_inner);
if self.stop.load(Ordering::SeqCst) {
anyhow::bail!("the mount is closing");
}
let stale = {
let mut inner = self.lock();
let snapshot = inner.snapshot(Duration::ZERO);
saved.skip_bookkeeping = inner.skip_bookkeeping;
let result = saved.save(&snapshot, &self.root, true);
for (path, version, stored) in saved.take_written() {
let _ = inner.ws.store(path.as_str(), version, stored);
}
if result.is_err() {
inner.unsaved = true;
}
result?;
let Some(next) = change()? else { return Ok(()) };
let stale = inner.replace(next.ws, next.times, next.dirs, next.changed.as_ref(), &next.whole);
inner.drafts = Some(next.saved.drafts());
let old = std::mem::replace(&mut *saved, next.saved);
saved.take_over(old);
stale
};
drop(saved);
#[cfg(fuse)]
if let Some(notifier) = self.notifier.lock().unwrap_or_else(PoisonError::into_inner).clone() {
for stale in stale {
let _ = match stale {
Stale::Inode(ino) => notifier.inval_inode(INodeNo(ino), 0, 0),
Stale::Entry(parent, name) => notifier.inval_entry(INodeNo(parent), OsStr::new(&name)),
};
}
}
#[cfg(not(fuse))]
drop(stale);
Ok(())
}
fn save_in_background(&self) {
let mut saved = self.saved.lock().unwrap_or_else(PoisonError::into_inner);
self.scheduled.store(false, Ordering::SeqCst);
if self.stop.load(Ordering::SeqCst) {
return;
}
let changed = {
let mut inner = self.lock();
inner.commit_written(SETTLE);
inner.unsaved
};
if changed && let Err(err) = self.save_locked(&mut saved, SETTLE, false) {
eprintln!("sandfs: saving changes failed: {err:#}");
self.schedule(Instant::now() + Duration::from_secs(1));
return;
}
drop(saved);
self.changed();
}
fn schedule(&self, at: Instant) {
if !self.scheduled.swap(true, Ordering::SeqCst) {
savers().schedule(at, self.me.clone());
}
}
pub fn stop(&self) {
let _saved = self.saved.lock().unwrap_or_else(PoisonError::into_inner);
self.stop.store(true, Ordering::SeqCst);
#[cfg(fuse)]
self.notifier.lock().unwrap_or_else(PoisonError::into_inner).take();
}
#[cfg(fuse)]
pub fn notify_stale_listings(&self, notifier: fuser::Notifier) {
*self.notifier.lock().unwrap_or_else(PoisonError::into_inner) = Some(notifier);
}
#[cfg(fuse)]
fn send_stale_listings(&self, inner: &mut Inner) {
let folders = inner.take_stale_listings();
if folders.is_empty() {
return;
}
if let Some(notifier) = &*self.notifier.lock().unwrap_or_else(PoisonError::into_inner) {
stale_listings().send((notifier.clone(), folders));
}
}
pub fn changed(&self) {
let pending = {
let inner = self.lock();
inner.unsaved || inner.writing
};
if pending && !self.scheduled.load(Ordering::SeqCst) {
self.schedule(Instant::now() + SETTLE);
}
}
}
struct Savers {
due: Mutex<BinaryHeap<Reverse<Due>>>,
wake: Condvar,
}
struct Due {
at: Instant,
seq: u64,
mount: Weak<Shared>,
}
impl PartialEq for Due {
fn eq(&self, other: &Self) -> bool {
(self.at, self.seq) == (other.at, other.seq)
}
}
impl Eq for Due {}
impl PartialOrd for Due {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other))
}
}
impl Ord for Due {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
(self.at, self.seq).cmp(&(other.at, other.seq))
}
}
const SAVERS: usize = 4;
fn savers() -> &'static Savers {
static STARTED: OnceLock<&'static Savers> = OnceLock::new();
STARTED.get_or_init(|| {
let savers: &'static Savers = Box::leak(Box::new(Savers { due: Mutex::default(), wake: Condvar::new() }));
for i in 0..SAVERS {
let _ = std::thread::Builder::new().name(format!("saver {i}")).spawn(move || savers.run());
}
savers
})
}
impl Savers {
fn schedule(&self, at: Instant, mount: Weak<Shared>) {
static SEQ: AtomicU64 = AtomicU64::new(0);
let seq = SEQ.fetch_add(1, Ordering::Relaxed);
self.due.lock().unwrap_or_else(PoisonError::into_inner).push(Reverse(Due { at, seq, mount }));
self.wake.notify_one();
}
fn run(&self) {
loop {
let mount = {
let mut due = self.due.lock().unwrap_or_else(PoisonError::into_inner);
loop {
let now = Instant::now();
let Some(Reverse(next)) = due.peek() else {
due = self.wake.wait(due).unwrap_or_else(PoisonError::into_inner);
continue;
};
if next.at <= now {
break due.pop().map(|Reverse(next)| next.mount);
}
let wait = next.at - now;
due = self.wake.wait_timeout(due, wait).unwrap_or_else(PoisonError::into_inner).0;
}
};
if let Some(mount) = mount.and_then(|mount| mount.upgrade()) {
mount.save_in_background();
}
}
}
}
#[cfg(fuse)]
struct StaleListings(Mutex<std::sync::mpsc::Sender<(fuser::Notifier, Vec<u64>)>>);
#[cfg(fuse)]
fn stale_listings() -> &'static StaleListings {
static STARTED: OnceLock<StaleListings> = OnceLock::new();
STARTED.get_or_init(|| {
let (send, stale) = std::sync::mpsc::channel::<(fuser::Notifier, Vec<u64>)>();
let _ = std::thread::Builder::new().name("stale listings".to_owned()).spawn(move || {
while let Ok((notifier, folders)) = stale.recv() {
for folder in folders.into_iter().collect::<BTreeSet<_>>() {
let _ = notifier.inval_inode(INodeNo(folder), 0, 0);
}
}
});
StaleListings(Mutex::new(send))
})
}
#[cfg(fuse)]
impl StaleListings {
fn send(&self, stale: (fuser::Notifier, Vec<u64>)) {
let _ = self.0.lock().unwrap_or_else(PoisonError::into_inner).send(stale);
}
}
#[cfg(fuse)]
pub struct SandFs(pub Arc<Shared>);
#[cfg(fuse)]
impl SandFs {
fn reply_entry(&self, reply: ReplyEntry, result: FsResult<FileAttr>) {
self.0.changed();
match result {
Ok(attr) => reply.entry(&TTL, &attr, Generation(0)),
Err(err) => reply.error(err),
}
}
fn reply_empty(&self, reply: ReplyEmpty, result: FsResult<()>) {
self.0.changed();
match result {
Ok(()) => reply.ok(),
Err(err) => reply.error(err),
}
}
}
#[cfg(fuse)]
impl SandFs {
fn listing(&self, inner: &mut Inner, ino: u64, fh: u64, offset: u64) -> FsResult<u64> {
if self.0.no_opendir.load(Ordering::SeqCst) { inner.listing(ino, offset) } else { Ok(fh) }
}
fn end_listing(&self, inner: &mut Inner, ino: u64, added: usize) {
if added == 0 && self.0.no_opendir.load(Ordering::SeqCst) {
inner.end_listing(ino);
}
}
}
#[cfg(fuse)]
impl Filesystem for SandFs {
fn init(&mut self, _req: &Request, config: &mut KernelConfig) -> std::io::Result<()> {
let _ = config.add_capabilities(InitFlags::FUSE_DO_READDIRPLUS);
let _ = config.add_capabilities(InitFlags::FUSE_READDIRPLUS_AUTO);
let no_open = config.add_capabilities(InitFlags::FUSE_NO_OPEN_SUPPORT).is_ok();
self.0.no_open.store(no_open, Ordering::SeqCst);
let no_opendir = config.add_capabilities(InitFlags::FUSE_NO_OPENDIR_SUPPORT).is_ok();
self.0.no_opendir.store(no_opendir, Ordering::SeqCst);
self.0.lock().keep_listed = no_opendir;
Ok(())
}
fn lookup(&self, _req: &Request, parent: INodeNo, name: &OsStr, reply: ReplyEntry) {
match self.0.lock().lookup(parent.0, name) {
Ok(attr) => reply.entry(&TTL, &attr, Generation(0)),
Err(Errno::ENOENT) => reply.entry(&TTL, &missing(), Generation(0)),
Err(err) => reply.error(err),
}
}
fn forget(&self, _req: &Request, ino: INodeNo, nlookup: u64) {
let mut inner = self.0.lock();
inner.forget(ino.0, nlookup);
self.0.send_stale_listings(&mut inner);
}
fn getattr(&self, _req: &Request, ino: INodeNo, _fh: Option<FileHandle>, reply: ReplyAttr) {
match self.0.lock().attr(ino.0) {
Ok(attr) => reply.attr(&TTL, &attr),
Err(err) => reply.error(err),
}
}
fn setattr(
&self,
_req: &Request,
ino: INodeNo,
mode: Option<u32>,
_uid: Option<u32>,
_gid: Option<u32>,
size: Option<u64>,
_atime: Option<TimeOrNow>,
mtime: Option<TimeOrNow>,
_ctime: Option<SystemTime>,
_fh: Option<FileHandle>,
_crtime: Option<SystemTime>,
_chgtime: Option<SystemTime>,
_bkuptime: Option<SystemTime>,
_flags: Option<fuser::BsdFileFlags>,
reply: ReplyAttr,
) {
let result = self.0.lock().setattr(ino.0, mode, size, mtime);
self.0.changed();
match result {
Ok(attr) => reply.attr(&TTL, &attr),
Err(err) => reply.error(err),
}
}
fn mkdir(&self, _req: &Request, parent: INodeNo, name: &OsStr, _mode: u32, _umask: u32, reply: ReplyEntry) {
let result = self.0.lock().mkdir(parent.0, name);
self.reply_entry(reply, result);
}
fn symlink(&self, _req: &Request, parent: INodeNo, name: &OsStr, target: &std::path::Path, reply: ReplyEntry) {
let result = self.0.lock().symlink(parent.0, name, target.as_os_str());
self.reply_entry(reply, result);
}
fn readlink(&self, _req: &Request, ino: INodeNo, reply: ReplyData) {
match self.0.lock().readlink(ino.0) {
Ok(target) => reply.data(target.as_bytes()),
Err(err) => reply.error(err),
}
}
fn link(&self, _req: &Request, ino: INodeNo, newparent: INodeNo, newname: &OsStr, reply: ReplyEntry) {
let result = self.0.lock().link(ino.0, newparent.0, newname);
self.reply_entry(reply, result);
}
fn unlink(&self, _req: &Request, parent: INodeNo, name: &OsStr, reply: ReplyEmpty) {
let result = self.0.lock().unlink(parent.0, name);
self.reply_empty(reply, result);
}
fn rmdir(&self, _req: &Request, parent: INodeNo, name: &OsStr, reply: ReplyEmpty) {
let result = self.0.lock().rmdir(parent.0, name);
self.reply_empty(reply, result);
}
fn rename(
&self,
_req: &Request,
parent: INodeNo,
name: &OsStr,
newparent: INodeNo,
newname: &OsStr,
flags: RenameFlags,
reply: ReplyEmpty,
) {
let result = self.0.lock().rename(parent.0, name, newparent.0, newname, flags);
self.reply_empty(reply, result);
}
fn open(&self, _req: &Request, _ino: INodeNo, _flags: OpenFlags, reply: ReplyOpen) {
if self.0.no_open.load(Ordering::SeqCst) {
reply.error(Errno::ENOSYS);
} else {
reply.opened(FileHandle(0), FopenFlags::empty());
}
}
fn read(
&self,
_req: &Request,
ino: INodeNo,
_fh: FileHandle,
offset: u64,
size: u32,
_flags: OpenFlags,
_lock_owner: Option<LockOwner>,
reply: ReplyData,
) {
let reading = self.0.lock().reading(ino.0, offset, size);
match reading.and_then(|reading| reading.read(offset, size)) {
Ok(data) => {
if data.len() < size as usize {
let mut inner = self.0.lock();
if inner.attr(ino.0).is_ok_and(|attr| attr.size > offset + data.len() as u64) {
let _ = inner.recheck(ino.0);
}
}
reply.data(&data)
}
Err(err) => reply.error(err),
}
}
fn write(
&self,
_req: &Request,
ino: INodeNo,
_fh: FileHandle,
offset: u64,
data: &[u8],
_write_flags: WriteFlags,
_flags: OpenFlags,
_lock_owner: Option<LockOwner>,
reply: ReplyWrite,
) {
let result = self.0.lock().write(ino.0, offset, data);
self.0.changed();
match result {
Ok(written) => reply.written(written),
Err(err) => reply.error(err),
}
}
fn copy_file_range(
&self,
_req: &Request,
ino_in: INodeNo,
_fh_in: FileHandle,
offset_in: u64,
ino_out: INodeNo,
_fh_out: FileHandle,
offset_out: u64,
len: u64,
_flags: CopyFileRangeFlags,
reply: ReplyWrite,
) {
let result = self.0.lock().copy_range(ino_in.0, offset_in, ino_out.0, offset_out, len);
self.0.changed();
match result {
Ok(copied) => reply.written(copied),
Err(err) => reply.error(err),
}
}
fn flush(&self, _req: &Request, _ino: INodeNo, _fh: FileHandle, _lock_owner: LockOwner, reply: ReplyEmpty) {
if self.0.no_open.load(Ordering::SeqCst) {
reply.error(Errno::ENOSYS);
} else {
reply.ok();
}
}
fn release(
&self,
_req: &Request,
_ino: INodeNo,
_fh: FileHandle,
_flags: OpenFlags,
_lock_owner: Option<LockOwner>,
_flush: bool,
reply: ReplyEmpty,
) {
reply.ok();
}
fn fsync(&self, _req: &Request, _ino: INodeNo, _fh: FileHandle, _datasync: bool, reply: ReplyEmpty) {
let result = self.0.save(true).map_err(|err| log_io(&err));
self.reply_empty(reply, result);
}
fn opendir(&self, _req: &Request, ino: INodeNo, _flags: OpenFlags, reply: ReplyOpen) {
if self.0.no_opendir.load(Ordering::SeqCst) {
return reply.error(Errno::ENOSYS);
}
match self.0.lock().opendir(ino.0) {
Ok(fh) => reply.opened(FileHandle(fh), FopenFlags::empty()),
Err(err) => reply.error(err),
}
}
fn readdir(&self, _req: &Request, ino: INodeNo, fh: FileHandle, offset: u64, mut reply: ReplyDirectory) {
let mut inner = self.0.lock();
let fh = match self.listing(&mut inner, ino.0, fh.0, offset) {
Ok(fh) => fh,
Err(err) => return reply.error(err),
};
let mut added = 0;
match inner.readdir(fh, offset) {
Ok(entries) => {
for (idx, (ino, kind, name)) in entries.iter().enumerate() {
if reply.add(INodeNo(*ino), offset + idx as u64 + 1, *kind, name) {
break;
}
added += 1;
}
reply.ok();
}
Err(err) => return reply.error(err),
}
self.end_listing(&mut inner, ino.0, added);
}
fn readdirplus(&self, _req: &Request, ino: INodeNo, fh: FileHandle, offset: u64, mut reply: ReplyDirectoryPlus) {
let mut inner = self.0.lock();
let fh = match self.listing(&mut inner, ino.0, fh.0, offset) {
Ok(fh) => fh,
Err(err) => return reply.error(err),
};
let mut added = 0;
let result = inner.readdirplus(fh, offset, |ino, next, name, attr, ttl| {
let full = reply.add(INodeNo(ino), next, name, &ttl, attr, Generation(0));
added += usize::from(!full);
full
});
match result {
Ok(()) => reply.ok(),
Err(err) => return reply.error(err),
}
self.end_listing(&mut inner, ino.0, added);
}
fn releasedir(&self, _req: &Request, _ino: INodeNo, fh: FileHandle, _flags: OpenFlags, reply: ReplyEmpty) {
self.0.lock().releasedir(fh.0);
reply.ok();
}
fn fsyncdir(&self, _req: &Request, _ino: INodeNo, _fh: FileHandle, datasync: bool, reply: ReplyEmpty) {
let result = self.0.save(!datasync).map_err(|err| log_io(&err));
self.reply_empty(reply, result);
}
fn statfs(&self, _req: &Request, _ino: INodeNo, reply: ReplyStatfs) {
let dir = self.0.lock().drafts.as_ref().and_then(|drafts| drafts.dir().parent().map(|dir| dir.to_path_buf()));
let disk = dir.and_then(|dir| nix::sys::statvfs::statvfs(&dir).ok());
match disk {
Some(disk) => reply.statfs(
disk.blocks(),
disk.blocks_free(),
disk.blocks_available(),
disk.files(),
disk.files_free(),
u32::try_from(disk.block_size()).unwrap_or(4096),
255,
u32::try_from(disk.fragment_size()).unwrap_or(4096),
),
None => {
let blocks = 1 << 28;
reply.statfs(blocks, blocks, blocks, 1 << 24, 1 << 24, 4096, 255, 4096);
}
}
}
fn create(
&self,
_req: &Request,
parent: INodeNo,
name: &OsStr,
mode: u32,
_umask: u32,
flags: i32,
reply: ReplyCreate,
) {
let exclusive = flags & libc::O_EXCL != 0;
let truncate = flags & libc::O_TRUNC != 0;
let result = self.0.lock().create(parent.0, name, mode, exclusive, truncate);
self.0.changed();
match result {
Ok(attr) => reply.created(&TTL, &attr, Generation(0), FileHandle(0), FopenFlags::empty()),
Err(err) => reply.error(err),
}
}
}
fn file_type(kind: Kind) -> (FileType, u16) {
match kind {
Kind::Dir => (FileType::Directory, 0o755),
Kind::File { executable: true } => (FileType::RegularFile, 0o755),
Kind::File { executable: false } => (FileType::RegularFile, 0o644),
Kind::Symlink => (FileType::Symlink, 0o777),
}
}
#[cfg(fuse)]
fn log_io(err: &anyhow::Error) -> Errno {
eprintln!("sandfs: saving changes failed: {err:#}");
Errno::EIO
}
#[cfg(test)]
mod tests {
use std::sync::Arc;
use sandfs_core::MemorySource;
use super::*;
fn fs() -> Inner {
let source =
MemorySource::from_files([("README.md", "hello\n"), ("src/lib.rs", "fn one() {}\n"), ("src/util.rs", "")])
.unwrap();
let mut ws = Workspace::new();
ws.mount("/mnt", Arc::new(source)).unwrap();
let root = VPath::parse("/mnt").unwrap();
Inner::new(ws, &root, BTreeMap::new(), BTreeSet::new(), SystemTime::UNIX_EPOCH, (1000, 1000))
}
fn name(s: &str) -> &OsStr {
OsStr::new(s)
}
fn names(fs: &mut Inner, ino: u64) -> Vec<String> {
let fh = fs.opendir(ino).unwrap();
let names = fs.readdir(fh, 2).unwrap().iter().map(|(_, _, name)| name.clone()).collect();
fs.releasedir(fh);
names
}
#[test]
fn lookups_and_attributes_come_from_the_workspace() {
let mut fs = fs();
let readme = fs.lookup(ROOT_INO, name("README.md")).unwrap();
assert_eq!((readme.kind, readme.size, readme.perm), (FileType::RegularFile, 6, 0o644));
let src = fs.lookup(ROOT_INO, name("src")).unwrap();
assert_eq!(src.kind, FileType::Directory);
assert_eq!(fs.lookup(ROOT_INO, name("README.md")).unwrap().ino, readme.ino, "inodes are stable");
assert_eq!(fs.lookup(ROOT_INO, name("missing")).unwrap_err(), Errno::ENOENT);
assert_eq!(names(&mut fs, ROOT_INO), ["README.md", "src"]);
}
const LATER: Duration = Duration::from_secs(3600);
#[test]
fn writes_are_committed_once_they_settle() {
let mut fs = fs();
let ino = fs.lookup(ROOT_INO, name("README.md")).unwrap().ino.0;
fs.write(ino, 6, b"world\n").unwrap();
assert!(fs.writing, "the saver has a buffer to come back for");
assert_eq!(&fs.read(ino, 0, 100).unwrap()[..], b"hello\nworld\n", "reads see the buffer");
assert_eq!(fs.attr(ino).unwrap().size, 12);
fs.commit_written(LATER);
assert_eq!(fs.ws.read_text("/mnt/README.md").unwrap(), "hello\n", "still being written");
assert!(fs.writing && !fs.unsaved);
fs.commit_written(Duration::ZERO);
assert_eq!(fs.ws.read_text("/mnt/README.md").unwrap(), "hello\nworld\n");
assert!(!fs.writing && fs.unsaved);
fs.write(ino, 12, b"!\n").unwrap();
assert_eq!(&fs.read(ino, 0, 100).unwrap()[..], b"hello\nworld\n!\n", "writes go on after a commit");
}
#[test]
fn files_are_written_on_disk_where_there_are_drafts() {
let state = tempfile::tempdir().unwrap();
let mut saved = Saved::new(state.path());
saved.pack_limit = None;
let mut fs = fs();
fs.drafts = Some(saved.drafts());
let ino = fs.lookup(ROOT_INO, name("README.md")).unwrap().ino.0;
fs.write(ino, 0, b"HE").unwrap();
assert_eq!(&fs.read(ino, 0, 100).unwrap()[..], b"HEllo\n", "copied from the source, then written");
fs.commit_written(Duration::ZERO);
let stored = fs.ws.stored("/mnt/README.md").unwrap().expect("committed as the draft on disk");
assert_eq!(std::fs::read(state.path().join("objects").join(&*stored.key)).unwrap(), b"HEllo\n");
saved.save(&fs.snapshot(Duration::ZERO), &VPath::parse("/mnt").unwrap(), false).unwrap();
assert!(saved.take_written().is_empty(), "the save writes nothing new");
fs.write(ino, 2, b"LLO").unwrap();
fs.commit_written(Duration::ZERO);
assert_eq!(fs.ws.read_text("/mnt/README.md").unwrap(), "HELLO\n");
assert_eq!(std::fs::read(state.path().join("objects").join(&*stored.key)).unwrap(), b"HEllo\n");
let huge = 5 << 30;
assert_eq!(fs.setattr(ino, None, Some(huge), None).unwrap().size, huge);
assert_eq!(&fs.read(ino, huge - 2, 10).unwrap()[..], b"\0\0");
let mut in_memory = super::tests::fs();
let ino = in_memory.lookup(ROOT_INO, name("README.md")).unwrap().ino.0;
assert_eq!(in_memory.setattr(ino, None, Some(huge), None).unwrap_err(), Errno::EFBIG);
assert_eq!(in_memory.write(ino, huge, b"x").unwrap_err(), Errno::EFBIG);
assert!(!in_memory.buffers.contains_key(&ino));
}
#[test]
fn small_files_are_written_in_memory_until_they_outgrow_a_pack() {
let state = tempfile::tempdir().unwrap();
let mut saved = Saved::new(state.path());
saved.pack_limit = Some(8);
let mut fs = fs();
fs.drafts = Some(saved.drafts());
let objects = || std::fs::read_dir(state.path().join("objects")).map_or(0, |dir| dir.count());
let ino = fs.create(ROOT_INO, name("small.txt"), 0o644, true, false).unwrap().ino.0;
fs.write(ino, 0, b"tiny\n").unwrap();
fs.commit_written(Duration::ZERO);
assert_eq!(fs.ws.read_text("/mnt/small.txt").unwrap(), "tiny\n");
assert_eq!(objects(), 0, "no file on disk for a file a save packs");
saved.save(&fs.snapshot(Duration::ZERO), &VPath::parse("/mnt").unwrap(), false).unwrap();
assert_eq!(objects(), 1, "the save packs it");
fs.write(ino, 5, b"and more\n").unwrap();
assert_eq!(objects(), 2);
fs.commit_written(Duration::ZERO);
let stored = fs.ws.stored("/mnt/small.txt").unwrap().expect("committed as the draft on disk");
assert_eq!(std::fs::read(state.path().join("objects").join(&*stored.key)).unwrap(), b"tiny\nand more\n");
}
#[test]
fn a_snapshot_leaves_out_files_still_being_written() {
let mut fs = fs();
let ino = fs.lookup(ROOT_INO, name("README.md")).unwrap().ino.0;
fs.write(ino, 0, b"HELLO\n").unwrap();
let snapshot = fs.snapshot(LATER);
assert_eq!(snapshot.ws.read_text("/mnt/README.md").unwrap(), "hello\n");
let snapshot = fs.snapshot(Duration::ZERO);
assert_eq!(snapshot.ws.read_text("/mnt/README.md").unwrap(), "HELLO\n", "fsync and unmount take everything");
}
#[derive(Debug)]
struct Unreadable(MemorySource);
impl sandfs_core::Source for Unreadable {
fn root(&self) -> sandfs_core::Result<sandfs_core::SourceKey> {
self.0.root()
}
fn read_dir(&self, key: &str) -> sandfs_core::Result<sandfs_core::Listing> {
self.0.read_dir(key)
}
fn read_file(&self, key: &str) -> sandfs_core::Result<Bytes> {
Err(Error::Source(format!("{key} was read")))
}
}
#[test]
fn host_folder_files_are_read_in_ranges_and_never_kept() {
let dir = tempfile::tempdir().unwrap();
std::fs::write(dir.path().join("notes.txt"), "before").unwrap();
let mut ws = Workspace::new();
ws.mount("/mnt", Arc::new(sandfs_core::DirSource::new(dir.path()).unwrap())).unwrap();
let root = VPath::parse("/mnt").unwrap();
let mut fs = Inner::new(ws, &root, BTreeMap::new(), BTreeSet::new(), SystemTime::UNIX_EPOCH, (1000, 1000));
fs.ranged_reads = true;
let ino = fs.lookup(ROOT_INO, name("notes.txt")).unwrap().ino.0;
assert_eq!(&fs.read(ino, 2, 3).unwrap()[..], b"for");
assert_eq!(&fs.read(ino, 0, 100).unwrap()[..], b"before");
std::fs::write(dir.path().join("notes.txt"), "after!").unwrap();
assert_eq!(&fs.read(ino, 0, 100).unwrap()[..], b"after!");
}
#[derive(Debug)]
struct Counting(MemorySource, std::sync::atomic::AtomicUsize);
impl sandfs_core::Source for Counting {
fn root(&self) -> sandfs_core::Result<sandfs_core::SourceKey> {
self.0.root()
}
fn read_dir(&self, key: &str) -> sandfs_core::Result<sandfs_core::Listing> {
self.0.read_dir(key)
}
fn read_file(&self, key: &str) -> sandfs_core::Result<Bytes> {
self.1.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
self.0.read_file(key)
}
}
#[test]
fn whole_file_sources_are_read_once_per_file_and_not_kept() {
let files = (0..20).map(|i| (format!("f{i:02}"), b"0123456789".to_vec()));
let source = Arc::new(Counting(MemorySource::from_files(files).unwrap(), Default::default()));
let mut ws = Workspace::new();
ws.mount("/mnt", source.clone()).unwrap();
let root = VPath::parse("/mnt").unwrap();
let mut fs = Inner::new(ws, &root, BTreeMap::new(), BTreeSet::new(), SystemTime::UNIX_EPOCH, (1000, 1000));
let reads = || source.1.load(std::sync::atomic::Ordering::SeqCst);
let inos: Vec<u64> = (0..20).map(|i| fs.lookup(ROOT_INO, name(&format!("f{i:02}"))).unwrap().ino.0).collect();
let chunks: Vec<u8> = (0..5).flat_map(|i| fs.read(inos[0], i * 2, 2).unwrap().to_vec()).collect();
assert_eq!(chunks, b"0123456789");
assert_eq!(reads(), 1);
for &ino in &inos {
fs.read(ino, 0, 100).unwrap();
}
assert_eq!(reads(), 20);
fs.read(inos[4], 0, 100).unwrap();
assert_eq!(reads(), 20, "kept while it is among the last read");
fs.read(inos[0], 0, 100).unwrap();
assert_eq!(reads(), 21, "dropped once later files took its place");
fs.read(inos[4], 0, 100).unwrap();
assert_eq!(reads(), 21);
fs.read(inos[5], 0, 100).unwrap();
assert_eq!(reads(), 22);
fs.write(inos[19], 0, b"new").unwrap();
fs.commit_written(Duration::ZERO);
assert_eq!(&fs.read(inos[19], 0, 100).unwrap()[..], b"new3456789");
}
#[test]
fn emptying_a_file_does_not_read_it() {
let source = Unreadable(MemorySource::from_files([("big.bin", vec![7u8; 1 << 20])]).unwrap());
let mut ws = Workspace::new();
ws.mount("/mnt", Arc::new(source)).unwrap();
let root = VPath::parse("/mnt").unwrap();
let mut fs = Inner::new(ws, &root, BTreeMap::new(), BTreeSet::new(), SystemTime::UNIX_EPOCH, (1000, 1000));
let ino = fs.lookup(ROOT_INO, name("big.bin")).unwrap().ino.0;
assert_eq!(fs.setattr(ino, None, Some(0), None).unwrap().size, 0);
fs.commit_written(Duration::ZERO);
assert_eq!(fs.ws.read("/mnt/big.bin").unwrap().len(), 0);
}
#[test]
fn create_truncate_and_chmod() {
let mut fs = fs();
let attr = fs.create(ROOT_INO, name("run.sh"), 0o755, true, false).unwrap();
assert_eq!(attr.perm, 0o755);
fs.write(attr.ino.0, 0, b"#!/bin/sh\n").unwrap();
assert_eq!(fs.create(ROOT_INO, name("run.sh"), 0o644, true, false).unwrap_err(), Errno::EEXIST);
let attr = fs.setattr(attr.ino.0, Some(0o644), Some(2), None).unwrap();
assert_eq!((attr.perm, attr.size), (0o644, 2));
fs.commit_written(Duration::ZERO);
assert_eq!(fs.ws.read_text("/mnt/run.sh").unwrap(), "#!");
let attr = fs.create(ROOT_INO, name("run.sh"), 0o644, false, true).unwrap();
assert_eq!(attr.size, 0, "an existing file opened with O_TRUNC");
fs.commit_written(Duration::ZERO);
assert_eq!(fs.ws.read_text("/mnt/run.sh").unwrap(), "");
}
#[test]
fn copies_share_whole_files_and_write_pieces() {
let mut fs = fs();
let readme = fs.lookup(ROOT_INO, name("README.md")).unwrap().ino.0;
let copy = fs.create(ROOT_INO, name("copy.md"), 0o644, true, false).unwrap().ino.0;
assert_eq!(fs.copy_range(readme, 0, copy, 0, 1 << 30).unwrap(), 6);
assert!(!fs.writing && fs.unsaved, "a whole copy needs no buffer");
assert_eq!(fs.ws.read_text("/mnt/copy.md").unwrap(), "hello\n");
assert_eq!(fs.attr(copy).unwrap().size, 6);
assert_eq!(fs.copy_range(readme, 1, copy, 6, 3).unwrap(), 3);
assert_eq!(&fs.read(copy, 0, 100).unwrap()[..], b"hello\nell");
assert!(fs.writing);
let again = fs.create(ROOT_INO, name("again.md"), 0o755, true, false).unwrap().ino.0;
assert_eq!(fs.copy_range(copy, 0, again, 0, 100).unwrap(), 9);
assert_eq!(fs.ws.read_text("/mnt/again.md").unwrap(), "hello\nell");
assert_eq!(fs.ws.read_text("/mnt/copy.md").unwrap(), "hello\nell");
assert_eq!(fs.attr(again).unwrap().perm, 0o755, "the destination keeps its mode");
assert_eq!(fs.copy_range(copy, 100, again, 0, 100).unwrap(), 0, "past the end");
assert_eq!(fs.ws.read_text("/mnt/README.md").unwrap(), "hello\n");
}
#[test]
fn copies_of_saved_files_share_their_object() {
let state = tempfile::tempdir().unwrap();
let mut saved = Saved::new(state.path());
saved.pack_limit = None;
let mut fs = fs();
fs.drafts = Some(saved.drafts());
let readme = fs.lookup(ROOT_INO, name("README.md")).unwrap().ino.0;
fs.write(readme, 0, b"HE").unwrap();
fs.commit_written(Duration::ZERO);
saved.save(&fs.snapshot(Duration::ZERO), &VPath::parse("/mnt").unwrap(), false).unwrap();
let stored = fs.ws.stored("/mnt/README.md").unwrap().unwrap();
let objects = std::fs::read_dir(state.path().join("objects")).unwrap().count();
let copy = fs.create(ROOT_INO, name("copy.md"), 0o644, true, false).unwrap().ino.0;
assert_eq!(fs.copy_range(readme, 0, copy, 0, 1 << 30).unwrap(), 6);
assert_eq!(fs.ws.stored("/mnt/copy.md").unwrap().unwrap().key, stored.key, "one object, two files");
saved.save(&fs.snapshot(Duration::ZERO), &VPath::parse("/mnt").unwrap(), false).unwrap();
assert_eq!(std::fs::read_dir(state.path().join("objects")).unwrap().count(), objects, "nothing new on disk");
fs.write(copy, 0, b"J").unwrap();
fs.commit_written(Duration::ZERO);
assert_eq!(fs.ws.read_text("/mnt/copy.md").unwrap(), "JEllo\n");
assert_eq!(fs.ws.read_text("/mnt/README.md").unwrap(), "HEllo\n");
saved.save(&fs.snapshot(Duration::ZERO), &VPath::parse("/mnt").unwrap(), false).unwrap();
let mut restored = super::tests::fs().ws;
Saved::new(state.path()).load(&mut restored, &VPath::parse("/mnt").unwrap()).unwrap();
assert_eq!(restored.read_text("/mnt/README.md").unwrap(), "HEllo\n");
assert_eq!(restored.read_text("/mnt/copy.md").unwrap(), "JEllo\n");
let src = fs.lookup(ROOT_INO, name("src")).unwrap().ino.0;
let lib = fs.lookup(src, name("lib.rs")).unwrap().ino.0;
let copied = fs.create(ROOT_INO, name("lib.rs"), 0o644, true, false).unwrap().ino.0;
assert_eq!(fs.copy_range(lib, 0, copied, 0, 1 << 30).unwrap(), 12);
assert!(fs.ws.stored("/mnt/lib.rs").unwrap().is_some());
assert_eq!(fs.ws.read_text("/mnt/lib.rs").unwrap(), "fn one() {}\n");
}
#[test]
fn an_fsync_with_nothing_changed_keeps_the_saved_index() {
use std::os::unix::fs::MetadataExt;
let state = tempfile::tempdir().unwrap();
let mut saved = Saved::new(state.path());
saved.pack_limit = None;
let mut inner = fs();
inner.drafts = Some(saved.drafts());
let shared = Shared::new(inner, saved, VPath::parse("/mnt").unwrap());
let index = state.path().join("changes.json");
let objects = || std::fs::read_dir(state.path().join("objects")).unwrap().count();
let index_ino = || std::fs::metadata(&index).unwrap().ino();
shared.save(true).unwrap();
assert!(!index.exists(), "nothing to save yet");
let readme = shared.lock().lookup(ROOT_INO, name("README.md")).unwrap().ino.0;
shared.lock().write(readme, 0, b"HE").unwrap();
shared.save(true).unwrap();
let saved_ino = index_ino();
shared.save(true).unwrap();
shared.save(false).unwrap();
assert_eq!(index_ino(), saved_ino, "the index is kept as it is");
let new = shared.lock().create(ROOT_INO, name("new.md"), 0o644, true, false).unwrap().ino.0;
shared.lock().write(new, 0, b"new\n").unwrap();
shared.save(true).unwrap();
assert_ne!(index_ino(), saved_ino, "a change is saved");
shared.lock().write(new, 0, b"NEW\n").unwrap();
shared.save(true).unwrap();
assert_eq!(objects(), 3, "the replaced object waits one save");
shared.save(true).unwrap();
assert_eq!(objects(), 2);
let saved_ino = index_ino();
shared.save(true).unwrap();
assert_eq!(index_ino(), saved_ino, "and then nothing is left to do");
let drafts = shared.lock().drafts.clone().unwrap();
drop(drafts.empty().unwrap());
assert_eq!(objects(), 3);
shared.save(true).unwrap();
assert_eq!(objects(), 2);
let mut restored = fs().ws;
Saved::new(state.path()).load(&mut restored, &VPath::parse("/mnt").unwrap()).unwrap();
assert_eq!(restored.read_text("/mnt/README.md").unwrap(), "HEllo\n");
assert_eq!(restored.read_text("/mnt/new.md").unwrap(), "NEW\n");
}
#[test]
fn a_trim_saves_writes_and_keeps_what_the_mount_serves() {
let state = tempfile::tempdir().unwrap();
let saved = Saved::new(state.path());
let mut inner = fs();
inner.drafts = Some(saved.drafts());
let shared = Shared::new(inner, saved, VPath::parse("/mnt").unwrap());
let readme = shared.lock().lookup(ROOT_INO, name("README.md")).unwrap().ino.0;
let src = shared.lock().lookup(ROOT_INO, name("src")).unwrap().ino.0;
shared.lock().write(readme, 0, b"HE").unwrap();
assert!(shared.idle_untrimmed(Duration::ZERO), "used, never trimmed");
shared.trim().unwrap();
assert!(!shared.idle_untrimmed(Duration::ZERO), "trimmed since last used");
std::thread::sleep(Duration::from_millis(2));
let mut restored = fs().ws;
Saved::new(state.path()).load(&mut restored, &VPath::parse("/mnt").unwrap()).unwrap();
assert_eq!(restored.read_text("/mnt/README.md").unwrap(), "HEllo\n");
let mut inner = shared.lock();
assert_eq!(&inner.read(readme, 0, 100).unwrap()[..], b"HEllo\n");
assert_eq!(inner.lookup(ROOT_INO, name("src")).unwrap().ino.0, src);
let lib = inner.lookup(src, name("lib.rs")).unwrap().ino.0;
assert_eq!(&inner.read(lib, 0, 100).unwrap()[..], b"fn one() {}\n");
drop(inner);
assert!(shared.idle_untrimmed(Duration::ZERO), "used since the trim");
}
#[test]
fn writes_are_saved_in_the_background_until_the_mount_stops() {
let state = tempfile::tempdir().unwrap();
let saved = Saved::new(state.path());
let mut inner = fs();
inner.drafts = Some(saved.drafts());
let shared = Shared::new(inner, saved, VPath::parse("/mnt").unwrap());
let readme = shared.lock().lookup(ROOT_INO, name("README.md")).unwrap().ino.0;
let restored = || {
let mut restored = fs().ws;
Saved::new(state.path()).load(&mut restored, &VPath::parse("/mnt").unwrap()).unwrap();
restored.read_text("/mnt/README.md").unwrap()
};
shared.lock().write(readme, 0, b"HE").unwrap();
shared.changed();
let deadline = Instant::now() + SETTLE * 20;
while restored() != "HEllo\n" {
assert!(Instant::now() < deadline, "the write was never saved in the background");
std::thread::sleep(SETTLE / 5);
}
shared.lock().write(readme, 0, b"HEL").unwrap();
shared.changed();
shared.stop();
std::thread::sleep(SETTLE * 2);
assert_eq!(restored(), "HEllo\n", "no background save after the stop");
shared.save(true).unwrap();
assert_eq!(restored(), "HELlo\n");
}
#[test]
fn a_failed_copy_keeps_the_destination_emptied() {
let state = tempfile::tempdir().unwrap();
let mut fs = fs();
let mut saved = Saved::new(state.path());
saved.pack_limit = None;
fs.drafts = Some(saved.drafts());
fs.ws.write("/mnt/out.md", "old\n").unwrap();
let out = fs.lookup(ROOT_INO, name("out.md")).unwrap().ino.0;
fs.setattr(out, None, Some(0), None).unwrap();
let objects = state.path().join("objects");
let _ = std::fs::remove_dir_all(&objects);
std::fs::write(&objects, "").unwrap();
let readme = fs.lookup(ROOT_INO, name("README.md")).unwrap().ino.0;
assert!(fs.copy_range(readme, 0, out, 0, 1 << 30).is_err());
assert_eq!(fs.attr(out).unwrap().size, 0);
assert_eq!(fs.read(out, 0, 100).unwrap().len(), 0, "the old bytes stay gone");
}
#[test]
fn rename_replaces_and_moves_inodes() {
let mut fs = fs();
let src = fs.lookup(ROOT_INO, name("src")).unwrap().ino.0;
let lib = fs.lookup(src, name("lib.rs")).unwrap().ino.0;
fs.rename(src, name("lib.rs"), src, name("util.rs"), RenameFlags::empty()).unwrap();
assert_eq!(names(&mut fs, src), ["util.rs"]);
assert_eq!(fs.lookup(src, name("util.rs")).unwrap().ino.0, lib, "the inode follows the file");
fs.rename(ROOT_INO, name("src"), ROOT_INO, name("code"), RenameFlags::empty()).unwrap();
assert_eq!(fs.path(lib).unwrap().as_str(), "/mnt/code/util.rs");
assert_eq!(fs.ws.read_text("/mnt/code/util.rs").unwrap(), "fn one() {}\n");
assert!(fs.dirs.contains("/mnt/src") && fs.dirs.contains("/mnt/code"));
let code = fs.lookup(ROOT_INO, name("code")).unwrap().ino.0;
assert_eq!(
fs.rename(ROOT_INO, name("code"), code, name("inside"), RenameFlags::empty()).unwrap_err(),
Errno::EINVAL
);
assert_eq!(
fs.rename(ROOT_INO, name("README.md"), ROOT_INO, name("code"), RenameFlags::empty()).unwrap_err(),
Errno::EISDIR
);
#[cfg(target_os = "linux")]
assert_eq!(
fs.rename(
code,
name("util.rs"),
ROOT_INO,
name("README.md"),
RenameFlags::from_bits_retain(RENAME_NOREPLACE)
)
.unwrap_err(),
Errno::EEXIST
);
}
#[test]
fn removed_files_stay_readable_until_forgotten() {
let mut fs = fs();
let ino = fs.lookup(ROOT_INO, name("README.md")).unwrap().ino.0;
fs.unlink(ROOT_INO, name("README.md")).unwrap();
assert_eq!(fs.lookup(ROOT_INO, name("README.md")).unwrap_err(), Errno::ENOENT);
assert_eq!(fs.attr(ino).unwrap().size, 6);
assert_eq!(&fs.read(ino, 0, 100).unwrap()[..], b"hello\n");
fs.write(ino, 0, b"HELLO\n").unwrap();
assert_eq!(fs.setattr(ino, None, Some(2), None).unwrap().size, 2, "ftruncate on the open handle");
assert_eq!(&fs.read(ino, 0, 100).unwrap()[..], b"HE");
fs.commit_written(Duration::ZERO);
assert!(!fs.ws.exists("/mnt/README.md"), "a removed file's writes go nowhere");
assert!(!fs.writing, "and keep nothing waiting");
assert_eq!(&fs.read(ino, 0, 100).unwrap()[..], b"HE");
let link = fs.symlink(ROOT_INO, name("link"), OsStr::new("README.md")).unwrap().ino.0;
fs.unlink(ROOT_INO, name("link")).unwrap();
assert_eq!(fs.attr(link).unwrap().kind, FileType::Symlink, "a removed link is still a link");
assert_eq!(fs.readlink(link).unwrap(), "README.md");
fs.forget(ino, 1);
assert!(!fs.nodes.contains_key(&ino) && !fs.buffers.contains_key(&ino), "closed once forgotten");
}
#[test]
fn a_newer_workspace_keeps_inodes_and_names_what_went_stale() {
let mut fs = fs();
let readme = fs.lookup(ROOT_INO, name("README.md")).unwrap().ino.0;
let src = fs.lookup(ROOT_INO, name("src")).unwrap().ino.0;
let lib = fs.lookup(src, name("lib.rs")).unwrap().ino.0;
let util = fs.lookup(src, name("util.rs")).unwrap().ino.0;
assert_eq!(&fs.read(readme, 0, 100).unwrap()[..], b"hello\n");
let newer =
MemorySource::from_files([("README.md", "hello again\n"), ("src", "a file now\n"), ("new.txt", "")])
.unwrap();
let mut ws = Workspace::new();
ws.mount("/mnt", Arc::new(newer)).unwrap();
let changed: BTreeSet<String> =
["/mnt/README.md", "/mnt/src/lib.rs", "/mnt/src/util.rs", "/mnt/src", "/mnt/new.txt"]
.map(str::to_owned)
.into();
let stale = fs.replace(ws, BTreeMap::new(), BTreeSet::new(), Some(&changed), &[]);
assert_eq!(fs.lookup(ROOT_INO, name("README.md")).unwrap().ino.0, readme, "a file keeps its inode");
assert_eq!(&fs.read(readme, 0, 100).unwrap()[..], b"hello again\n", "and reads as it is now");
assert!(stale.contains(&Stale::Inode(readme)));
assert!(stale.contains(&Stale::Entry(ROOT_INO, "src".into())) && stale.contains(&Stale::Inode(ROOT_INO)));
let file = fs.lookup(ROOT_INO, name("src")).unwrap();
assert_ne!(file.ino.0, src);
assert_eq!(file.kind, FileType::RegularFile);
assert_eq!(&fs.read(lib, 0, 100).unwrap()[..], b"fn one() {}\n");
assert_eq!(fs.path(util).unwrap_err(), Errno::ENOENT);
assert!(!stale.iter().any(|stale| matches!(stale, Stale::Entry(parent, _) if *parent == src)));
assert!(stale.contains(&Stale::Entry(ROOT_INO, "new.txt".into())));
assert_eq!(names(&mut fs, ROOT_INO), ["README.md", "new.txt", "src"]);
assert!(fs.times.contains_key("/mnt"), "the folder's time changed with its names");
}
#[test]
fn a_newer_workspace_renumbers_files_that_may_have_changed_where_asked() {
let mut fs = fs();
fs.renumber_changed = true;
let readme = fs.lookup(ROOT_INO, name("README.md")).unwrap().ino.0;
let src = fs.lookup(ROOT_INO, name("src")).unwrap().ino.0;
let newer = MemorySource::from_files([("README.md", "hello again\n"), ("src/lib.rs", "")]).unwrap();
let mut ws = Workspace::new();
ws.mount("/mnt", Arc::new(newer)).unwrap();
let changed: BTreeSet<String> = ["/mnt/README.md", "/mnt/src/util.rs"].map(str::to_owned).into();
let stale = fs.replace(ws, BTreeMap::new(), BTreeSet::new(), Some(&changed), &[]);
assert!(stale.contains(&Stale::Entry(ROOT_INO, "README.md".into())));
assert_ne!(fs.lookup(ROOT_INO, name("README.md")).unwrap().ino.0, readme, "a new inode has nothing cached");
assert_eq!(fs.lookup(ROOT_INO, name("src")).unwrap().ino.0, src, "a folder keeps its inode");
assert!(fs.times.contains_key("/mnt") && fs.times.contains_key("/mnt/src"));
fs.keep_removed = false;
let readme_now = fs.lookup(ROOT_INO, name("README.md")).unwrap().ino.0;
let changed: BTreeSet<String> = ["/mnt/README.md".to_owned()].into();
let ws = fs.ws.fork();
fs.replace(ws, BTreeMap::new(), BTreeSet::new(), Some(&changed), &[]);
assert_eq!(&fs.read(readme_now, 0, 100).unwrap()[..], b"hello again\n");
let ws = fs.ws.fork();
fs.replace(ws, BTreeMap::new(), BTreeSet::new(), Some(&changed), &[]);
assert!(!fs.nodes.contains_key(&readme_now));
}
#[test]
fn a_newer_workspace_keeps_hard_links_on_one_live_inode() {
let mut fs = fs();
fs.renumber_changed = true;
fs.keep_removed = false;
let readme = fs.lookup(ROOT_INO, name("README.md")).unwrap().ino.0;
fs.link(readme, ROOT_INO, name("copy.md")).unwrap();
let changed: BTreeSet<String> = ["/mnt/README.md".to_owned()].into();
let ws = fs.ws.fork();
fs.replace(ws, BTreeMap::new(), BTreeSet::new(), Some(&changed), &[]);
let (readme, copy) = (
fs.lookup(ROOT_INO, name("README.md")).unwrap().ino.0,
fs.lookup(ROOT_INO, name("copy.md")).unwrap().ino.0,
);
assert_eq!(readme, copy);
assert!(fs.nodes[&readme].linked);
let mut ws = fs.ws.fork();
ws.remove("/mnt/README.md", false).unwrap();
ws.write("/mnt/README.md", "new\n").unwrap();
let stale = fs.replace(ws, BTreeMap::new(), BTreeSet::new(), Some(&changed), &[]);
assert!(stale.contains(&Stale::Entry(ROOT_INO, "copy.md".into())));
let copy = fs.lookup(ROOT_INO, name("copy.md")).unwrap().ino.0;
assert!(fs.nodes[©].linked);
assert_ne!(copy, fs.lookup(ROOT_INO, name("README.md")).unwrap().ino.0);
assert_eq!(&fs.read(copy, 0, 100).unwrap()[..], b"hello\n");
}
#[test]
fn a_file_replaced_by_rename_stays_readable_until_forgotten() {
let mut fs = fs();
let src = fs.lookup(ROOT_INO, name("src")).unwrap().ino.0;
let util = fs.lookup(src, name("util.rs")).unwrap().ino.0;
fs.rename(src, name("lib.rs"), src, name("util.rs"), RenameFlags::empty()).unwrap();
assert_eq!(&fs.read(util, 0, 100).unwrap()[..], b"", "the old util.rs");
fs.forget(util, 1);
assert!(!fs.nodes.contains_key(&util));
}
#[test]
fn operations_that_would_follow_a_link_fail_with_eloop() {
let mut ws = Workspace::new();
ws.symlink("/link", "target").unwrap();
assert_eq!(errno(&ws.write("/link", "x").unwrap_err()), Errno::ELOOP);
assert_eq!(errno(&ws.read("/link").unwrap_err()), Errno::ELOOP);
assert_eq!(errno(&ws.remove("/", true).unwrap_err()), Errno::EPERM, "other refusals stay EPERM");
}
#[test]
fn a_link_has_no_size_to_set() {
let mut fs = fs();
let link = fs.symlink(ROOT_INO, name("link"), OsStr::new("README.md")).unwrap().ino.0;
for size in [0, 3] {
assert_eq!(fs.setattr(link, None, Some(size), None).unwrap_err(), Errno::EINVAL);
}
assert_eq!(fs.create(ROOT_INO, name("link"), 0o644, false, true).unwrap_err(), Errno::ELOOP);
assert_eq!(fs.create(ROOT_INO, name("link"), 0o644, false, false).unwrap().ino.0, link);
assert_eq!(fs.readlink(link).unwrap(), "README.md");
assert!(fs.buffers.is_empty());
#[derive(Debug)]
struct Broken;
impl sandfs_core::Source for Broken {
fn root(&self) -> sandfs_core::Result<sandfs_core::SourceKey> {
Ok("".into())
}
fn read_dir(&self, _key: &str) -> sandfs_core::Result<sandfs_core::Listing> {
let entry =
sandfs_core::SourceEntry { kind: Kind::Symlink, key: "link".into(), size: Some(4), modified: None };
Ok([("link".to_owned(), entry)].into())
}
fn read_file(&self, _key: &str) -> sandfs_core::Result<Bytes> {
Err(Error::Source("gone".into()))
}
}
let mut ws = Workspace::new();
ws.mount("/mnt", Arc::new(Broken)).unwrap();
let mut fs = Inner::new(
ws,
&VPath::parse("/mnt").unwrap(),
BTreeMap::new(),
BTreeSet::new(),
SystemTime::UNIX_EPOCH,
(1000, 1000),
);
let link = fs.lookup(ROOT_INO, name("link")).unwrap().ino.0;
assert_eq!(fs.setattr(link, None, Some(0), None).unwrap_err(), Errno::EIO);
assert!(fs.buffers.is_empty());
}
#[test]
fn hard_links_are_one_inode_until_the_last_name_goes() {
let mut fs = fs();
let readme = fs.lookup(ROOT_INO, name("README.md")).unwrap().ino.0;
let src = fs.lookup(ROOT_INO, name("src")).unwrap().ino.0;
let attr = fs.link(readme, src, name("readme")).unwrap();
assert_eq!((attr.ino.0, attr.nlink), (readme, 2));
assert_eq!(attr.mtime, SystemTime::UNIX_EPOCH, "a new name leaves the file's time alone");
assert_ne!(fs.attr(src).unwrap().mtime, SystemTime::UNIX_EPOCH, "its folder changed");
assert_eq!(fs.lookup(src, name("readme")).unwrap().ino.0, readme);
fs.write(readme, 0, b"HELLO").unwrap();
fs.commit_written(Duration::ZERO);
assert_eq!(fs.ws.read_text("/mnt/src/readme").unwrap(), "HELLO\n");
let written = fs.times["/mnt/README.md"];
assert_eq!(fs.times["/mnt/src/readme"], written, "every name has the time of the write");
fs.rename(ROOT_INO, name("README.md"), src, name("readme"), RenameFlags::empty()).unwrap();
assert_eq!(fs.attr(readme).unwrap().nlink, 2);
fs.unlink(ROOT_INO, name("README.md")).unwrap();
let attr = fs.attr(readme).unwrap();
assert_eq!((attr.nlink, attr.size), (1, 6));
assert_eq!(fs.read(readme, 0, 100).unwrap(), "HELLO\n");
assert_eq!(fs.lookup(src, name("readme")).unwrap().ino.0, readme);
assert!(matches!(fs.link(src, ROOT_INO, name("dir")), Err(Errno::EISDIR)));
}
#[test]
fn directories_are_created_and_removed() {
let mut fs = fs();
let dir = fs.mkdir(ROOT_INO, name("new")).unwrap();
assert_eq!(dir.kind, FileType::Directory);
fs.mkdir(dir.ino.0, name("sub")).unwrap();
assert_eq!(fs.rmdir(ROOT_INO, name("new")).unwrap_err(), Errno::ENOTEMPTY);
fs.rmdir(dir.ino.0, name("sub")).unwrap();
fs.rmdir(ROOT_INO, name("new")).unwrap();
assert_eq!(fs.unlink(ROOT_INO, name("src")).unwrap_err(), Errno::EISDIR);
assert_eq!(fs.rmdir(ROOT_INO, name("README.md")).unwrap_err(), Errno::ENOTDIR);
assert!(fs.dirs.contains("/mnt/new"));
}
#[test]
fn listings_with_attributes_count_lookups_and_resume() {
let mut fs = fs();
let fh = fs.opendir(ROOT_INO).unwrap();
let mut seen = Vec::new();
fs.readdirplus(fh, 0, |_, next, name, _, _| {
seen.push((next, name.to_owned()));
seen.len() > 3
})
.unwrap();
assert_eq!(seen.iter().map(|(_, name)| name.as_str()).collect::<Vec<_>>(), [".", "..", "README.md", "src"]);
let resume = seen[2].0;
let mut rest = Vec::new();
fs.readdirplus(fh, resume, |_, _, name, attr, _| {
rest.push((name.to_owned(), attr.kind));
false
})
.unwrap();
assert_eq!(rest, [("src".to_owned(), FileType::Directory)], "the refused entry comes again");
fs.releasedir(fh);
let readme = fs.lookup(ROOT_INO, name("README.md")).unwrap().ino.0;
let src = fs.lookup(ROOT_INO, name("src")).unwrap().ino.0;
assert_eq!(fs.nodes[&readme].lookups, 2, "one from the listing, one from lookup");
assert_eq!(fs.nodes[&src].lookups, 2, "counted once, when accepted");
assert_eq!(fs.nodes[&ROOT_INO].lookups, 0, ". and .. are not counted");
}
#[test]
fn listings_with_attributes_leave_unknown_sizes_to_the_kernel() {
#[derive(Debug)]
struct Sizeless(MemorySource, std::sync::atomic::AtomicUsize);
impl sandfs_core::Source for Sizeless {
fn root(&self) -> sandfs_core::Result<sandfs_core::SourceKey> {
self.0.root()
}
fn read_dir(&self, key: &str) -> sandfs_core::Result<sandfs_core::Listing> {
let mut listing = self.0.read_dir(key)?;
listing.values_mut().for_each(|entry| entry.size = None);
Ok(listing)
}
fn read_file(&self, key: &str) -> sandfs_core::Result<bytes::Bytes> {
self.1.fetch_add(1, Ordering::SeqCst);
self.0.read_file(key)
}
}
let source = Arc::new(Sizeless(
MemorySource::from_files([("README.md", "hello\n"), ("src/lib.rs", "")]).unwrap(),
Default::default(),
));
let mut ws = Workspace::new();
ws.mount("/mnt", source.clone()).unwrap();
let root = VPath::parse("/mnt").unwrap();
let mut fs = Inner::new(ws, &root, BTreeMap::new(), BTreeSet::new(), SystemTime::UNIX_EPOCH, (1000, 1000));
fs.ws.hard_link("/mnt/README.md", "/mnt/README.txt").unwrap();
let fh = fs.opendir(ROOT_INO).unwrap();
let mut seen = Vec::new();
fs.readdirplus(fh, 2, |ino, _, name, attr, ttl| {
seen.push((name.to_owned(), ino, attr.size, ttl, attr.nlink));
false
})
.unwrap();
fs.releasedir(fh);
let readme = seen.iter().find(|(name, ..)| name == "README.md").unwrap();
assert_eq!((readme.2, readme.3), (0, Duration::ZERO), "no size yet, and nothing to keep");
let other = seen.iter().find(|(name, ..)| name == "README.txt").unwrap();
assert_eq!((readme.4, other.1, other.4), (2, readme.1, 2), "one inode with two names: {seen:?}");
let src = seen.iter().find(|(name, ..)| name == "src").unwrap();
assert_eq!(src.3, TTL);
assert_eq!(source.1.load(Ordering::SeqCst), 0, "listing read no file");
assert_eq!(fs.attr(readme.1).unwrap().size, 6);
let fh = fs.opendir(ROOT_INO).unwrap();
let mut again = Vec::new();
fs.readdirplus(fh, 2, |_, _, name, attr, ttl| {
again.push((name.to_owned(), attr.size, ttl));
false
})
.unwrap();
fs.releasedir(fh);
assert!(again.contains(&("README.md".to_owned(), 6, TTL)), "{again:?}");
assert_eq!(source.1.load(Ordering::SeqCst), 1, "only the first size read the file");
}
#[test]
fn listings_with_attributes_never_publish_stale_names() {
let mut fs = fs();
let src = fs.lookup(ROOT_INO, name("src")).unwrap().ino.0;
let fh = fs.opendir(ROOT_INO).unwrap();
fs.rename(ROOT_INO, name("README.md"), ROOT_INO, name("NOTES.md"), RenameFlags::empty()).unwrap();
fs.forget(src, 1);
let mut seen = Vec::new();
fs.readdirplus(fh, 2, |ino, _, name, attr, _| {
seen.push((name.to_owned(), ino, attr.kind));
false
})
.unwrap();
fs.releasedir(fh);
assert_eq!(seen.len(), 1, "the moved name is left out: {seen:?}");
let (name_seen, ino, kind) = &seen[0];
assert_eq!((name_seen.as_str(), *kind), ("src", FileType::Directory), "the forgotten child is still listed");
assert_eq!(fs.path(*ino).unwrap().as_str(), "/mnt/src");
assert_eq!(fs.nodes[ino].lookups, 1);
}
#[test]
fn unopened_listings_page_through_one_look_and_let_go_at_the_end() {
let mut fs = fs();
let fh = fs.listing(ROOT_INO, 0).unwrap();
let first = fs.readdir(fh, 0).unwrap()[..3].to_vec();
fs.create(ROOT_INO, name("late.txt"), 0o644, true, false).unwrap();
assert_eq!(fs.listing(ROOT_INO, 3).unwrap(), fh);
let rest: Vec<String> = fs.readdir(fh, 3).unwrap().iter().map(|(_, _, name)| name.clone()).collect();
assert!(!rest.contains(&"late.txt".to_owned()), "{first:?} then {rest:?}");
let again = fs.listing(ROOT_INO, 0).unwrap();
assert_ne!(again, fh);
assert!(!fs.handles.contains_key(&fh));
assert!(fs.readdir(again, 0).unwrap().iter().any(|(_, _, name)| name == "late.txt"));
fs.end_listing(ROOT_INO);
assert!(fs.handles.is_empty() && fs.listings.is_empty() && !fs.open.contains_key(&ROOT_INO));
}
#[test]
fn dropping_a_listed_inode_marks_its_folder_listing_stale() {
let mut fs = fs();
let src = fs.lookup(ROOT_INO, name("src")).unwrap().ino.0;
let lib = fs.lookup(src, name("lib.rs")).unwrap().ino.0;
let util = fs.lookup(src, name("util.rs")).unwrap().ino.0;
fs.forget(lib, 1);
assert!(fs.take_stale_listings().is_empty(), "nothing is kept without kept listings");
fs.keep_listed = true;
let lib = fs.lookup(src, name("lib.rs")).unwrap().ino.0;
fs.forget(lib, 1);
fs.forget(util, 1);
assert_eq!(fs.take_stale_listings(), [src], "once per folder");
assert!(fs.take_stale_listings().is_empty());
let lib = fs.lookup(src, name("lib.rs")).unwrap().ino.0;
fs.forget(lib, 1);
fs.forget(src, 1);
assert_eq!(fs.take_stale_listings(), [ROOT_INO]);
}
#[test]
fn only_the_newest_unopened_listings_are_kept() {
let mut fs = fs();
let folders: Vec<u64> =
(0..LISTINGS_KEPT + 6).map(|i| fs.mkdir(ROOT_INO, name(&format!("d{i}"))).unwrap().ino.0).collect();
let first = fs.listing(folders[0], 0).unwrap();
for &ino in &folders[1..] {
fs.listing(ino, 0).unwrap();
}
assert_eq!((fs.listings.len(), fs.handles.len()), (LISTINGS_KEPT, LISTINGS_KEPT));
assert!(!fs.handles.contains_key(&first), "the oldest listing was let go");
assert_ne!(fs.listing(folders[0], 2).unwrap(), first);
}
#[test]
fn an_open_folder_outlives_its_last_lookup() {
let mut fs = fs();
let src = fs.lookup(ROOT_INO, name("src")).unwrap().ino.0;
let fh = fs.opendir(src).unwrap();
fs.forget(src, 1);
let mut names = Vec::new();
fs.readdirplus(fh, 2, |_, _, name, _, _| {
names.push(name.to_owned());
false
})
.unwrap();
assert!(!names.is_empty(), "the folder's entries are still listed");
fs.releasedir(fh);
assert!(!fs.nodes.contains_key(&src), "dropped once the handle is released");
}
#[test]
fn a_forgotten_file_keeps_its_writes() {
let mut fs = fs();
let ino = fs.create(ROOT_INO, name("index.lock"), 0o644, true, false).unwrap().ino.0;
fs.write(ino, 0, b"written").unwrap();
fs.rename(ROOT_INO, name("index.lock"), ROOT_INO, name("index"), RenameFlags::empty()).unwrap();
fs.forget(ino, 1);
assert!(!fs.nodes.contains_key(&ino));
assert_eq!(fs.ws.read_text("/mnt/index").unwrap(), "written");
assert!(fs.unsaved, "a save picks it up");
}
#[test]
fn forgotten_inodes_are_dropped() {
let mut fs = fs();
let ino = fs.lookup(ROOT_INO, name("README.md")).unwrap().ino.0;
fs.lookup(ROOT_INO, name("README.md")).unwrap();
fs.forget(ino, 1);
assert!(fs.nodes.contains_key(&ino));
fs.forget(ino, 1);
assert!(!fs.nodes.contains_key(&ino) && !fs.inos.contains_key("/mnt/README.md"));
}
}