2026-07-16 13:03:04 +09:00
|
|
|
use std::collections::{BTreeSet, HashMap};
|
|
|
|
|
use std::ffi::{OsStr, OsString};
|
|
|
|
|
use std::fs::File;
|
|
|
|
|
use std::fs::OpenOptions;
|
|
|
|
|
use std::io;
|
|
|
|
|
use std::os::fd::{AsFd, AsRawFd};
|
|
|
|
|
use std::os::unix::ffi::{OsStrExt, OsStringExt};
|
|
|
|
|
use std::os::unix::fs::FileExt;
|
|
|
|
|
use std::os::unix::fs::OpenOptionsExt;
|
|
|
|
|
use std::path::{Path, PathBuf};
|
|
|
|
|
use std::sync::atomic::{AtomicU64, Ordering};
|
|
|
|
|
use std::sync::{Arc, Mutex};
|
|
|
|
|
use std::time::{Duration, SystemTime, UNIX_EPOCH};
|
|
|
|
|
|
|
|
|
|
use fuser::{
|
|
|
|
|
BsdFileFlags, Config, CopyFileRangeFlags, Errno, FileAttr, FileHandle, FileType, Filesystem,
|
|
|
|
|
FopenFlags, Generation, INodeNo, InitFlags, KernelConfig, LockFlags, LockOwner, MountOption,
|
|
|
|
|
OpenFlags, RenameFlags as FuseRenameFlags, ReplyAttr, ReplyCreate, ReplyData, ReplyDirectory,
|
|
|
|
|
ReplyEmpty, ReplyEntry, ReplyLock, ReplyLseek, ReplyOpen, ReplyStatfs, ReplyWrite, ReplyXattr,
|
|
|
|
|
Request, TimeOrNow, WriteFlags,
|
|
|
|
|
};
|
|
|
|
|
use nix::fcntl::{FcntlArg, fcntl};
|
|
|
|
|
use rustix::fs::{
|
|
|
|
|
FallocateFlags, FlockOperation, Mode, OFlags, RenameFlags, SeekFrom, Timestamps, UTIME_NOW,
|
|
|
|
|
UTIME_OMIT, XattrFlags, copy_file_range, fallocate, fchmod, fchown, fgetxattr, flistxattr,
|
|
|
|
|
flock as bsd_flock, fremovexattr, fsetxattr, fstat, fstatvfs, ftruncate, futimens, mknodat,
|
|
|
|
|
seek,
|
|
|
|
|
};
|
|
|
|
|
use rustix::process::{Gid, Uid};
|
|
|
|
|
use rustix::time::Timespec;
|
|
|
|
|
|
|
|
|
|
use crate::backing::{BackingRoot, validate_name, validate_relative};
|
|
|
|
|
use crate::classifier::tracked_paths_from_index;
|
|
|
|
|
use crate::classifier::{Classification, ClassificationSnapshot, GitClassifier};
|
|
|
|
|
|
|
|
|
|
const TTL: Duration = Duration::ZERO;
|
|
|
|
|
|
|
|
|
|
#[derive(Clone, Debug)]
|
|
|
|
|
pub struct MountConfiguration {
|
|
|
|
|
pub workspace: PathBuf,
|
|
|
|
|
pub isolated_state: PathBuf,
|
|
|
|
|
pub mountpoint: PathBuf,
|
|
|
|
|
pub global_excludes: Option<PathBuf>,
|
|
|
|
|
pub threads: usize,
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-18 02:08:14 +09:00
|
|
|
pub(crate) struct RouterConfiguration {
|
|
|
|
|
pub workspace_label: PathBuf,
|
|
|
|
|
pub workspace: File,
|
|
|
|
|
pub isolated_state: File,
|
|
|
|
|
pub mountpoint: PathBuf,
|
|
|
|
|
pub global_excludes: Option<PathBuf>,
|
|
|
|
|
pub isolate_ignored: bool,
|
|
|
|
|
pub threads: usize,
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-16 13:03:04 +09:00
|
|
|
pub struct IgnoredWorkspace {
|
|
|
|
|
host: BackingRoot,
|
|
|
|
|
isolated: BackingRoot,
|
|
|
|
|
classifier: GitClassifier,
|
2026-07-18 02:08:14 +09:00
|
|
|
isolate_ignored: bool,
|
2026-07-16 13:03:04 +09:00
|
|
|
nodes: Mutex<NodeTable>,
|
|
|
|
|
files: Mutex<HashMap<u64, Arc<OpenFile>>>,
|
|
|
|
|
directories: Mutex<HashMap<u64, DirectoryStream>>,
|
|
|
|
|
posix_lock_files: Mutex<HashMap<PosixLockKey, Arc<File>>>,
|
|
|
|
|
next_handle: AtomicU64,
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-18 02:08:14 +09:00
|
|
|
fn mount_config(threads: usize) -> Config {
|
|
|
|
|
let mut config = Config::default();
|
|
|
|
|
config.mount_options.extend([
|
|
|
|
|
MountOption::FSName("lazaret-ignored-workspace".to_owned()),
|
|
|
|
|
MountOption::RW,
|
|
|
|
|
MountOption::Exec,
|
|
|
|
|
MountOption::NoSuid,
|
|
|
|
|
MountOption::NoDev,
|
|
|
|
|
MountOption::DefaultPermissions,
|
|
|
|
|
]);
|
|
|
|
|
config.n_threads = Some(threads.max(1));
|
|
|
|
|
config.clone_fd = true;
|
|
|
|
|
config
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-16 13:03:04 +09:00
|
|
|
impl IgnoredWorkspace {
|
|
|
|
|
/// # Errors
|
|
|
|
|
/// Refuses roots that are not distinct, descriptor-openable directories in a Git workspace.
|
|
|
|
|
pub fn open(
|
|
|
|
|
workspace: &Path,
|
|
|
|
|
isolated_state: &Path,
|
|
|
|
|
global_excludes: Option<PathBuf>,
|
|
|
|
|
) -> io::Result<Self> {
|
|
|
|
|
let workspace = workspace.canonicalize()?;
|
|
|
|
|
let isolated_state = isolated_state.canonicalize()?;
|
|
|
|
|
if workspace == isolated_state {
|
|
|
|
|
return Err(io::Error::other("workspace and isolated roots must differ"));
|
|
|
|
|
}
|
|
|
|
|
let host = BackingRoot::open(&workspace)?;
|
|
|
|
|
let isolated = BackingRoot::open(&isolated_state)?;
|
|
|
|
|
Self::from_roots(workspace, host, isolated, global_excludes)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// # Errors
|
|
|
|
|
/// Refuses aliased or non-directory authorities and unreadable Git policy inputs.
|
|
|
|
|
pub fn from_authorities(
|
|
|
|
|
workspace_label: PathBuf,
|
|
|
|
|
workspace: File,
|
|
|
|
|
isolated_state: File,
|
|
|
|
|
global_excludes: Option<PathBuf>,
|
|
|
|
|
) -> io::Result<Self> {
|
|
|
|
|
let host = BackingRoot::from_file(workspace)?;
|
|
|
|
|
let isolated = BackingRoot::from_file(isolated_state)?;
|
|
|
|
|
Self::from_roots(workspace_label, host, isolated, global_excludes)
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-18 02:08:14 +09:00
|
|
|
/// # Errors
|
|
|
|
|
/// Refuses aliased or non-directory authorities and unreadable Git policy inputs.
|
|
|
|
|
pub fn from_authorities_with_policy(
|
|
|
|
|
workspace_label: PathBuf,
|
|
|
|
|
workspace: File,
|
|
|
|
|
isolated_state: File,
|
|
|
|
|
global_excludes: Option<PathBuf>,
|
|
|
|
|
isolate_ignored: bool,
|
|
|
|
|
) -> io::Result<Self> {
|
|
|
|
|
let host = BackingRoot::from_file(workspace)?;
|
|
|
|
|
let isolated = BackingRoot::from_file(isolated_state)?;
|
|
|
|
|
Self::from_roots_with_policy(
|
|
|
|
|
workspace_label,
|
|
|
|
|
host,
|
|
|
|
|
isolated,
|
|
|
|
|
global_excludes,
|
|
|
|
|
isolate_ignored,
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-16 13:03:04 +09:00
|
|
|
fn from_roots(
|
|
|
|
|
workspace_label: PathBuf,
|
|
|
|
|
host: BackingRoot,
|
|
|
|
|
isolated: BackingRoot,
|
|
|
|
|
global_excludes: Option<PathBuf>,
|
2026-07-18 02:08:14 +09:00
|
|
|
) -> io::Result<Self> {
|
|
|
|
|
Self::from_roots_with_policy(workspace_label, host, isolated, global_excludes, true)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn from_roots_with_policy(
|
|
|
|
|
workspace_label: PathBuf,
|
|
|
|
|
host: BackingRoot,
|
|
|
|
|
isolated: BackingRoot,
|
|
|
|
|
global_excludes: Option<PathBuf>,
|
|
|
|
|
isolate_ignored: bool,
|
2026-07-16 13:03:04 +09:00
|
|
|
) -> io::Result<Self> {
|
|
|
|
|
if host.identity()? == isolated.identity()? {
|
|
|
|
|
return Err(io::Error::other("workspace and isolated authorities alias"));
|
|
|
|
|
}
|
|
|
|
|
let classifier_root = BackingRoot::from_file(host.duplicate()?)?;
|
|
|
|
|
let classifier =
|
|
|
|
|
GitClassifier::from_root(workspace_label, classifier_root, global_excludes)?;
|
|
|
|
|
Ok(Self {
|
|
|
|
|
host,
|
|
|
|
|
isolated,
|
|
|
|
|
classifier,
|
2026-07-18 02:08:14 +09:00
|
|
|
isolate_ignored,
|
2026-07-16 13:03:04 +09:00
|
|
|
nodes: Mutex::new(NodeTable::new()),
|
|
|
|
|
files: Mutex::new(HashMap::new()),
|
|
|
|
|
directories: Mutex::new(HashMap::new()),
|
|
|
|
|
posix_lock_files: Mutex::new(HashMap::new()),
|
|
|
|
|
next_handle: AtomicU64::new(1),
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// # Errors
|
|
|
|
|
/// Returns mount or daemon I/O failures.
|
|
|
|
|
pub fn mount(configuration: MountConfiguration) -> io::Result<()> {
|
|
|
|
|
let filesystem = Self::open(
|
|
|
|
|
&configuration.workspace,
|
|
|
|
|
&configuration.isolated_state,
|
|
|
|
|
configuration.global_excludes,
|
|
|
|
|
)?;
|
2026-07-18 02:08:14 +09:00
|
|
|
let config = mount_config(configuration.threads);
|
2026-07-16 13:03:04 +09:00
|
|
|
fuser::mount2(filesystem, configuration.mountpoint, &config)
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-18 02:08:14 +09:00
|
|
|
/// # Errors
|
|
|
|
|
/// Returns mount, readiness, control-channel, unmount, or daemon I/O failures.
|
|
|
|
|
pub(crate) fn serve_from_authorities(
|
|
|
|
|
configuration: RouterConfiguration,
|
|
|
|
|
ready: impl FnOnce() -> io::Result<()>,
|
|
|
|
|
) -> io::Result<()> {
|
|
|
|
|
let filesystem = Self::from_authorities_with_policy(
|
|
|
|
|
configuration.workspace_label,
|
|
|
|
|
configuration.workspace,
|
|
|
|
|
configuration.isolated_state,
|
|
|
|
|
configuration.global_excludes,
|
|
|
|
|
configuration.isolate_ignored,
|
|
|
|
|
)?;
|
|
|
|
|
let session = fuser::spawn_mount2(
|
|
|
|
|
filesystem,
|
|
|
|
|
configuration.mountpoint,
|
|
|
|
|
&mount_config(configuration.threads),
|
|
|
|
|
)?;
|
|
|
|
|
if let Err(error) = ready() {
|
|
|
|
|
let _ = session.umount_and_join();
|
|
|
|
|
return Err(error);
|
|
|
|
|
}
|
|
|
|
|
std::io::copy(&mut std::io::stdin().lock(), &mut std::io::sink())?;
|
|
|
|
|
session.umount_and_join()
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-16 13:03:04 +09:00
|
|
|
fn root(&self, backing: Classification) -> &BackingRoot {
|
|
|
|
|
match backing {
|
|
|
|
|
Classification::Host => &self.host,
|
|
|
|
|
Classification::Isolated => &self.isolated,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn path_for(&self, inode: INodeNo) -> io::Result<PathBuf> {
|
|
|
|
|
self.nodes
|
|
|
|
|
.lock()
|
|
|
|
|
.unwrap_or_else(std::sync::PoisonError::into_inner)
|
|
|
|
|
.path(inode)
|
|
|
|
|
.ok_or_else(|| io::Error::from_raw_os_error(libc::ENOENT))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn child_path(&self, parent: INodeNo, name: &OsStr) -> io::Result<PathBuf> {
|
|
|
|
|
validate_name(name)?;
|
|
|
|
|
let path = self.path_for(parent)?.join(name);
|
|
|
|
|
validate_relative(&path)?;
|
|
|
|
|
Ok(path)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn inode_for(&self, entry: &ResolvedEntry) -> INodeNo {
|
|
|
|
|
if entry.path.as_os_str().is_empty() {
|
|
|
|
|
return INodeNo::ROOT;
|
|
|
|
|
}
|
|
|
|
|
self.nodes
|
|
|
|
|
.lock()
|
|
|
|
|
.unwrap_or_else(std::sync::PoisonError::into_inner)
|
|
|
|
|
.inode(entry)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn resolve(&self, snapshot: &ClassificationSnapshot, path: &Path) -> io::Result<ResolvedEntry> {
|
|
|
|
|
let host = self.host.stat(path).ok();
|
|
|
|
|
let isolated = self.isolated.stat(path).ok();
|
|
|
|
|
let directory = isolated
|
|
|
|
|
.as_ref()
|
|
|
|
|
.or(host.as_ref())
|
|
|
|
|
.is_some_and(|stat| stat.st_mode & libc::S_IFMT == libc::S_IFDIR);
|
2026-07-18 02:08:14 +09:00
|
|
|
let backing = self.classify(snapshot, path, directory);
|
2026-07-16 13:03:04 +09:00
|
|
|
if snapshot.is_tracked(path) {
|
|
|
|
|
if let (Some(host), Some(isolated)) = (&host, &isolated)
|
|
|
|
|
&& host.st_mode & libc::S_IFMT != isolated.st_mode & libc::S_IFMT
|
|
|
|
|
{
|
|
|
|
|
return Err(io::Error::from_raw_os_error(libc::EIO));
|
|
|
|
|
}
|
|
|
|
|
if backing == Classification::Host && host.is_none() && isolated.is_some() {
|
|
|
|
|
return Err(io::Error::from_raw_os_error(libc::EIO));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
let stat = match backing {
|
|
|
|
|
Classification::Host => host,
|
|
|
|
|
Classification::Isolated => isolated,
|
|
|
|
|
}
|
|
|
|
|
.ok_or_else(|| io::Error::from_raw_os_error(libc::ENOENT))?;
|
|
|
|
|
Ok(ResolvedEntry {
|
|
|
|
|
path: path.to_path_buf(),
|
|
|
|
|
backing,
|
|
|
|
|
stat,
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn create_backing(
|
|
|
|
|
&self,
|
|
|
|
|
snapshot: &ClassificationSnapshot,
|
|
|
|
|
path: &Path,
|
|
|
|
|
directory: bool,
|
|
|
|
|
) -> io::Result<Classification> {
|
2026-07-18 02:08:14 +09:00
|
|
|
let backing = self.classify(snapshot, path, directory);
|
2026-07-16 13:03:04 +09:00
|
|
|
if backing == Classification::Isolated {
|
|
|
|
|
self.isolated.ensure_parent_directories(path)?;
|
|
|
|
|
}
|
|
|
|
|
Ok(backing)
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-18 02:08:14 +09:00
|
|
|
fn classify(
|
|
|
|
|
&self,
|
|
|
|
|
snapshot: &ClassificationSnapshot,
|
|
|
|
|
path: &Path,
|
|
|
|
|
directory: bool,
|
|
|
|
|
) -> Classification {
|
|
|
|
|
if self.isolate_ignored {
|
|
|
|
|
snapshot.classify(path, directory)
|
|
|
|
|
} else {
|
|
|
|
|
Classification::Host
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-16 13:03:04 +09:00
|
|
|
fn open_file(&self, inode: INodeNo, flags: i32) -> io::Result<u64> {
|
|
|
|
|
let path = self.path_for(inode)?;
|
|
|
|
|
let snapshot = self.classifier.snapshot_for(&path);
|
|
|
|
|
let entry = self.resolve(&snapshot, &path)?;
|
|
|
|
|
let file = self
|
|
|
|
|
.root(entry.backing)
|
|
|
|
|
.open_entry(&path, open_flags(flags), Mode::empty())?;
|
|
|
|
|
let handle = self.next_handle.fetch_add(1, Ordering::Relaxed);
|
|
|
|
|
self.files
|
|
|
|
|
.lock()
|
|
|
|
|
.unwrap_or_else(std::sync::PoisonError::into_inner)
|
|
|
|
|
.insert(
|
|
|
|
|
handle,
|
|
|
|
|
Arc::new(OpenFile {
|
|
|
|
|
file,
|
|
|
|
|
backing: entry.backing,
|
|
|
|
|
access_mode: flags & libc::O_ACCMODE,
|
|
|
|
|
}),
|
|
|
|
|
);
|
|
|
|
|
Ok(handle)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn visible_entries(
|
|
|
|
|
&self,
|
|
|
|
|
directory: &Path,
|
|
|
|
|
snapshot: &ClassificationSnapshot,
|
|
|
|
|
) -> io::Result<Vec<DirectoryEntry>> {
|
|
|
|
|
let mut names = BTreeSet::new();
|
|
|
|
|
for root in [&self.host, &self.isolated] {
|
|
|
|
|
match root.entries(directory) {
|
|
|
|
|
Ok(entries) => names.extend(entries),
|
|
|
|
|
Err(error) if error.kind() == io::ErrorKind::NotFound => {}
|
|
|
|
|
Err(error) => return Err(error),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
let mut entries = Vec::new();
|
|
|
|
|
for name in names {
|
|
|
|
|
let path = directory.join(&name);
|
|
|
|
|
if let Ok(entry) = self.resolve(snapshot, &path) {
|
|
|
|
|
entries.push(DirectoryEntry {
|
|
|
|
|
inode: self.inode_for(&entry),
|
|
|
|
|
kind: file_type(entry.stat.st_mode)?,
|
|
|
|
|
name,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
Ok(entries)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn reply_entry(&self, path: &Path, reply: ReplyEntry) {
|
|
|
|
|
let snapshot = self.classifier.snapshot_for(path);
|
|
|
|
|
match self
|
|
|
|
|
.resolve(&snapshot, path)
|
|
|
|
|
.and_then(|entry| self.attributes(&entry))
|
|
|
|
|
{
|
2026-07-18 02:08:14 +09:00
|
|
|
Ok(attributes) => reply.entry(&TTL, &attributes, Generation(0)),
|
2026-07-16 13:03:04 +09:00
|
|
|
Err(error) => reply.error(errno(&error)),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn attributes(&self, entry: &ResolvedEntry) -> io::Result<FileAttr> {
|
|
|
|
|
let stat = entry.stat;
|
|
|
|
|
Ok(FileAttr {
|
|
|
|
|
ino: self.inode_for(entry),
|
|
|
|
|
size: u64::try_from(stat.st_size)
|
|
|
|
|
.map_err(|_| io::Error::other("negative file size"))?,
|
|
|
|
|
blocks: u64::try_from(stat.st_blocks)
|
|
|
|
|
.map_err(|_| io::Error::other("negative block count"))?,
|
|
|
|
|
atime: system_time(stat.st_atime, stat.st_atime_nsec),
|
|
|
|
|
mtime: system_time(stat.st_mtime, stat.st_mtime_nsec),
|
|
|
|
|
ctime: system_time(stat.st_ctime, stat.st_ctime_nsec),
|
|
|
|
|
crtime: UNIX_EPOCH,
|
|
|
|
|
kind: file_type(stat.st_mode)?,
|
|
|
|
|
perm: (stat.st_mode & 0o7777) as u16,
|
|
|
|
|
nlink: u32::try_from(stat.st_nlink).unwrap_or(u32::MAX),
|
|
|
|
|
uid: stat.st_uid,
|
|
|
|
|
gid: stat.st_gid,
|
|
|
|
|
rdev: u32::try_from(stat.st_rdev).unwrap_or(u32::MAX),
|
|
|
|
|
blksize: u32::try_from(stat.st_blksize).unwrap_or(u32::MAX),
|
|
|
|
|
flags: 0,
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn with_file<T>(
|
|
|
|
|
&self,
|
|
|
|
|
handle: FileHandle,
|
|
|
|
|
operation: impl FnOnce(&OpenFile) -> io::Result<T>,
|
|
|
|
|
) -> io::Result<T> {
|
|
|
|
|
let open = self
|
|
|
|
|
.files
|
|
|
|
|
.lock()
|
|
|
|
|
.unwrap_or_else(std::sync::PoisonError::into_inner)
|
|
|
|
|
.get(&handle.0)
|
|
|
|
|
.cloned()
|
|
|
|
|
.ok_or_else(|| io::Error::from_raw_os_error(libc::EBADF))?;
|
|
|
|
|
operation(&open)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn posix_lock_file(&self, open: &OpenFile, owner: LockOwner) -> io::Result<Arc<File>> {
|
|
|
|
|
let stat = fstat(open.file.as_fd())?;
|
|
|
|
|
let key = PosixLockKey {
|
|
|
|
|
backing: open.backing,
|
|
|
|
|
device: stat.st_dev,
|
|
|
|
|
inode: stat.st_ino,
|
|
|
|
|
owner: owner.0,
|
|
|
|
|
};
|
|
|
|
|
let mut files = self
|
|
|
|
|
.posix_lock_files
|
|
|
|
|
.lock()
|
|
|
|
|
.unwrap_or_else(std::sync::PoisonError::into_inner);
|
|
|
|
|
if let Some(file) = files.get(&key) {
|
|
|
|
|
return Ok(Arc::clone(file));
|
|
|
|
|
}
|
|
|
|
|
let mut options = OpenOptions::new();
|
|
|
|
|
match open.access_mode {
|
|
|
|
|
libc::O_RDONLY => {
|
|
|
|
|
options.read(true);
|
|
|
|
|
}
|
|
|
|
|
libc::O_WRONLY => {
|
|
|
|
|
options.write(true);
|
|
|
|
|
}
|
|
|
|
|
libc::O_RDWR => {
|
|
|
|
|
options.read(true).write(true);
|
|
|
|
|
}
|
|
|
|
|
_ => return Err(io::Error::from_raw_os_error(libc::EINVAL)),
|
|
|
|
|
}
|
|
|
|
|
let file = Arc::new(
|
|
|
|
|
options
|
|
|
|
|
.custom_flags(libc::O_CLOEXEC)
|
|
|
|
|
.open(format!("/proc/self/fd/{}", open.file.as_raw_fd()))?,
|
|
|
|
|
);
|
|
|
|
|
let reopened = fstat(file.as_fd())?;
|
|
|
|
|
if (reopened.st_dev, reopened.st_ino) != (stat.st_dev, stat.st_ino) {
|
|
|
|
|
return Err(io::Error::other("lock descriptor identity changed"));
|
|
|
|
|
}
|
|
|
|
|
files.insert(key, Arc::clone(&file));
|
|
|
|
|
Ok(file)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn release_posix_locks(&self, open: &OpenFile, owner: LockOwner) -> io::Result<()> {
|
|
|
|
|
let stat = fstat(open.file.as_fd())?;
|
|
|
|
|
self.posix_lock_files
|
|
|
|
|
.lock()
|
|
|
|
|
.unwrap_or_else(std::sync::PoisonError::into_inner)
|
|
|
|
|
.remove(&PosixLockKey {
|
|
|
|
|
backing: open.backing,
|
|
|
|
|
device: stat.st_dev,
|
|
|
|
|
inode: stat.st_ino,
|
|
|
|
|
owner: owner.0,
|
|
|
|
|
});
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl Filesystem for IgnoredWorkspace {
|
|
|
|
|
fn init(&mut self, _request: &Request, config: &mut KernelConfig) -> io::Result<()> {
|
|
|
|
|
config
|
|
|
|
|
.add_capabilities(
|
|
|
|
|
InitFlags::FUSE_AUTO_INVAL_DATA
|
|
|
|
|
| InitFlags::FUSE_POSIX_ACL
|
|
|
|
|
| InitFlags::FUSE_POSIX_LOCKS
|
|
|
|
|
| InitFlags::FUSE_FLOCK_LOCKS,
|
|
|
|
|
)
|
|
|
|
|
.map_err(|unsupported| {
|
|
|
|
|
io::Error::other(format!(
|
|
|
|
|
"required FUSE capabilities are unavailable: {unsupported:?}"
|
|
|
|
|
))
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn lookup(&self, _request: &Request, parent: INodeNo, name: &OsStr, reply: ReplyEntry) {
|
|
|
|
|
match self.child_path(parent, name) {
|
|
|
|
|
Ok(path) => self.reply_entry(&path, reply),
|
|
|
|
|
Err(error) => reply.error(errno(&error)),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn getattr(
|
|
|
|
|
&self,
|
|
|
|
|
_request: &Request,
|
|
|
|
|
inode: INodeNo,
|
|
|
|
|
handle: Option<FileHandle>,
|
|
|
|
|
reply: ReplyAttr,
|
|
|
|
|
) {
|
|
|
|
|
let result = if let Some(handle) = handle {
|
|
|
|
|
self.with_file(handle, |open| {
|
|
|
|
|
let stat = fstat(open.file.as_fd())?;
|
|
|
|
|
let path = self.path_for(inode)?;
|
|
|
|
|
self.attributes(&ResolvedEntry {
|
|
|
|
|
path,
|
|
|
|
|
backing: open.backing,
|
|
|
|
|
stat,
|
|
|
|
|
})
|
|
|
|
|
})
|
|
|
|
|
} else {
|
|
|
|
|
self.path_for(inode).and_then(|path| {
|
|
|
|
|
let snapshot = self.classifier.snapshot_for(&path);
|
|
|
|
|
self.resolve(&snapshot, &path)
|
|
|
|
|
.and_then(|entry| self.attributes(&entry))
|
|
|
|
|
})
|
|
|
|
|
};
|
|
|
|
|
match result {
|
|
|
|
|
Ok(attributes) => reply.attr(&TTL, &attributes),
|
|
|
|
|
Err(error) => reply.error(errno(&error)),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn setattr(
|
|
|
|
|
&self,
|
|
|
|
|
_request: &Request,
|
|
|
|
|
inode: INodeNo,
|
|
|
|
|
mode: Option<u32>,
|
|
|
|
|
uid: Option<u32>,
|
|
|
|
|
gid: Option<u32>,
|
|
|
|
|
size: Option<u64>,
|
|
|
|
|
atime: Option<TimeOrNow>,
|
|
|
|
|
mtime: Option<TimeOrNow>,
|
|
|
|
|
_ctime: Option<SystemTime>,
|
|
|
|
|
handle: Option<FileHandle>,
|
|
|
|
|
_crtime: Option<SystemTime>,
|
|
|
|
|
_chgtime: Option<SystemTime>,
|
|
|
|
|
_bkuptime: Option<SystemTime>,
|
|
|
|
|
_flags: Option<BsdFileFlags>,
|
|
|
|
|
reply: ReplyAttr,
|
|
|
|
|
) {
|
|
|
|
|
let operation = |open: &OpenFile| -> io::Result<FileAttr> {
|
|
|
|
|
if let Some(mode) = mode {
|
|
|
|
|
fchmod(open.file.as_fd(), Mode::from_raw_mode(mode & 0o7777))?;
|
|
|
|
|
}
|
|
|
|
|
if uid.is_some() || gid.is_some() {
|
|
|
|
|
fchown(
|
|
|
|
|
open.file.as_fd(),
|
|
|
|
|
uid.map(Uid::from_raw),
|
|
|
|
|
gid.map(Gid::from_raw),
|
|
|
|
|
)?;
|
|
|
|
|
}
|
|
|
|
|
if let Some(size) = size {
|
|
|
|
|
ftruncate(open.file.as_fd(), size)?;
|
|
|
|
|
}
|
|
|
|
|
if atime.is_some() || mtime.is_some() {
|
|
|
|
|
futimens(
|
|
|
|
|
open.file.as_fd(),
|
|
|
|
|
&Timestamps {
|
|
|
|
|
last_access: timestamp(atime),
|
|
|
|
|
last_modification: timestamp(mtime),
|
|
|
|
|
},
|
|
|
|
|
)?;
|
|
|
|
|
}
|
|
|
|
|
let path = self.path_for(inode)?;
|
|
|
|
|
self.attributes(&ResolvedEntry {
|
|
|
|
|
path,
|
|
|
|
|
backing: open.backing,
|
|
|
|
|
stat: fstat(open.file.as_fd())?,
|
|
|
|
|
})
|
|
|
|
|
};
|
|
|
|
|
let result = if let Some(handle) = handle {
|
|
|
|
|
self.with_file(handle, operation)
|
|
|
|
|
} else {
|
|
|
|
|
let path = match self.path_for(inode) {
|
|
|
|
|
Ok(path) => path,
|
|
|
|
|
Err(error) => {
|
|
|
|
|
reply.error(errno(&error));
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
let snapshot = self.classifier.snapshot_for(&path);
|
|
|
|
|
let entry = match self.resolve(&snapshot, &path) {
|
|
|
|
|
Ok(entry) => entry,
|
|
|
|
|
Err(error) => {
|
|
|
|
|
reply.error(errno(&error));
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
if entry.stat.st_mode & libc::S_IFMT == libc::S_IFLNK {
|
|
|
|
|
if mode.is_some() || uid.is_some() || gid.is_some() || size.is_some() {
|
|
|
|
|
Err(io::Error::from_raw_os_error(libc::EOPNOTSUPP))
|
|
|
|
|
} else {
|
|
|
|
|
self.root(entry.backing)
|
|
|
|
|
.set_symlink_times(
|
|
|
|
|
&path,
|
|
|
|
|
&Timestamps {
|
|
|
|
|
last_access: timestamp(atime),
|
|
|
|
|
last_modification: timestamp(mtime),
|
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
.and_then(|()| {
|
|
|
|
|
self.root(entry.backing).stat(&path).and_then(|stat| {
|
|
|
|
|
self.attributes(&ResolvedEntry {
|
|
|
|
|
path,
|
|
|
|
|
backing: entry.backing,
|
|
|
|
|
stat,
|
|
|
|
|
})
|
|
|
|
|
})
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
let flags = if size.is_some() {
|
|
|
|
|
libc::O_RDWR
|
|
|
|
|
} else {
|
|
|
|
|
libc::O_RDONLY
|
|
|
|
|
};
|
|
|
|
|
self.open_file(inode, flags).and_then(|handle| {
|
|
|
|
|
let result = self.with_file(FileHandle(handle), operation);
|
|
|
|
|
self.files
|
|
|
|
|
.lock()
|
|
|
|
|
.unwrap_or_else(std::sync::PoisonError::into_inner)
|
|
|
|
|
.remove(&handle);
|
|
|
|
|
result
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
match result {
|
|
|
|
|
Ok(attributes) => reply.attr(&TTL, &attributes),
|
|
|
|
|
Err(error) => reply.error(errno(&error)),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn readlink(&self, _request: &Request, inode: INodeNo, reply: ReplyData) {
|
|
|
|
|
let result = self.path_for(inode).and_then(|path| {
|
|
|
|
|
let snapshot = self.classifier.snapshot_for(&path);
|
|
|
|
|
let entry = self.resolve(&snapshot, &path)?;
|
|
|
|
|
self.root(entry.backing).readlink(&path)
|
|
|
|
|
});
|
|
|
|
|
match result {
|
|
|
|
|
Ok(bytes) => reply.data(&bytes),
|
|
|
|
|
Err(error) => reply.error(errno(&error)),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn mknod(
|
|
|
|
|
&self,
|
|
|
|
|
_request: &Request,
|
|
|
|
|
parent: INodeNo,
|
|
|
|
|
name: &OsStr,
|
|
|
|
|
mode: u32,
|
|
|
|
|
umask: u32,
|
|
|
|
|
rdev: u32,
|
|
|
|
|
reply: ReplyEntry,
|
|
|
|
|
) {
|
|
|
|
|
if matches!(mode & libc::S_IFMT, libc::S_IFCHR | libc::S_IFBLK) {
|
|
|
|
|
reply.error(Errno::EPERM);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
let result = (|| {
|
|
|
|
|
let path = self.child_path(parent, name)?;
|
|
|
|
|
let snapshot = self.classifier.snapshot_for(&path);
|
|
|
|
|
let backing = self.create_backing(&snapshot, &path, false)?;
|
|
|
|
|
let root = self.root(backing);
|
|
|
|
|
let parent_path = path.parent().unwrap_or_else(|| Path::new(""));
|
|
|
|
|
let directory = root.open_directory(parent_path)?;
|
|
|
|
|
mknodat(
|
|
|
|
|
directory.as_fd(),
|
|
|
|
|
name,
|
|
|
|
|
rustix::fs::FileType::from_raw_mode(mode),
|
|
|
|
|
Mode::from_raw_mode(mode & !umask),
|
|
|
|
|
rustix::fs::makedev(rdev >> 8, rdev & 0xff),
|
|
|
|
|
)?;
|
|
|
|
|
Ok(path)
|
|
|
|
|
})();
|
|
|
|
|
match result {
|
|
|
|
|
Ok(path) => self.reply_entry(&path, reply),
|
|
|
|
|
Err(error) => reply.error(errno(&error)),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn mkdir(
|
|
|
|
|
&self,
|
|
|
|
|
_request: &Request,
|
|
|
|
|
parent: INodeNo,
|
|
|
|
|
name: &OsStr,
|
|
|
|
|
mode: u32,
|
|
|
|
|
umask: u32,
|
|
|
|
|
reply: ReplyEntry,
|
|
|
|
|
) {
|
|
|
|
|
let result = (|| {
|
|
|
|
|
let path = self.child_path(parent, name)?;
|
|
|
|
|
let snapshot = self.classifier.snapshot_for(&path);
|
|
|
|
|
let backing = self.create_backing(&snapshot, &path, true)?;
|
|
|
|
|
self.root(backing)
|
|
|
|
|
.mkdir(&path, Mode::from_raw_mode((mode & !umask) & 0o7777))?;
|
|
|
|
|
Ok(path)
|
|
|
|
|
})();
|
|
|
|
|
match result {
|
|
|
|
|
Ok(path) => self.reply_entry(&path, reply),
|
|
|
|
|
Err(error) => reply.error(errno(&error)),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn unlink(&self, _request: &Request, parent: INodeNo, name: &OsStr, reply: ReplyEmpty) {
|
|
|
|
|
self.remove(parent, name, false, reply);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn rmdir(&self, _request: &Request, parent: INodeNo, name: &OsStr, reply: ReplyEmpty) {
|
|
|
|
|
self.remove(parent, name, true, reply);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn symlink(
|
|
|
|
|
&self,
|
|
|
|
|
_request: &Request,
|
|
|
|
|
parent: INodeNo,
|
|
|
|
|
name: &OsStr,
|
|
|
|
|
target: &Path,
|
|
|
|
|
reply: ReplyEntry,
|
|
|
|
|
) {
|
|
|
|
|
let result = (|| {
|
|
|
|
|
let path = self.child_path(parent, name)?;
|
|
|
|
|
let snapshot = self.classifier.snapshot_for(&path);
|
|
|
|
|
let backing = self.create_backing(&snapshot, &path, false)?;
|
|
|
|
|
self.root(backing).symlink(target, &path)?;
|
|
|
|
|
Ok(path)
|
|
|
|
|
})();
|
|
|
|
|
match result {
|
|
|
|
|
Ok(path) => self.reply_entry(&path, reply),
|
|
|
|
|
Err(error) => reply.error(errno(&error)),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn rename(
|
|
|
|
|
&self,
|
|
|
|
|
_request: &Request,
|
|
|
|
|
parent: INodeNo,
|
|
|
|
|
name: &OsStr,
|
|
|
|
|
new_parent: INodeNo,
|
|
|
|
|
new_name: &OsStr,
|
|
|
|
|
flags: FuseRenameFlags,
|
|
|
|
|
reply: ReplyEmpty,
|
|
|
|
|
) {
|
|
|
|
|
if flags.contains(FuseRenameFlags::RENAME_WHITEOUT) {
|
|
|
|
|
reply.error(Errno::EOPNOTSUPP);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
let result = (|| {
|
|
|
|
|
let source = self.child_path(parent, name)?;
|
|
|
|
|
let destination = self.child_path(new_parent, new_name)?;
|
|
|
|
|
let snapshot = self.classifier.snapshot_for_paths(&[&source, &destination]);
|
|
|
|
|
let source_entry = self.resolve(&snapshot, &source)?;
|
2026-07-18 02:08:14 +09:00
|
|
|
let destination_backing = self.classify(
|
|
|
|
|
&snapshot,
|
2026-07-16 13:03:04 +09:00
|
|
|
&destination,
|
|
|
|
|
source_entry.stat.st_mode & libc::S_IFMT == libc::S_IFDIR,
|
|
|
|
|
);
|
|
|
|
|
if source_entry.backing != destination_backing {
|
|
|
|
|
return Err(io::Error::from_raw_os_error(libc::EXDEV));
|
|
|
|
|
}
|
|
|
|
|
if destination == Path::new(".git/index") {
|
|
|
|
|
self.require_index_publication_safe(source_entry.backing, &source, &snapshot)?;
|
|
|
|
|
}
|
|
|
|
|
let flags = RenameFlags::from_bits_retain(flags.bits());
|
|
|
|
|
self.root(source_entry.backing)
|
|
|
|
|
.rename(&source, &destination, flags)
|
|
|
|
|
})();
|
|
|
|
|
reply_result(result, reply);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn link(
|
|
|
|
|
&self,
|
|
|
|
|
_request: &Request,
|
|
|
|
|
inode: INodeNo,
|
|
|
|
|
new_parent: INodeNo,
|
|
|
|
|
new_name: &OsStr,
|
|
|
|
|
reply: ReplyEntry,
|
|
|
|
|
) {
|
|
|
|
|
let result = (|| {
|
|
|
|
|
let source = self.path_for(inode)?;
|
|
|
|
|
let destination = self.child_path(new_parent, new_name)?;
|
|
|
|
|
let snapshot = self.classifier.snapshot_for_paths(&[&source, &destination]);
|
|
|
|
|
let source_entry = self.resolve(&snapshot, &source)?;
|
2026-07-18 02:08:14 +09:00
|
|
|
let destination_backing = self.classify(&snapshot, &destination, false);
|
2026-07-16 13:03:04 +09:00
|
|
|
if source_entry.backing != destination_backing {
|
|
|
|
|
return Err(io::Error::from_raw_os_error(libc::EXDEV));
|
|
|
|
|
}
|
|
|
|
|
self.root(source_entry.backing)
|
|
|
|
|
.link(&source, &destination)?;
|
|
|
|
|
Ok(destination)
|
|
|
|
|
})();
|
|
|
|
|
match result {
|
|
|
|
|
Ok(path) => self.reply_entry(&path, reply),
|
|
|
|
|
Err(error) => reply.error(errno(&error)),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn open(&self, _request: &Request, inode: INodeNo, flags: OpenFlags, reply: ReplyOpen) {
|
|
|
|
|
match self.open_file(inode, flags.0) {
|
|
|
|
|
Ok(handle) => reply.opened(FileHandle(handle), FopenFlags::empty()),
|
|
|
|
|
Err(error) => reply.error(errno(&error)),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn read(
|
|
|
|
|
&self,
|
|
|
|
|
_request: &Request,
|
|
|
|
|
_inode: INodeNo,
|
|
|
|
|
handle: FileHandle,
|
|
|
|
|
offset: u64,
|
|
|
|
|
size: u32,
|
|
|
|
|
_flags: OpenFlags,
|
|
|
|
|
_lock_owner: Option<LockOwner>,
|
|
|
|
|
reply: ReplyData,
|
|
|
|
|
) {
|
|
|
|
|
let result = self.with_file(handle, |open| {
|
|
|
|
|
let mut bytes = vec![0; size as usize];
|
|
|
|
|
let read = open.file.read_at(&mut bytes, offset)?;
|
|
|
|
|
bytes.truncate(read);
|
|
|
|
|
Ok(bytes)
|
|
|
|
|
});
|
|
|
|
|
match result {
|
|
|
|
|
Ok(bytes) => reply.data(&bytes),
|
|
|
|
|
Err(error) => reply.error(errno(&error)),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn write(
|
|
|
|
|
&self,
|
|
|
|
|
_request: &Request,
|
|
|
|
|
_inode: INodeNo,
|
|
|
|
|
handle: FileHandle,
|
|
|
|
|
offset: u64,
|
|
|
|
|
data: &[u8],
|
|
|
|
|
_write_flags: WriteFlags,
|
|
|
|
|
flags: OpenFlags,
|
|
|
|
|
_lock_owner: Option<LockOwner>,
|
|
|
|
|
reply: ReplyWrite,
|
|
|
|
|
) {
|
|
|
|
|
let result = self.with_file(handle, |open| {
|
|
|
|
|
let offset = if flags.0 & libc::O_APPEND != 0 {
|
|
|
|
|
open.file.metadata()?.len()
|
|
|
|
|
} else {
|
|
|
|
|
offset
|
|
|
|
|
};
|
|
|
|
|
open.file.write_at(data, offset)
|
|
|
|
|
});
|
|
|
|
|
match result {
|
|
|
|
|
Ok(written) => reply.written(u32::try_from(written).unwrap_or(u32::MAX)),
|
|
|
|
|
Err(error) => reply.error(errno(&error)),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn flush(
|
|
|
|
|
&self,
|
|
|
|
|
_request: &Request,
|
|
|
|
|
_inode: INodeNo,
|
|
|
|
|
handle: FileHandle,
|
|
|
|
|
owner: LockOwner,
|
|
|
|
|
reply: ReplyEmpty,
|
|
|
|
|
) {
|
|
|
|
|
let result = self.with_file(handle, |open| {
|
|
|
|
|
self.release_posix_locks(open, owner)?;
|
|
|
|
|
open.file.sync_data()
|
|
|
|
|
});
|
|
|
|
|
reply_result(result, reply);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn release(
|
|
|
|
|
&self,
|
|
|
|
|
_request: &Request,
|
|
|
|
|
_inode: INodeNo,
|
|
|
|
|
handle: FileHandle,
|
|
|
|
|
_flags: OpenFlags,
|
|
|
|
|
owner: Option<LockOwner>,
|
|
|
|
|
_flush: bool,
|
|
|
|
|
reply: ReplyEmpty,
|
|
|
|
|
) {
|
|
|
|
|
let open = self
|
|
|
|
|
.files
|
|
|
|
|
.lock()
|
|
|
|
|
.unwrap_or_else(std::sync::PoisonError::into_inner)
|
|
|
|
|
.remove(&handle.0);
|
|
|
|
|
let result = match (open, owner) {
|
|
|
|
|
(Some(open), Some(owner)) => self.release_posix_locks(&open, owner),
|
|
|
|
|
(Some(_), None) => Ok(()),
|
|
|
|
|
(None, _) => Err(io::Error::from_raw_os_error(libc::EBADF)),
|
|
|
|
|
};
|
|
|
|
|
reply_result(result, reply);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn fsync(
|
|
|
|
|
&self,
|
|
|
|
|
_request: &Request,
|
|
|
|
|
_inode: INodeNo,
|
|
|
|
|
handle: FileHandle,
|
|
|
|
|
datasync: bool,
|
|
|
|
|
reply: ReplyEmpty,
|
|
|
|
|
) {
|
|
|
|
|
let result = self.with_file(handle, |open| {
|
|
|
|
|
if datasync {
|
|
|
|
|
open.file.sync_data()
|
|
|
|
|
} else {
|
|
|
|
|
open.file.sync_all()
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
reply_result(result, reply);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn getlk(
|
|
|
|
|
&self,
|
|
|
|
|
_request: &Request,
|
|
|
|
|
_inode: INodeNo,
|
|
|
|
|
handle: FileHandle,
|
|
|
|
|
owner: LockOwner,
|
|
|
|
|
start: u64,
|
|
|
|
|
end: u64,
|
|
|
|
|
typ: i32,
|
|
|
|
|
_pid: u32,
|
|
|
|
|
flags: LockFlags,
|
|
|
|
|
reply: ReplyLock,
|
|
|
|
|
) {
|
|
|
|
|
if unsupported_lock_flags(flags) {
|
|
|
|
|
reply.error(Errno::EOPNOTSUPP);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
if flags.contains(LockFlags::FUSE_LK_FLOCK) {
|
|
|
|
|
reply.locked(0, u64::MAX, libc::F_UNLCK, 0);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
let result = self.with_file(handle, |open| {
|
|
|
|
|
let file = self.posix_lock_file(open, owner)?;
|
|
|
|
|
let mut lock = record_lock(start, end, typ)?;
|
|
|
|
|
fcntl(file.as_fd(), FcntlArg::F_OFD_GETLK(&mut lock))?;
|
|
|
|
|
fuse_lock(&lock)
|
|
|
|
|
});
|
|
|
|
|
match result {
|
|
|
|
|
Ok((start, end, typ, pid)) => reply.locked(start, end, typ, pid),
|
|
|
|
|
Err(error) => reply.error(errno(&error)),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn setlk(
|
|
|
|
|
&self,
|
|
|
|
|
_request: &Request,
|
|
|
|
|
_inode: INodeNo,
|
|
|
|
|
handle: FileHandle,
|
|
|
|
|
owner: LockOwner,
|
|
|
|
|
start: u64,
|
|
|
|
|
end: u64,
|
|
|
|
|
typ: i32,
|
|
|
|
|
_pid: u32,
|
|
|
|
|
sleep: bool,
|
|
|
|
|
flags: LockFlags,
|
|
|
|
|
reply: ReplyEmpty,
|
|
|
|
|
) {
|
|
|
|
|
if unsupported_lock_flags(flags) {
|
|
|
|
|
reply.error(Errno::EOPNOTSUPP);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
let result = self.with_file(handle, |open| {
|
|
|
|
|
if flags.contains(LockFlags::FUSE_LK_FLOCK) {
|
|
|
|
|
if (start, end) != (0, i64::MAX as u64) {
|
|
|
|
|
return Err(io::Error::from_raw_os_error(libc::EINVAL));
|
|
|
|
|
}
|
|
|
|
|
return Ok(bsd_flock(
|
|
|
|
|
open.file.as_fd(),
|
|
|
|
|
bsd_lock_operation(typ, sleep)?,
|
|
|
|
|
)?);
|
|
|
|
|
}
|
|
|
|
|
let file = self.posix_lock_file(open, owner)?;
|
|
|
|
|
let lock = record_lock(start, end, typ)?;
|
|
|
|
|
let command = if sleep {
|
|
|
|
|
FcntlArg::F_OFD_SETLKW(&lock)
|
|
|
|
|
} else {
|
|
|
|
|
FcntlArg::F_OFD_SETLK(&lock)
|
|
|
|
|
};
|
|
|
|
|
fcntl(file.as_fd(), command)?;
|
|
|
|
|
Ok(())
|
|
|
|
|
});
|
|
|
|
|
reply_result(result, reply);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn opendir(&self, _request: &Request, inode: INodeNo, _flags: OpenFlags, reply: ReplyOpen) {
|
|
|
|
|
let result = (|| {
|
|
|
|
|
let path = self.path_for(inode)?;
|
|
|
|
|
let snapshot = self.classifier.snapshot_for(&path);
|
|
|
|
|
let entry = self.resolve(&snapshot, &path)?;
|
|
|
|
|
let directory = self.root(entry.backing).open_directory(&path)?;
|
|
|
|
|
let mut entries = vec![
|
|
|
|
|
DirectoryEntry {
|
|
|
|
|
inode,
|
|
|
|
|
kind: FileType::Directory,
|
|
|
|
|
name: OsString::from("."),
|
|
|
|
|
},
|
|
|
|
|
DirectoryEntry {
|
|
|
|
|
inode: if path.as_os_str().is_empty() {
|
|
|
|
|
INodeNo::ROOT
|
|
|
|
|
} else {
|
|
|
|
|
let parent = path.parent().unwrap_or_else(|| Path::new(""));
|
|
|
|
|
let parent_entry = self.resolve(&snapshot, parent)?;
|
|
|
|
|
self.inode_for(&parent_entry)
|
|
|
|
|
},
|
|
|
|
|
kind: FileType::Directory,
|
|
|
|
|
name: OsString::from(".."),
|
|
|
|
|
},
|
|
|
|
|
];
|
|
|
|
|
entries.extend(self.visible_entries(&path, &snapshot)?);
|
|
|
|
|
let handle = self.next_handle.fetch_add(1, Ordering::Relaxed);
|
|
|
|
|
self.directories
|
|
|
|
|
.lock()
|
|
|
|
|
.unwrap_or_else(std::sync::PoisonError::into_inner)
|
|
|
|
|
.insert(
|
|
|
|
|
handle,
|
|
|
|
|
DirectoryStream {
|
|
|
|
|
directory,
|
|
|
|
|
entries,
|
|
|
|
|
_snapshot: snapshot,
|
|
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
Ok(handle)
|
|
|
|
|
})();
|
|
|
|
|
match result {
|
|
|
|
|
Ok(handle) => reply.opened(FileHandle(handle), FopenFlags::empty()),
|
|
|
|
|
Err(error) => reply.error(errno(&error)),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn readdir(
|
|
|
|
|
&self,
|
|
|
|
|
_request: &Request,
|
|
|
|
|
_inode: INodeNo,
|
|
|
|
|
handle: FileHandle,
|
|
|
|
|
offset: u64,
|
|
|
|
|
mut reply: ReplyDirectory,
|
|
|
|
|
) {
|
|
|
|
|
let directories = self
|
|
|
|
|
.directories
|
|
|
|
|
.lock()
|
|
|
|
|
.unwrap_or_else(std::sync::PoisonError::into_inner);
|
|
|
|
|
let Some(directory) = directories.get(&handle.0) else {
|
|
|
|
|
reply.error(Errno::EBADF);
|
|
|
|
|
return;
|
|
|
|
|
};
|
|
|
|
|
for (index, entry) in directory
|
|
|
|
|
.entries
|
|
|
|
|
.iter()
|
|
|
|
|
.enumerate()
|
|
|
|
|
.skip(usize::try_from(offset).unwrap_or(usize::MAX))
|
|
|
|
|
{
|
|
|
|
|
if reply.add(entry.inode, (index + 1) as u64, entry.kind, &entry.name) {
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
reply.ok();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn releasedir(
|
|
|
|
|
&self,
|
|
|
|
|
_request: &Request,
|
|
|
|
|
_inode: INodeNo,
|
|
|
|
|
handle: FileHandle,
|
|
|
|
|
_flags: OpenFlags,
|
|
|
|
|
reply: ReplyEmpty,
|
|
|
|
|
) {
|
|
|
|
|
self.directories
|
|
|
|
|
.lock()
|
|
|
|
|
.unwrap_or_else(std::sync::PoisonError::into_inner)
|
|
|
|
|
.remove(&handle.0);
|
|
|
|
|
reply.ok();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn fsyncdir(
|
|
|
|
|
&self,
|
|
|
|
|
_request: &Request,
|
|
|
|
|
_inode: INodeNo,
|
|
|
|
|
handle: FileHandle,
|
|
|
|
|
datasync: bool,
|
|
|
|
|
reply: ReplyEmpty,
|
|
|
|
|
) {
|
|
|
|
|
let directories = self
|
|
|
|
|
.directories
|
|
|
|
|
.lock()
|
|
|
|
|
.unwrap_or_else(std::sync::PoisonError::into_inner);
|
|
|
|
|
let result = directories
|
|
|
|
|
.get(&handle.0)
|
|
|
|
|
.ok_or_else(|| io::Error::from_raw_os_error(libc::EBADF))
|
|
|
|
|
.and_then(|stream| {
|
|
|
|
|
if datasync {
|
|
|
|
|
stream.directory.sync_data()
|
|
|
|
|
} else {
|
|
|
|
|
stream.directory.sync_all()
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
reply_result(result, reply);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn statfs(&self, _request: &Request, _inode: INodeNo, reply: ReplyStatfs) {
|
|
|
|
|
match self
|
|
|
|
|
.host
|
|
|
|
|
.duplicate()
|
|
|
|
|
.and_then(|file| Ok(fstatvfs(file.as_fd())?))
|
|
|
|
|
{
|
|
|
|
|
Ok(stat) => reply.statfs(
|
|
|
|
|
stat.f_blocks,
|
|
|
|
|
stat.f_bfree,
|
|
|
|
|
stat.f_bavail,
|
|
|
|
|
stat.f_files,
|
|
|
|
|
stat.f_ffree,
|
|
|
|
|
u32::try_from(stat.f_bsize).unwrap_or(u32::MAX),
|
|
|
|
|
u32::try_from(stat.f_namemax).unwrap_or(u32::MAX),
|
|
|
|
|
u32::try_from(stat.f_frsize).unwrap_or(u32::MAX),
|
|
|
|
|
),
|
|
|
|
|
Err(error) => reply.error(errno(&error)),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn setxattr(
|
|
|
|
|
&self,
|
|
|
|
|
_request: &Request,
|
|
|
|
|
inode: INodeNo,
|
|
|
|
|
name: &OsStr,
|
|
|
|
|
value: &[u8],
|
|
|
|
|
flags: i32,
|
|
|
|
|
position: u32,
|
|
|
|
|
reply: ReplyEmpty,
|
|
|
|
|
) {
|
|
|
|
|
if position != 0 || !writable_xattr(name) {
|
|
|
|
|
reply.error(Errno::ENOTSUP);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
let result = self.with_temporary_file(inode, |file| {
|
|
|
|
|
Ok(fsetxattr(
|
|
|
|
|
file.as_fd(),
|
|
|
|
|
name,
|
|
|
|
|
value,
|
|
|
|
|
XattrFlags::from_bits_retain(u32::from_ne_bytes(flags.to_ne_bytes())),
|
|
|
|
|
)?)
|
|
|
|
|
});
|
|
|
|
|
reply_result(result, reply);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn getxattr(
|
|
|
|
|
&self,
|
|
|
|
|
_request: &Request,
|
|
|
|
|
inode: INodeNo,
|
|
|
|
|
name: &OsStr,
|
|
|
|
|
size: u32,
|
|
|
|
|
reply: ReplyXattr,
|
|
|
|
|
) {
|
|
|
|
|
let result = self.with_temporary_file(inode, |file| {
|
|
|
|
|
let mut bytes = vec![0; 64 * 1024];
|
|
|
|
|
let length = fgetxattr(file.as_fd(), name, &mut bytes)?;
|
|
|
|
|
bytes.truncate(length);
|
|
|
|
|
Ok(bytes)
|
|
|
|
|
});
|
|
|
|
|
reply_xattr(result, size, reply);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn listxattr(&self, _request: &Request, inode: INodeNo, size: u32, reply: ReplyXattr) {
|
|
|
|
|
let result = self.with_temporary_file(inode, |file| {
|
|
|
|
|
let mut bytes = vec![0; 64 * 1024];
|
|
|
|
|
let length = flistxattr(file.as_fd(), &mut bytes)?;
|
|
|
|
|
bytes.truncate(length);
|
|
|
|
|
Ok(bytes)
|
|
|
|
|
});
|
|
|
|
|
reply_xattr(result, size, reply);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn removexattr(&self, _request: &Request, inode: INodeNo, name: &OsStr, reply: ReplyEmpty) {
|
|
|
|
|
if !writable_xattr(name) {
|
|
|
|
|
reply.error(Errno::ENOTSUP);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
let result = self.with_temporary_file(inode, |file| Ok(fremovexattr(file.as_fd(), name)?));
|
|
|
|
|
reply_result(result, reply);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn create(
|
|
|
|
|
&self,
|
|
|
|
|
_request: &Request,
|
|
|
|
|
parent: INodeNo,
|
|
|
|
|
name: &OsStr,
|
|
|
|
|
mode: u32,
|
|
|
|
|
umask: u32,
|
|
|
|
|
flags: i32,
|
|
|
|
|
reply: ReplyCreate,
|
|
|
|
|
) {
|
|
|
|
|
let result = (|| {
|
|
|
|
|
let path = self.child_path(parent, name)?;
|
|
|
|
|
let snapshot = self.classifier.snapshot_for(&path);
|
|
|
|
|
let backing = self.create_backing(&snapshot, &path, false)?;
|
|
|
|
|
let file = self.root(backing).open_entry(
|
|
|
|
|
&path,
|
|
|
|
|
open_flags(flags) | OFlags::CREATE,
|
|
|
|
|
Mode::from_raw_mode((mode & !umask) & 0o7777),
|
|
|
|
|
)?;
|
|
|
|
|
let stat = fstat(file.as_fd())?;
|
|
|
|
|
let attributes = self.attributes(&ResolvedEntry {
|
|
|
|
|
path,
|
|
|
|
|
backing,
|
|
|
|
|
stat,
|
|
|
|
|
})?;
|
|
|
|
|
let handle = self.next_handle.fetch_add(1, Ordering::Relaxed);
|
|
|
|
|
self.files
|
|
|
|
|
.lock()
|
|
|
|
|
.unwrap_or_else(std::sync::PoisonError::into_inner)
|
|
|
|
|
.insert(
|
|
|
|
|
handle,
|
|
|
|
|
Arc::new(OpenFile {
|
|
|
|
|
file,
|
|
|
|
|
backing,
|
|
|
|
|
access_mode: flags & libc::O_ACCMODE,
|
|
|
|
|
}),
|
|
|
|
|
);
|
2026-07-18 02:08:14 +09:00
|
|
|
Ok((attributes, handle))
|
2026-07-16 13:03:04 +09:00
|
|
|
})();
|
|
|
|
|
match result {
|
2026-07-18 02:08:14 +09:00
|
|
|
Ok((attributes, handle)) => reply.created(
|
2026-07-16 13:03:04 +09:00
|
|
|
&TTL,
|
|
|
|
|
&attributes,
|
2026-07-18 02:08:14 +09:00
|
|
|
Generation(0),
|
2026-07-16 13:03:04 +09:00
|
|
|
FileHandle(handle),
|
|
|
|
|
FopenFlags::empty(),
|
|
|
|
|
),
|
|
|
|
|
Err(error) => reply.error(errno(&error)),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn fallocate(
|
|
|
|
|
&self,
|
|
|
|
|
_request: &Request,
|
|
|
|
|
_inode: INodeNo,
|
|
|
|
|
handle: FileHandle,
|
|
|
|
|
offset: u64,
|
|
|
|
|
length: u64,
|
|
|
|
|
mode: i32,
|
|
|
|
|
reply: ReplyEmpty,
|
|
|
|
|
) {
|
|
|
|
|
let result = self.with_file(handle, |open| {
|
|
|
|
|
Ok(fallocate(
|
|
|
|
|
open.file.as_fd(),
|
|
|
|
|
FallocateFlags::from_bits_retain(u32::from_ne_bytes(mode.to_ne_bytes())),
|
|
|
|
|
offset,
|
|
|
|
|
length,
|
|
|
|
|
)?)
|
|
|
|
|
});
|
|
|
|
|
reply_result(result, reply);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn lseek(
|
|
|
|
|
&self,
|
|
|
|
|
_request: &Request,
|
|
|
|
|
_inode: INodeNo,
|
|
|
|
|
handle: FileHandle,
|
|
|
|
|
offset: i64,
|
|
|
|
|
whence: i32,
|
|
|
|
|
reply: ReplyLseek,
|
|
|
|
|
) {
|
|
|
|
|
let position = match whence {
|
|
|
|
|
libc::SEEK_SET => u64::try_from(offset).ok().map(SeekFrom::Start),
|
|
|
|
|
libc::SEEK_CUR => Some(SeekFrom::Current(offset)),
|
|
|
|
|
libc::SEEK_END => Some(SeekFrom::End(offset)),
|
|
|
|
|
libc::SEEK_DATA => u64::try_from(offset).ok().map(SeekFrom::Data),
|
|
|
|
|
libc::SEEK_HOLE => u64::try_from(offset).ok().map(SeekFrom::Hole),
|
|
|
|
|
_ => None,
|
|
|
|
|
};
|
|
|
|
|
let result = position
|
|
|
|
|
.ok_or_else(|| io::Error::from_raw_os_error(libc::EINVAL))
|
|
|
|
|
.and_then(|position| {
|
|
|
|
|
self.with_file(handle, |open| Ok(seek(open.file.as_fd(), position)?))
|
|
|
|
|
});
|
|
|
|
|
match result {
|
|
|
|
|
Ok(position) => match i64::try_from(position) {
|
|
|
|
|
Ok(position) => reply.offset(position),
|
|
|
|
|
Err(_) => reply.error(Errno::EOVERFLOW),
|
|
|
|
|
},
|
|
|
|
|
Err(error) => reply.error(errno(&error)),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn copy_file_range(
|
|
|
|
|
&self,
|
|
|
|
|
_request: &Request,
|
|
|
|
|
_inode_in: INodeNo,
|
|
|
|
|
handle_in: FileHandle,
|
|
|
|
|
offset_in: u64,
|
|
|
|
|
_inode_out: INodeNo,
|
|
|
|
|
handle_out: FileHandle,
|
|
|
|
|
offset_out: u64,
|
|
|
|
|
length: u64,
|
|
|
|
|
flags: CopyFileRangeFlags,
|
|
|
|
|
reply: ReplyWrite,
|
|
|
|
|
) {
|
|
|
|
|
if !flags.is_empty() {
|
|
|
|
|
reply.error(Errno::EINVAL);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
let files = self
|
|
|
|
|
.files
|
|
|
|
|
.lock()
|
|
|
|
|
.unwrap_or_else(std::sync::PoisonError::into_inner);
|
|
|
|
|
let result = (|| {
|
|
|
|
|
let input = files
|
|
|
|
|
.get(&handle_in.0)
|
|
|
|
|
.ok_or_else(|| io::Error::from_raw_os_error(libc::EBADF))?;
|
|
|
|
|
let output = files
|
|
|
|
|
.get(&handle_out.0)
|
|
|
|
|
.ok_or_else(|| io::Error::from_raw_os_error(libc::EBADF))?;
|
|
|
|
|
if input.backing != output.backing {
|
|
|
|
|
return Err(io::Error::from_raw_os_error(libc::EXDEV));
|
|
|
|
|
}
|
|
|
|
|
let mut input_offset = offset_in;
|
|
|
|
|
let mut output_offset = offset_out;
|
|
|
|
|
copy_file_range(
|
|
|
|
|
input.file.as_fd(),
|
|
|
|
|
Some(&mut input_offset),
|
|
|
|
|
output.file.as_fd(),
|
|
|
|
|
Some(&mut output_offset),
|
|
|
|
|
usize::try_from(length).unwrap_or(usize::MAX),
|
|
|
|
|
)
|
|
|
|
|
.map_err(io::Error::from)
|
|
|
|
|
})();
|
|
|
|
|
match result {
|
|
|
|
|
Ok(written) => reply.written(u32::try_from(written).unwrap_or(u32::MAX)),
|
|
|
|
|
Err(error) => reply.error(errno(&error)),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl IgnoredWorkspace {
|
|
|
|
|
fn remove(&self, parent: INodeNo, name: &OsStr, directory: bool, reply: ReplyEmpty) {
|
|
|
|
|
let result = (|| {
|
|
|
|
|
let path = self.child_path(parent, name)?;
|
|
|
|
|
let snapshot = self.classifier.snapshot_for(&path);
|
|
|
|
|
let entry = self.resolve(&snapshot, &path)?;
|
|
|
|
|
self.root(entry.backing).unlink(&path, directory)
|
|
|
|
|
})();
|
|
|
|
|
reply_result(result, reply);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn with_temporary_file<T>(
|
|
|
|
|
&self,
|
|
|
|
|
inode: INodeNo,
|
|
|
|
|
operation: impl FnOnce(&File) -> io::Result<T>,
|
|
|
|
|
) -> io::Result<T> {
|
|
|
|
|
let path = self.path_for(inode)?;
|
|
|
|
|
let snapshot = self.classifier.snapshot_for(&path);
|
|
|
|
|
let entry = self.resolve(&snapshot, &path)?;
|
|
|
|
|
let file = self
|
|
|
|
|
.root(entry.backing)
|
|
|
|
|
.open_entry(&path, OFlags::RDONLY, Mode::empty())?;
|
|
|
|
|
operation(&file)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn require_index_publication_safe(
|
|
|
|
|
&self,
|
|
|
|
|
backing: Classification,
|
|
|
|
|
source: &Path,
|
|
|
|
|
snapshot: &ClassificationSnapshot,
|
|
|
|
|
) -> io::Result<()> {
|
|
|
|
|
let bytes = self.root(backing).read_file(source, 512 * 1024 * 1024)?;
|
|
|
|
|
for path in tracked_paths_from_index(&bytes)? {
|
|
|
|
|
let path = PathBuf::from(OsString::from_vec(path));
|
|
|
|
|
if snapshot.is_tracked(&path) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
let host = self.host.stat(&path).ok();
|
|
|
|
|
let isolated = self.isolated.stat(&path).ok();
|
|
|
|
|
if let Some(isolated) = isolated {
|
|
|
|
|
let compatible = host.is_some_and(|host| {
|
|
|
|
|
host.st_mode & libc::S_IFMT == isolated.st_mode & libc::S_IFMT
|
|
|
|
|
});
|
|
|
|
|
if !compatible {
|
|
|
|
|
return Err(io::Error::from_raw_os_error(libc::EBUSY));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
struct ResolvedEntry {
|
|
|
|
|
path: PathBuf,
|
|
|
|
|
backing: Classification,
|
|
|
|
|
stat: rustix::fs::Stat,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
struct OpenFile {
|
|
|
|
|
file: File,
|
|
|
|
|
backing: Classification,
|
|
|
|
|
access_mode: i32,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
|
|
|
|
|
struct PosixLockKey {
|
|
|
|
|
backing: Classification,
|
|
|
|
|
device: u64,
|
|
|
|
|
inode: u64,
|
|
|
|
|
owner: u64,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
struct DirectoryStream {
|
|
|
|
|
directory: File,
|
|
|
|
|
entries: Vec<DirectoryEntry>,
|
|
|
|
|
_snapshot: ClassificationSnapshot,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
struct DirectoryEntry {
|
|
|
|
|
inode: INodeNo,
|
|
|
|
|
kind: FileType,
|
|
|
|
|
name: OsString,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
struct NodeTable {
|
|
|
|
|
paths: HashMap<u64, PathBuf>,
|
|
|
|
|
inodes: HashMap<NodeKey, u64>,
|
|
|
|
|
next_inode: u64,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl NodeTable {
|
|
|
|
|
fn new() -> Self {
|
|
|
|
|
Self {
|
|
|
|
|
paths: HashMap::from([(INodeNo::ROOT.0, PathBuf::new())]),
|
|
|
|
|
inodes: HashMap::new(),
|
|
|
|
|
next_inode: 2,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn path(&self, inode: INodeNo) -> Option<PathBuf> {
|
|
|
|
|
self.paths.get(&inode.0).cloned()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn inode(&mut self, entry: &ResolvedEntry) -> INodeNo {
|
|
|
|
|
let key = NodeKey {
|
|
|
|
|
path: entry.path.clone(),
|
|
|
|
|
backing: entry.backing,
|
|
|
|
|
device: entry.stat.st_dev,
|
|
|
|
|
inode: entry.stat.st_ino,
|
|
|
|
|
};
|
|
|
|
|
if let Some(inode) = self.inodes.get(&key) {
|
|
|
|
|
return INodeNo(*inode);
|
|
|
|
|
}
|
|
|
|
|
let inode = self.next_inode;
|
|
|
|
|
self.next_inode = self.next_inode.saturating_add(1);
|
|
|
|
|
self.paths.insert(inode, entry.path.clone());
|
|
|
|
|
self.inodes.insert(key, inode);
|
|
|
|
|
INodeNo(inode)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
|
|
|
|
|
struct NodeKey {
|
|
|
|
|
path: PathBuf,
|
|
|
|
|
backing: Classification,
|
|
|
|
|
device: u64,
|
|
|
|
|
inode: u64,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn open_flags(flags: i32) -> OFlags {
|
|
|
|
|
let mut result = OFlags::from_bits_retain(u32::from_ne_bytes(flags.to_ne_bytes()));
|
|
|
|
|
result.remove(OFlags::CREATE | OFlags::EXCL | OFlags::NOFOLLOW | OFlags::CLOEXEC);
|
|
|
|
|
result
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn file_type(mode: u32) -> io::Result<FileType> {
|
|
|
|
|
match mode & libc::S_IFMT {
|
|
|
|
|
libc::S_IFREG => Ok(FileType::RegularFile),
|
|
|
|
|
libc::S_IFDIR => Ok(FileType::Directory),
|
|
|
|
|
libc::S_IFLNK => Ok(FileType::Symlink),
|
|
|
|
|
libc::S_IFIFO => Ok(FileType::NamedPipe),
|
|
|
|
|
libc::S_IFSOCK => Ok(FileType::Socket),
|
|
|
|
|
libc::S_IFCHR => Ok(FileType::CharDevice),
|
|
|
|
|
libc::S_IFBLK => Ok(FileType::BlockDevice),
|
|
|
|
|
_ => Err(io::Error::from_raw_os_error(libc::EOPNOTSUPP)),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn system_time(seconds: i64, nanoseconds: u64) -> SystemTime {
|
|
|
|
|
if seconds >= 0 {
|
|
|
|
|
UNIX_EPOCH
|
|
|
|
|
+ Duration::new(
|
|
|
|
|
u64::try_from(seconds).unwrap_or(0),
|
|
|
|
|
u32::try_from(nanoseconds).unwrap_or(999_999_999),
|
|
|
|
|
)
|
|
|
|
|
} else {
|
|
|
|
|
UNIX_EPOCH
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn timestamp(value: Option<TimeOrNow>) -> Timespec {
|
|
|
|
|
match value {
|
|
|
|
|
Some(TimeOrNow::Now) => Timespec {
|
|
|
|
|
tv_sec: 0,
|
|
|
|
|
tv_nsec: UTIME_NOW,
|
|
|
|
|
},
|
|
|
|
|
Some(TimeOrNow::SpecificTime(value)) => {
|
|
|
|
|
let duration = value.duration_since(UNIX_EPOCH).unwrap_or_default();
|
|
|
|
|
Timespec {
|
|
|
|
|
tv_sec: i64::try_from(duration.as_secs()).unwrap_or(i64::MAX),
|
|
|
|
|
tv_nsec: i64::from(duration.subsec_nanos()),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
None => Timespec {
|
|
|
|
|
tv_sec: 0,
|
|
|
|
|
tv_nsec: UTIME_OMIT,
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn writable_xattr(name: &OsStr) -> bool {
|
|
|
|
|
matches!(
|
|
|
|
|
name.as_bytes(),
|
|
|
|
|
b"system.posix_acl_access" | b"system.posix_acl_default"
|
|
|
|
|
) || name.as_bytes().starts_with(b"user.")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn unsupported_lock_flags(flags: LockFlags) -> bool {
|
|
|
|
|
flags.bits() & !LockFlags::FUSE_LK_FLOCK.bits() != 0
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn bsd_lock_operation(typ: i32, sleep: bool) -> io::Result<FlockOperation> {
|
|
|
|
|
match (typ, sleep) {
|
|
|
|
|
(libc::F_RDLCK, true) => Ok(FlockOperation::LockShared),
|
|
|
|
|
(libc::F_RDLCK, false) => Ok(FlockOperation::NonBlockingLockShared),
|
|
|
|
|
(libc::F_WRLCK, true) => Ok(FlockOperation::LockExclusive),
|
|
|
|
|
(libc::F_WRLCK, false) => Ok(FlockOperation::NonBlockingLockExclusive),
|
|
|
|
|
(libc::F_UNLCK, true) => Ok(FlockOperation::Unlock),
|
|
|
|
|
(libc::F_UNLCK, false) => Ok(FlockOperation::NonBlockingUnlock),
|
|
|
|
|
_ => Err(io::Error::from_raw_os_error(libc::EINVAL)),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn record_lock(start: u64, end: u64, typ: i32) -> io::Result<libc::flock> {
|
|
|
|
|
if start > end {
|
|
|
|
|
return Err(io::Error::from_raw_os_error(libc::EINVAL));
|
|
|
|
|
}
|
|
|
|
|
let start_u64 = start;
|
|
|
|
|
let start =
|
|
|
|
|
i64::try_from(start_u64).map_err(|_| io::Error::from_raw_os_error(libc::EOVERFLOW))?;
|
|
|
|
|
let length = if end == u64::MAX {
|
|
|
|
|
0
|
|
|
|
|
} else {
|
|
|
|
|
let length = end
|
|
|
|
|
.checked_sub(start_u64)
|
|
|
|
|
.and_then(|length| length.checked_add(1))
|
|
|
|
|
.ok_or_else(|| io::Error::from_raw_os_error(libc::EOVERFLOW))?;
|
|
|
|
|
i64::try_from(length).map_err(|_| io::Error::from_raw_os_error(libc::EOVERFLOW))?
|
|
|
|
|
};
|
|
|
|
|
let typ = i16::try_from(typ).map_err(|_| io::Error::from_raw_os_error(libc::EINVAL))?;
|
|
|
|
|
let whence =
|
|
|
|
|
i16::try_from(libc::SEEK_SET).map_err(|_| io::Error::from_raw_os_error(libc::EINVAL))?;
|
|
|
|
|
if !matches!(
|
|
|
|
|
i32::from(typ),
|
|
|
|
|
libc::F_RDLCK | libc::F_WRLCK | libc::F_UNLCK
|
|
|
|
|
) {
|
|
|
|
|
return Err(io::Error::from_raw_os_error(libc::EINVAL));
|
|
|
|
|
}
|
|
|
|
|
Ok(libc::flock {
|
|
|
|
|
l_type: typ,
|
|
|
|
|
l_whence: whence,
|
|
|
|
|
l_start: start,
|
|
|
|
|
l_len: length,
|
|
|
|
|
l_pid: 0,
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn fuse_lock(lock: &libc::flock) -> io::Result<(u64, u64, i32, u32)> {
|
|
|
|
|
let start =
|
|
|
|
|
u64::try_from(lock.l_start).map_err(|_| io::Error::from_raw_os_error(libc::EOVERFLOW))?;
|
|
|
|
|
let end = if lock.l_len == 0 {
|
|
|
|
|
u64::MAX
|
|
|
|
|
} else {
|
|
|
|
|
let length =
|
|
|
|
|
u64::try_from(lock.l_len).map_err(|_| io::Error::from_raw_os_error(libc::EOVERFLOW))?;
|
|
|
|
|
start
|
|
|
|
|
.checked_add(length.saturating_sub(1))
|
|
|
|
|
.ok_or_else(|| io::Error::from_raw_os_error(libc::EOVERFLOW))?
|
|
|
|
|
};
|
|
|
|
|
Ok((
|
|
|
|
|
start,
|
|
|
|
|
end,
|
|
|
|
|
i32::from(lock.l_type),
|
|
|
|
|
u32::from_ne_bytes(lock.l_pid.to_ne_bytes()),
|
|
|
|
|
))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn errno(error: &io::Error) -> Errno {
|
|
|
|
|
Errno::from_i32(error.raw_os_error().unwrap_or(libc::EIO))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn reply_result(result: io::Result<()>, reply: ReplyEmpty) {
|
|
|
|
|
match result {
|
|
|
|
|
Ok(()) => reply.ok(),
|
|
|
|
|
Err(error) => reply.error(errno(&error)),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn reply_xattr(result: io::Result<Vec<u8>>, size: u32, reply: ReplyXattr) {
|
|
|
|
|
match result {
|
|
|
|
|
Ok(bytes) if size == 0 => reply.size(u32::try_from(bytes.len()).unwrap_or(u32::MAX)),
|
|
|
|
|
Ok(bytes) if bytes.len() <= size as usize => reply.data(&bytes),
|
|
|
|
|
Ok(_) => reply.error(Errno::ERANGE),
|
|
|
|
|
Err(error) => reply.error(errno(&error)),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[cfg(test)]
|
|
|
|
|
mod tests {
|
|
|
|
|
use std::fs::{self, OpenOptions};
|
|
|
|
|
use std::io::{Read, Seek, Write};
|
|
|
|
|
use std::os::fd::{AsFd, AsRawFd};
|
|
|
|
|
use std::sync::atomic::{AtomicU64, Ordering};
|
|
|
|
|
|
|
|
|
|
use fuser::{Config, LockOwner, MountOption};
|
|
|
|
|
use nix::fcntl::{FcntlArg, fcntl};
|
|
|
|
|
|
|
|
|
|
use super::{Classification, IgnoredWorkspace, OpenFile, fuse_lock, record_lock};
|
|
|
|
|
|
|
|
|
|
static SEQUENCE: AtomicU64 = AtomicU64::new(0);
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn posix_lock_owners_use_distinct_backing_lock_domains() {
|
|
|
|
|
let fixture = Fixture::new();
|
|
|
|
|
repository(&fixture.workspace);
|
|
|
|
|
let path = fixture.workspace.join("ordinary");
|
|
|
|
|
fs::write(&path, b"lock").unwrap();
|
|
|
|
|
let filesystem =
|
|
|
|
|
IgnoredWorkspace::open(&fixture.workspace, &fixture.isolated, None).unwrap();
|
|
|
|
|
let open = OpenFile {
|
|
|
|
|
file: OpenOptions::new()
|
|
|
|
|
.read(true)
|
|
|
|
|
.write(true)
|
|
|
|
|
.open(path)
|
|
|
|
|
.unwrap(),
|
|
|
|
|
backing: Classification::Host,
|
|
|
|
|
access_mode: libc::O_RDWR,
|
|
|
|
|
};
|
|
|
|
|
let first_owner = LockOwner(101);
|
|
|
|
|
let second_owner = LockOwner(202);
|
|
|
|
|
let first = filesystem.posix_lock_file(&open, first_owner).unwrap();
|
|
|
|
|
let second = filesystem.posix_lock_file(&open, second_owner).unwrap();
|
|
|
|
|
assert_ne!(first.as_raw_fd(), second.as_raw_fd());
|
|
|
|
|
|
|
|
|
|
let lock = record_lock(0, 0, libc::F_WRLCK).unwrap();
|
|
|
|
|
fcntl(first.as_fd(), FcntlArg::F_OFD_SETLK(&lock)).unwrap();
|
|
|
|
|
let error = fcntl(second.as_fd(), FcntlArg::F_OFD_SETLK(&lock)).unwrap_err();
|
|
|
|
|
assert_eq!(error, nix::errno::Errno::EAGAIN);
|
|
|
|
|
|
|
|
|
|
drop(first);
|
|
|
|
|
filesystem.release_posix_locks(&open, first_owner).unwrap();
|
|
|
|
|
fcntl(second.as_fd(), FcntlArg::F_OFD_SETLK(&lock)).unwrap();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn record_lock_ranges_round_trip() {
|
|
|
|
|
for (start, end) in [(0, 0), (7, 19), (11, u64::MAX)] {
|
|
|
|
|
let lock = record_lock(start, end, libc::F_RDLCK).unwrap();
|
|
|
|
|
assert_eq!(fuse_lock(&lock).unwrap(), (start, end, libc::F_RDLCK, 0));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-18 02:08:14 +09:00
|
|
|
#[test]
|
|
|
|
|
fn disabled_isolation_routes_ignored_paths_to_the_host_backing() {
|
|
|
|
|
let fixture = Fixture::new();
|
|
|
|
|
repository(&fixture.workspace);
|
|
|
|
|
fs::write(fixture.workspace.join(".gitignore"), b"ignored/\n").unwrap();
|
|
|
|
|
let filesystem = IgnoredWorkspace::from_authorities_with_policy(
|
|
|
|
|
fixture.workspace.clone(),
|
|
|
|
|
fs::File::open(&fixture.workspace).unwrap(),
|
|
|
|
|
fs::File::open(&fixture.isolated).unwrap(),
|
|
|
|
|
None,
|
|
|
|
|
false,
|
|
|
|
|
)
|
|
|
|
|
.unwrap();
|
|
|
|
|
let path = std::path::Path::new("ignored/item");
|
|
|
|
|
let snapshot = filesystem.classifier.snapshot_for(path);
|
|
|
|
|
assert_eq!(
|
|
|
|
|
filesystem.classify(&snapshot, path, false),
|
|
|
|
|
Classification::Host
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-16 13:03:04 +09:00
|
|
|
#[test]
|
|
|
|
|
#[ignore = "requires a usable host FUSE device"]
|
|
|
|
|
fn physical_router_preserves_lock_domains_across_transition() {
|
|
|
|
|
let fixture = Fixture::new();
|
|
|
|
|
repository(&fixture.workspace);
|
|
|
|
|
fs::write(fixture.workspace.join(".gitignore"), b"ignored/\n").unwrap();
|
|
|
|
|
fs::create_dir(fixture.workspace.join("ignored")).unwrap();
|
|
|
|
|
fs::write(fixture.workspace.join("ignored/locked"), b"host").unwrap();
|
|
|
|
|
fs::create_dir(fixture.isolated.join("ignored")).unwrap();
|
|
|
|
|
fs::write(fixture.isolated.join("ignored/locked"), b"isolated").unwrap();
|
|
|
|
|
|
|
|
|
|
let filesystem =
|
|
|
|
|
IgnoredWorkspace::open(&fixture.workspace, &fixture.isolated, None).unwrap();
|
|
|
|
|
let mut config = Config::default();
|
|
|
|
|
config.mount_options.extend([
|
|
|
|
|
MountOption::RW,
|
|
|
|
|
MountOption::DefaultPermissions,
|
|
|
|
|
MountOption::NoDev,
|
|
|
|
|
MountOption::NoSuid,
|
|
|
|
|
]);
|
|
|
|
|
let session = fuser::spawn_mount2(filesystem, &fixture.mountpoint, &config).unwrap();
|
|
|
|
|
|
|
|
|
|
let mounted_path = fixture.mountpoint.join("ignored/locked");
|
|
|
|
|
let old = OpenOptions::new()
|
|
|
|
|
.read(true)
|
|
|
|
|
.write(true)
|
|
|
|
|
.open(&mounted_path)
|
|
|
|
|
.unwrap();
|
|
|
|
|
let lock = record_lock(0, 0, libc::F_WRLCK).unwrap();
|
|
|
|
|
fcntl(old.as_fd(), FcntlArg::F_SETLK(&lock)).unwrap();
|
|
|
|
|
|
|
|
|
|
fs::write(fixture.workspace.join(".gitignore"), b"different/\n").unwrap();
|
|
|
|
|
let new = OpenOptions::new()
|
|
|
|
|
.read(true)
|
|
|
|
|
.write(true)
|
|
|
|
|
.open(&mounted_path)
|
|
|
|
|
.unwrap();
|
|
|
|
|
assert_eq!(fs::read(&mounted_path).unwrap(), b"host");
|
|
|
|
|
|
|
|
|
|
let isolated = OpenOptions::new()
|
|
|
|
|
.read(true)
|
|
|
|
|
.write(true)
|
|
|
|
|
.open(fixture.isolated.join("ignored/locked"))
|
|
|
|
|
.unwrap();
|
|
|
|
|
assert_eq!(
|
|
|
|
|
fcntl(isolated.as_fd(), FcntlArg::F_OFD_SETLK(&lock)).unwrap_err(),
|
|
|
|
|
nix::errno::Errno::EAGAIN
|
|
|
|
|
);
|
|
|
|
|
let host = OpenOptions::new()
|
|
|
|
|
.read(true)
|
|
|
|
|
.write(true)
|
|
|
|
|
.open(fixture.workspace.join("ignored/locked"))
|
|
|
|
|
.unwrap();
|
|
|
|
|
fcntl(host.as_fd(), FcntlArg::F_OFD_SETLK(&lock)).unwrap();
|
|
|
|
|
let unlock = record_lock(0, 0, libc::F_UNLCK).unwrap();
|
|
|
|
|
fcntl(host.as_fd(), FcntlArg::F_OFD_SETLK(&unlock)).unwrap();
|
|
|
|
|
|
|
|
|
|
fcntl(new.as_fd(), FcntlArg::F_SETLK(&lock)).unwrap();
|
|
|
|
|
assert_eq!(
|
|
|
|
|
fcntl(host.as_fd(), FcntlArg::F_OFD_SETLK(&lock)).unwrap_err(),
|
|
|
|
|
nix::errno::Errno::EAGAIN
|
|
|
|
|
);
|
|
|
|
|
drop(old);
|
|
|
|
|
fcntl(isolated.as_fd(), FcntlArg::F_OFD_SETLK(&lock)).unwrap();
|
|
|
|
|
drop(new);
|
|
|
|
|
fcntl(host.as_fd(), FcntlArg::F_OFD_SETLK(&lock)).unwrap();
|
|
|
|
|
|
|
|
|
|
session.umount_and_join().unwrap();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
#[ignore = "requires a usable host FUSE device"]
|
|
|
|
|
fn physical_router_preserves_routes_transitions_and_cross_route_atomicity() {
|
|
|
|
|
let fixture = Fixture::new();
|
|
|
|
|
repository(&fixture.workspace);
|
|
|
|
|
fs::write(fixture.workspace.join(".gitignore"), b"ignored/\n").unwrap();
|
|
|
|
|
fs::create_dir(fixture.workspace.join("ignored")).unwrap();
|
|
|
|
|
fs::write(fixture.workspace.join("ignored/sentinel"), b"host").unwrap();
|
|
|
|
|
fs::create_dir(fixture.isolated.join("ignored")).unwrap();
|
|
|
|
|
fs::write(fixture.isolated.join("ignored/sentinel"), b"isolated").unwrap();
|
|
|
|
|
|
|
|
|
|
let filesystem =
|
|
|
|
|
IgnoredWorkspace::open(&fixture.workspace, &fixture.isolated, None).unwrap();
|
|
|
|
|
let mut config = Config::default();
|
|
|
|
|
config.mount_options.extend([
|
|
|
|
|
MountOption::RW,
|
|
|
|
|
MountOption::DefaultPermissions,
|
|
|
|
|
MountOption::NoDev,
|
|
|
|
|
MountOption::NoSuid,
|
|
|
|
|
]);
|
|
|
|
|
let session = fuser::spawn_mount2(filesystem, &fixture.mountpoint, &config).unwrap();
|
|
|
|
|
|
|
|
|
|
assert_eq!(
|
|
|
|
|
fs::read(fixture.mountpoint.join("ignored/sentinel")).unwrap(),
|
|
|
|
|
b"isolated"
|
|
|
|
|
);
|
|
|
|
|
fs::write(fixture.mountpoint.join("ordinary"), b"bidirectional").unwrap();
|
|
|
|
|
assert_eq!(
|
|
|
|
|
fs::read(fixture.workspace.join("ordinary")).unwrap(),
|
|
|
|
|
b"bidirectional"
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
let mut pinned = OpenOptions::new()
|
|
|
|
|
.read(true)
|
|
|
|
|
.write(true)
|
|
|
|
|
.open(fixture.mountpoint.join("ignored/sentinel"))
|
|
|
|
|
.unwrap();
|
|
|
|
|
fs::write(fixture.workspace.join(".gitignore"), b"different/\n").unwrap();
|
|
|
|
|
assert_eq!(
|
|
|
|
|
fs::read(fixture.mountpoint.join("ignored/sentinel")).unwrap(),
|
|
|
|
|
b"host"
|
|
|
|
|
);
|
|
|
|
|
pinned.write_all(b"-pinned").unwrap();
|
|
|
|
|
pinned.flush().unwrap();
|
|
|
|
|
let mut pinned_bytes = Vec::new();
|
|
|
|
|
pinned.rewind().unwrap();
|
|
|
|
|
pinned.read_to_end(&mut pinned_bytes).unwrap();
|
|
|
|
|
assert_eq!(pinned_bytes, b"-pinnedd");
|
|
|
|
|
assert_eq!(
|
|
|
|
|
fs::read(fixture.workspace.join("ignored/sentinel")).unwrap(),
|
|
|
|
|
b"host"
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
fs::write(fixture.workspace.join("host-source"), b"source").unwrap();
|
|
|
|
|
fs::write(fixture.workspace.join(".gitignore"), b"destination\n").unwrap();
|
|
|
|
|
let error = fs::rename(
|
|
|
|
|
fixture.mountpoint.join("host-source"),
|
|
|
|
|
fixture.mountpoint.join("destination"),
|
|
|
|
|
)
|
|
|
|
|
.unwrap_err();
|
|
|
|
|
assert_eq!(error.raw_os_error(), Some(libc::EXDEV));
|
|
|
|
|
assert_eq!(
|
|
|
|
|
fs::read(fixture.workspace.join("host-source")).unwrap(),
|
|
|
|
|
b"source"
|
|
|
|
|
);
|
|
|
|
|
assert!(!fixture.isolated.join("destination").exists());
|
|
|
|
|
|
|
|
|
|
drop(pinned);
|
|
|
|
|
session.umount_and_join().unwrap();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn repository(root: &std::path::Path) {
|
|
|
|
|
fs::create_dir_all(root.join(".git/info")).unwrap();
|
|
|
|
|
fs::create_dir_all(root.join(".git/hooks")).unwrap();
|
|
|
|
|
fs::write(root.join(".git/config"), b"[core]\nignoreCase = false\n").unwrap();
|
|
|
|
|
fs::write(root.join(".git/info/exclude"), b"").unwrap();
|
|
|
|
|
let mut index = b"DIRC".to_vec();
|
|
|
|
|
index.extend_from_slice(&2u32.to_be_bytes());
|
|
|
|
|
index.extend_from_slice(&0u32.to_be_bytes());
|
|
|
|
|
index.extend_from_slice(&[0; 20]);
|
|
|
|
|
fs::write(root.join(".git/index"), index).unwrap();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
struct Fixture {
|
|
|
|
|
root: std::path::PathBuf,
|
|
|
|
|
workspace: std::path::PathBuf,
|
|
|
|
|
isolated: std::path::PathBuf,
|
|
|
|
|
mountpoint: std::path::PathBuf,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl Fixture {
|
|
|
|
|
fn new() -> Self {
|
|
|
|
|
let sequence = SEQUENCE.fetch_add(1, Ordering::Relaxed);
|
|
|
|
|
let root = std::env::current_dir()
|
|
|
|
|
.unwrap()
|
|
|
|
|
.join("target/ignored-workspace-tests")
|
|
|
|
|
.join(format!("physical-{}-{sequence}", std::process::id()));
|
|
|
|
|
let workspace = root.join("workspace");
|
|
|
|
|
let isolated = root.join("isolated");
|
|
|
|
|
let mountpoint = root.join("mountpoint");
|
|
|
|
|
fs::create_dir_all(&workspace).unwrap();
|
|
|
|
|
fs::create_dir(&isolated).unwrap();
|
|
|
|
|
fs::create_dir(&mountpoint).unwrap();
|
|
|
|
|
Self {
|
|
|
|
|
root,
|
|
|
|
|
workspace,
|
|
|
|
|
isolated,
|
|
|
|
|
mountpoint,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl Drop for Fixture {
|
|
|
|
|
fn drop(&mut self) {
|
|
|
|
|
fs::remove_dir_all(&self.root).unwrap();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|