2026-07-19 05:12:57 +09:00
|
|
|
use super::{
|
|
|
|
|
Classification, ClassificationSnapshot, Duration, Errno, File, FileType, FlockOperation,
|
|
|
|
|
HashMap, INodeNo, IgnoredWorkspace, LockFlags, Mode, OFlags, OsStr, OsStrExt, OsString,
|
|
|
|
|
OsStringExt, Path, PathBuf, ReplyEmpty, ReplyXattr, SystemTime, TimeOrNow, Timespec,
|
|
|
|
|
UNIX_EPOCH, UTIME_NOW, UTIME_OMIT, io, tracked_paths_from_index,
|
|
|
|
|
};
|
|
|
|
|
|
2026-07-19 03:02:44 +09:00
|
|
|
impl IgnoredWorkspace {
|
2026-07-19 05:12:57 +09:00
|
|
|
pub(super) fn remove(&self, parent: INodeNo, name: &OsStr, directory: bool, reply: ReplyEmpty) {
|
2026-07-19 03:02:44 +09:00
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-19 05:12:57 +09:00
|
|
|
pub(super) fn with_temporary_file<T>(
|
2026-07-19 03:02:44 +09:00
|
|
|
&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)
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-19 05:12:57 +09:00
|
|
|
pub(super) fn require_index_publication_safe(
|
2026-07-19 03:02:44 +09:00
|
|
|
&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(())
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-19 05:12:57 +09:00
|
|
|
pub(super) struct ResolvedEntry {
|
|
|
|
|
pub(super) path: PathBuf,
|
|
|
|
|
pub(super) backing: Classification,
|
|
|
|
|
pub(super) stat: rustix::fs::Stat,
|
2026-07-19 03:02:44 +09:00
|
|
|
}
|
|
|
|
|
|
2026-07-19 05:12:57 +09:00
|
|
|
pub(super) struct OpenFile {
|
|
|
|
|
pub(super) file: File,
|
|
|
|
|
pub(super) backing: Classification,
|
|
|
|
|
pub(super) access_mode: i32,
|
2026-07-19 03:02:44 +09:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
|
2026-07-19 05:12:57 +09:00
|
|
|
pub(super) struct PosixLockKey {
|
|
|
|
|
pub(super) backing: Classification,
|
|
|
|
|
pub(super) device: u64,
|
|
|
|
|
pub(super) inode: u64,
|
|
|
|
|
pub(super) owner: u64,
|
2026-07-19 03:02:44 +09:00
|
|
|
}
|
|
|
|
|
|
2026-07-19 05:12:57 +09:00
|
|
|
pub(super) struct DirectoryStream {
|
|
|
|
|
pub(super) directory: File,
|
|
|
|
|
pub(super) entries: Vec<DirectoryEntry>,
|
|
|
|
|
pub(super) _snapshot: ClassificationSnapshot,
|
2026-07-19 03:02:44 +09:00
|
|
|
}
|
|
|
|
|
|
2026-07-19 05:12:57 +09:00
|
|
|
pub(super) struct DirectoryEntry {
|
|
|
|
|
pub(super) inode: INodeNo,
|
|
|
|
|
pub(super) kind: FileType,
|
|
|
|
|
pub(super) name: OsString,
|
2026-07-19 03:02:44 +09:00
|
|
|
}
|
|
|
|
|
|
2026-07-19 05:12:57 +09:00
|
|
|
pub(super) struct NodeTable {
|
2026-07-19 03:02:44 +09:00
|
|
|
paths: HashMap<u64, PathBuf>,
|
|
|
|
|
inodes: HashMap<NodeKey, u64>,
|
|
|
|
|
next_inode: u64,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl NodeTable {
|
2026-07-19 05:12:57 +09:00
|
|
|
pub(super) fn new() -> Self {
|
2026-07-19 03:02:44 +09:00
|
|
|
Self {
|
|
|
|
|
paths: HashMap::from([(INodeNo::ROOT.0, PathBuf::new())]),
|
|
|
|
|
inodes: HashMap::new(),
|
|
|
|
|
next_inode: 2,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-19 05:12:57 +09:00
|
|
|
pub(super) fn path(&self, inode: INodeNo) -> Option<PathBuf> {
|
2026-07-19 03:02:44 +09:00
|
|
|
self.paths.get(&inode.0).cloned()
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-19 05:12:57 +09:00
|
|
|
pub(super) fn inode(&mut self, entry: &ResolvedEntry) -> INodeNo {
|
2026-07-19 03:02:44 +09:00
|
|
|
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,
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-19 05:12:57 +09:00
|
|
|
pub(super) fn open_flags(flags: i32) -> OFlags {
|
2026-07-19 03:02:44 +09:00
|
|
|
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
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-19 05:12:57 +09:00
|
|
|
pub(super) fn file_type(mode: u32) -> io::Result<FileType> {
|
2026-07-19 03:02:44 +09:00
|
|
|
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)),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-19 05:12:57 +09:00
|
|
|
pub(super) fn system_time(seconds: i64, nanoseconds: u64) -> SystemTime {
|
2026-07-19 03:02:44 +09:00
|
|
|
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
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-19 05:12:57 +09:00
|
|
|
pub(super) fn timestamp(value: Option<TimeOrNow>) -> Timespec {
|
2026-07-19 03:02:44 +09:00
|
|
|
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,
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-19 05:12:57 +09:00
|
|
|
pub(super) fn writable_xattr(name: &OsStr) -> bool {
|
2026-07-19 03:02:44 +09:00
|
|
|
matches!(
|
|
|
|
|
name.as_bytes(),
|
|
|
|
|
b"system.posix_acl_access" | b"system.posix_acl_default"
|
|
|
|
|
) || name.as_bytes().starts_with(b"user.")
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-19 05:12:57 +09:00
|
|
|
pub(super) fn unsupported_lock_flags(flags: LockFlags) -> bool {
|
2026-07-19 03:02:44 +09:00
|
|
|
flags.bits() & !LockFlags::FUSE_LK_FLOCK.bits() != 0
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-19 05:12:57 +09:00
|
|
|
pub(super) fn bsd_lock_operation(typ: i32, sleep: bool) -> io::Result<FlockOperation> {
|
2026-07-19 03:02:44 +09:00
|
|
|
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)),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-19 05:12:57 +09:00
|
|
|
pub(super) fn record_lock(start: u64, end: u64, typ: i32) -> io::Result<libc::flock> {
|
2026-07-19 03:02:44 +09:00
|
|
|
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,
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-19 05:12:57 +09:00
|
|
|
pub(super) fn fuse_lock(lock: &libc::flock) -> io::Result<(u64, u64, i32, u32)> {
|
2026-07-19 03:02:44 +09:00
|
|
|
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()),
|
|
|
|
|
))
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-19 05:12:57 +09:00
|
|
|
pub(super) fn errno(error: &io::Error) -> Errno {
|
2026-07-19 03:02:44 +09:00
|
|
|
Errno::from_i32(error.raw_os_error().unwrap_or(libc::EIO))
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-19 05:12:57 +09:00
|
|
|
pub(super) fn reply_result(result: io::Result<()>, reply: ReplyEmpty) {
|
2026-07-19 03:02:44 +09:00
|
|
|
match result {
|
|
|
|
|
Ok(()) => reply.ok(),
|
|
|
|
|
Err(error) => reply.error(errno(&error)),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-19 05:12:57 +09:00
|
|
|
pub(super) fn reply_xattr(result: io::Result<Vec<u8>>, size: u32, reply: ReplyXattr) {
|
2026-07-19 03:02:44 +09:00
|
|
|
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)),
|
|
|
|
|
}
|
|
|
|
|
}
|