use super::nodes::{ DirectoryStream, OpenFile, ResolvedEntry, bsd_lock_operation, errno, fuse_lock, record_lock, reply_result, reply_xattr, timestamp, unsupported_lock_flags, writable_xattr, }; use super::{ Arc, AsFd, BsdFileFlags, CopyFileRangeFlags, DirectoryEntry, Errno, FallocateFlags, FcntlArg, FileAttr, FileExt, FileHandle, FileType, Filesystem, FopenFlags, FuseRenameFlags, Generation, Gid, INodeNo, IgnoredWorkspace, InitFlags, KernelConfig, LockFlags, LockOwner, Mode, OFlags, OpenFlags, Ordering, OsStr, OsString, Path, RenameFlags, ReplyAttr, ReplyCreate, ReplyData, ReplyDirectory, ReplyEmpty, ReplyEntry, ReplyLock, ReplyLseek, ReplyOpen, ReplyStatfs, ReplyWrite, ReplyXattr, Request, SeekFrom, SystemTime, TTL, TimeOrNow, Timestamps, Uid, WriteFlags, XattrFlags, bsd_flock, copy_file_range, fallocate, fchmod, fchown, fcntl, fgetxattr, flistxattr, fremovexattr, fsetxattr, fstat, fstatvfs, ftruncate, futimens, io, mknodat, open_flags, seek, }; 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, 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, uid: Option, gid: Option, size: Option, atime: Option, mtime: Option, _ctime: Option, handle: Option, _crtime: Option, _chgtime: Option, _bkuptime: Option, _flags: Option, reply: ReplyAttr, ) { let operation = |open: &OpenFile| -> io::Result { 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)?; let destination_backing = self.classify( &snapshot, &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)?; let destination_backing = self.classify(&snapshot, &destination, false); 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, 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, 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, _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, }), ); Ok((attributes, handle)) })(); match result { Ok((attributes, handle)) => reply.created( &TTL, &attributes, Generation(0), 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)), } } }