// SPDX-FileCopyrightText: edef // SPDX-License-Identifier: OSL-3.0 use { anyhow::Result, clap::StructOpt, fossil::FileRef, lazy_static::lazy_static, libc::{c_int, EINVAL, ENOENT, ENOSYS, EROFS}, log::debug, std::{ cell::RefCell, io::{self, Read, Seek}, path::PathBuf, time::{Duration, SystemTime, UNIX_EPOCH}, }, }; lazy_static! { static ref EPOCH_PLUS_ONE: SystemTime = UNIX_EPOCH + Duration::from_secs(1); } fn file_attr(ino: u64, node: &memtree::Node) -> fuser::FileAttr { let size = match node { memtree::Node::Directory(d) => d.len() as u64, memtree::Node::File(f) => f.size as u64, memtree::Node::Link { target } => target.len() as u64, }; let blksize = 512; fuser::FileAttr { // Inode number ino, // Size in bytes size, // Size in blocks // TODO(edef): switch to u64::div_ceil blocks: (size + blksize as u64 - 1) / (blksize as u64), // Time of last access atime: *EPOCH_PLUS_ONE, // Time of last modification mtime: *EPOCH_PLUS_ONE, // Time of last change ctime: *EPOCH_PLUS_ONE, // Time of creation (macOS only) crtime: *EPOCH_PLUS_ONE, // Kind of file (directory, file, pipe, etc) kind: match node { memtree::Node::Directory(_) => fuser::FileType::Directory, memtree::Node::File(_) => fuser::FileType::RegularFile, memtree::Node::Link { .. } => fuser::FileType::Symlink, }, // Permissions perm: match node { memtree::Node::Directory(_) | memtree::Node::File(FileRef { executable: true, .. }) => 0o755, _ => 0o644, }, // Number of hard links nlink: 1, // User id uid: 1000, // Group id gid: 100, // Rdev rdev: 0, // Block size blksize, // Flags (macOS only, see chflags(2)) flags: 0, } } #[derive(clap::Parser)] struct Args { #[clap(long, default_value = "fossil.db")] store: PathBuf, #[clap(parse(try_from_str = fossil::digest_from_str))] root: fossil::Digest, } fn main() { env_logger::init(); let args = Args::parse(); let store = fossil::Store::open(args.store).unwrap(); let root = memtree::load_root(&store, args.root); fuser::mount2( Filesystem::open(store, root), "mnt", &[fuser::MountOption::DefaultPermissions], ) .unwrap(); } struct Filesystem { store: fossil::Store, root: memtree::Node, } impl Filesystem { fn open(store: fossil::Store, root: memtree::Directory) -> Filesystem { Filesystem { store, root: memtree::Node::Directory(root), } } fn find(&self, ino: u64) -> Option<&memtree::Node> { self.root.find(ino.checked_sub(1)?.try_into().ok()?) } unsafe fn from_fh<'a>(&'a self, fh: u64) -> *mut Handle<'a> { fh as *mut Handle<'a> } } enum Handle<'a> { File { contents: RefCell> }, _NonExhaustive, } impl fuser::Filesystem for Filesystem { fn init( &mut self, _req: &fuser::Request<'_>, _config: &mut fuser::KernelConfig, ) -> Result<(), c_int> { Ok(()) } fn destroy(&mut self) {} fn lookup( &mut self, _req: &fuser::Request<'_>, parent: u64, name: &std::ffi::OsStr, reply: fuser::ReplyEntry, ) { let dir = match self.find(parent) { Some(memtree::Node::Directory(d)) => d, Some(_) => { reply.error(EINVAL); return; } None => { reply.error(ENOENT); return; } }; let entry = name.to_str().and_then(|name| dir.lookup(name)); match entry { None => reply.error(ENOENT), Some((idx, node)) => { let ino = parent + idx as u64 + 1; reply.entry(&Duration::ZERO, &file_attr(ino, node), 0); } } } fn forget(&mut self, _req: &fuser::Request<'_>, _ino: u64, _nlookup: u64) {} fn getattr(&mut self, _req: &fuser::Request<'_>, ino: u64, reply: fuser::ReplyAttr) { if let Some(node) = self.find(ino) { reply.attr(&Duration::ZERO, &file_attr(ino, node)); } else { reply.error(ENOENT); } } fn readlink(&mut self, _req: &fuser::Request<'_>, ino: u64, reply: fuser::ReplyData) { match self.find(ino) { Some(memtree::Node::Link { target }) => reply.data(target.as_bytes()), Some(_) => reply.error(EINVAL), None => reply.error(ENOENT), } } fn open(&mut self, _req: &fuser::Request<'_>, ino: u64, _flags: i32, reply: fuser::ReplyOpen) { match self.find(ino) { Some(memtree::Node::File(f)) => { let contents = self.store.open_blob(f.ident); let fh = Box::new(Handle::File { contents: RefCell::new(contents), }); reply.opened(Box::into_raw(fh) as u64, 0); } Some(_) => reply.error(EINVAL), None => reply.error(ENOENT), } } fn read( &mut self, _req: &fuser::Request<'_>, _ino: u64, fh: u64, offset: i64, size: u32, _flags: i32, _lock_owner: Option, reply: fuser::ReplyData, ) { let fh = unsafe { &*self.from_fh(fh) }; match fh { Handle::File { contents } => { let mut contents = contents.borrow_mut(); let offset = offset as usize; let size = size as usize; assert_eq!( contents.seek(io::SeekFrom::Start(offset as u64)).unwrap(), offset as u64 ); // NOTE: FUSE read() doesn't actually work like you expect. // If you return a short read, it *will* simply treat that // as the file being short. `contents` is an `io::Read`, // which adheres to the usual contract, so we partially // reimplement `read_exact` here. let mut buffer = vec![0u8; size]; let n = { let mut buffer = &mut buffer[..]; let mut total = 0; loop { match contents.read(buffer).unwrap() { 0 => break, n => { buffer = &mut buffer[n..]; total += n; } } } total }; reply.data(&buffer[..n]); } _ => reply.error(EINVAL), } } fn flush( &mut self, _req: &fuser::Request<'_>, ino: u64, fh: u64, lock_owner: u64, reply: fuser::ReplyEmpty, ) { debug!( "[Not Implemented] flush(ino: {:#x?}, fh: {}, lock_owner: {:?})", ino, fh, lock_owner ); reply.error(ENOSYS); } fn release( &mut self, _req: &fuser::Request<'_>, _ino: u64, fh: u64, _flags: i32, _lock_owner: Option, _flush: bool, reply: fuser::ReplyEmpty, ) { let _ = unsafe { let ptr = fh as *mut Handle; Box::from_raw(ptr) }; reply.ok(); } fn fsync( &mut self, _req: &fuser::Request<'_>, ino: u64, fh: u64, datasync: bool, reply: fuser::ReplyEmpty, ) { debug!( "[Not Implemented] fsync(ino: {:#x?}, fh: {}, datasync: {})", ino, fh, datasync ); reply.error(ENOSYS); } fn opendir( &mut self, _req: &fuser::Request<'_>, _ino: u64, _flags: i32, reply: fuser::ReplyOpen, ) { reply.opened(0, 0); } fn readdir( &mut self, _req: &fuser::Request<'_>, ino: u64, _fh: u64, offset: i64, mut reply: fuser::ReplyDirectory, ) { let dir = match self.find(ino) { Some(memtree::Node::Directory(d)) => d, Some(_) => { reply.error(EINVAL); return; } None => { reply.error(ENOENT); return; } }; let mut children = vec![ (ino, fuser::FileType::Directory, "."), (ino, fuser::FileType::Directory, ".."), ]; for (name, idx, node) in dir.iter() { let kind = match node { memtree::Node::Directory(_) => fuser::FileType::Directory, memtree::Node::File(_) => fuser::FileType::RegularFile, memtree::Node::Link { .. } => fuser::FileType::Symlink, }; children.push((ino + idx as u64 + 1, kind, name)); } for (offset, &(ino, kind, name)) in children.iter().enumerate().skip(offset as usize) { if reply.add(ino, (offset + 1) as i64, kind, name) { break; } } reply.ok(); } fn readdirplus( &mut self, _req: &fuser::Request<'_>, ino: u64, fh: u64, offset: i64, reply: fuser::ReplyDirectoryPlus, ) { debug!( "[Not Implemented] readdirplus(ino: {:#x?}, fh: {}, offset: {})", ino, fh, offset ); reply.error(ENOSYS); } fn releasedir( &mut self, _req: &fuser::Request<'_>, _ino: u64, _fh: u64, _flags: i32, reply: fuser::ReplyEmpty, ) { reply.ok(); } fn fsyncdir( &mut self, _req: &fuser::Request<'_>, ino: u64, fh: u64, datasync: bool, reply: fuser::ReplyEmpty, ) { debug!( "[Not Implemented] fsyncdir(ino: {:#x?}, fh: {}, datasync: {})", ino, fh, datasync ); reply.error(ENOSYS); } fn statfs(&mut self, _req: &fuser::Request<'_>, _ino: u64, reply: fuser::ReplyStatfs) { reply.statfs(0, 0, 0, 0, 0, 512, 255, 0); } fn getxattr( &mut self, _req: &fuser::Request<'_>, _ino: u64, _name: &std::ffi::OsStr, _size: u32, reply: fuser::ReplyXattr, ) { reply.error(ENOSYS); } fn listxattr( &mut self, _req: &fuser::Request<'_>, _ino: u64, _size: u32, reply: fuser::ReplyXattr, ) { reply.error(ENOSYS); } fn access(&mut self, _req: &fuser::Request<'_>, ino: u64, mask: i32, reply: fuser::ReplyEmpty) { debug!("[Not Implemented] access(ino: {:#x?}, mask: {})", ino, mask); reply.error(ENOSYS); } fn getlk( &mut self, _req: &fuser::Request<'_>, _ino: u64, _fh: u64, _lock_owner: u64, _start: u64, _end: u64, _typ: i32, _pid: u32, reply: fuser::ReplyLock, ) { reply.error(ENOSYS); } fn setlk( &mut self, _req: &fuser::Request<'_>, _ino: u64, _fh: u64, _lock_owner: u64, _start: u64, _end: u64, _typ: i32, _pid: u32, _sleep: bool, reply: fuser::ReplyEmpty, ) { reply.error(ENOSYS); } fn ioctl( &mut self, _req: &fuser::Request<'_>, _ino: u64, _fh: u64, _flags: u32, _cmd: u32, _in_data: &[u8], _out_size: u32, reply: fuser::ReplyIoctl, ) { reply.error(ENOSYS); } fn lseek( &mut self, _req: &fuser::Request<'_>, ino: u64, fh: u64, offset: i64, whence: i32, reply: fuser::ReplyLseek, ) { debug!( "[Not Implemented] lseek(ino: {:#x?}, fh: {}, offset: {}, whence: {})", ino, fh, offset, whence ); reply.error(ENOSYS); } // read-write methods fn setattr( &mut self, _req: &fuser::Request<'_>, _ino: u64, _mode: Option, _uid: Option, _gid: Option, _size: Option, _atime: Option, _mtime: Option, _ctime: Option, _fh: Option, _crtime: Option, _chgtime: Option, _bkuptime: Option, _flags: Option, reply: fuser::ReplyAttr, ) { reply.error(EROFS); } fn mknod( &mut self, _req: &fuser::Request<'_>, _parent: u64, _name: &std::ffi::OsStr, _mode: u32, _umask: u32, _rdev: u32, reply: fuser::ReplyEntry, ) { reply.error(EROFS); } fn mkdir( &mut self, _req: &fuser::Request<'_>, _parent: u64, _name: &std::ffi::OsStr, _mode: u32, _umask: u32, reply: fuser::ReplyEntry, ) { reply.error(EROFS); } fn unlink( &mut self, _req: &fuser::Request<'_>, _parent: u64, _name: &std::ffi::OsStr, reply: fuser::ReplyEmpty, ) { reply.error(EROFS); } fn rmdir( &mut self, _req: &fuser::Request<'_>, _parent: u64, _name: &std::ffi::OsStr, reply: fuser::ReplyEmpty, ) { reply.error(EROFS); } fn symlink( &mut self, _req: &fuser::Request<'_>, _parent: u64, _name: &std::ffi::OsStr, _link: &std::path::Path, reply: fuser::ReplyEntry, ) { reply.error(EROFS); } fn rename( &mut self, _req: &fuser::Request<'_>, _parent: u64, _name: &std::ffi::OsStr, _newparent: u64, _newname: &std::ffi::OsStr, _flags: u32, reply: fuser::ReplyEmpty, ) { reply.error(EROFS); } fn link( &mut self, _req: &fuser::Request<'_>, _ino: u64, _newparent: u64, _newname: &std::ffi::OsStr, reply: fuser::ReplyEntry, ) { reply.error(EROFS); } fn write( &mut self, _req: &fuser::Request<'_>, _ino: u64, _fh: u64, _offset: i64, _data: &[u8], _write_flags: u32, _flags: i32, _lock_owner: Option, reply: fuser::ReplyWrite, ) { reply.error(EROFS); } fn setxattr( &mut self, _req: &fuser::Request<'_>, _ino: u64, _name: &std::ffi::OsStr, _value: &[u8], _flags: i32, _position: u32, reply: fuser::ReplyEmpty, ) { reply.error(EROFS); } fn removexattr( &mut self, _req: &fuser::Request<'_>, _ino: u64, _name: &std::ffi::OsStr, reply: fuser::ReplyEmpty, ) { reply.error(EROFS); } fn create( &mut self, _req: &fuser::Request<'_>, _parent: u64, _name: &std::ffi::OsStr, _mode: u32, _umask: u32, _flags: i32, reply: fuser::ReplyCreate, ) { reply.error(EROFS); } fn fallocate( &mut self, _req: &fuser::Request<'_>, _ino: u64, _fh: u64, _offset: i64, _length: i64, _mode: i32, reply: fuser::ReplyEmpty, ) { reply.error(EROFS); } fn copy_file_range( &mut self, _req: &fuser::Request<'_>, _ino_in: u64, _fh_in: u64, _offset_in: i64, _ino_out: u64, _fh_out: u64, _offset_out: i64, _len: u64, _flags: u32, reply: fuser::ReplyWrite, ) { reply.error(EROFS); } } mod memtree { pub use fossil::FileRef; use { fossil::{store, Digest}, prost::Message, std::{collections::BTreeMap, fmt}, }; #[derive(Debug)] pub enum Node { Directory(Directory), File(FileRef), Link { target: String }, } #[derive(Default)] pub struct Directory { by_index: BTreeMap, by_name: BTreeMap, size: u32, } impl Directory { pub fn iter(&self) -> impl Iterator { let by_name = self.by_name.iter(); let by_index = self.by_index.iter(); by_name.zip(by_index).map(|((name, &idx), (&idy, node))| { assert_eq!(idx, idy); (name.as_str(), idx, node) }) } pub fn lookup(&self, name: &str) -> Option<(u32, &Node)> { let &idx = self.by_name.get(name)?; Some((idx, &self.by_index[&idx])) } } impl fmt::Debug for Directory { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_struct("Directory") .field("size", &self.size) .field("entries", &DirectoryMembers(self)) .finish() } } struct DirectoryMembers<'a>(&'a Directory); impl fmt::Debug for DirectoryMembers<'_> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_map() .entries(self.0.iter().map(|(name, idx, node)| ((name, idx), node))) .finish() } } pub fn load_root(store: &fossil::Store, ident: Digest) -> Directory { let pb = { let bytes = store.read_blob(ident); store::Directory::decode(&*bytes).unwrap() }; let mut children = BTreeMap::new(); for store::DirectoryNode { name, r#ref, size: _, } in pb.directories { let child = load_root(store, fossil::digest_from_bytes(&r#ref)); children.insert(name, Node::Directory(child)); } for store::FileNode { name, r#ref, executable, size: child_size, } in pb.files { let child = fossil::FileRef { ident: fossil::digest_from_bytes(&r#ref), executable, size: child_size, }; children.insert(name, Node::File(child)); } for store::LinkNode { name, target } in pb.links { children.insert(name, Node::Link { target }); } Directory::from_children(children) } impl Directory { fn from_children(children: BTreeMap) -> Directory { let mut d = Directory::default(); for (name, child) in children { let index = d.size; let size = match child { Node::Directory(Directory { size, .. }) => { size.checked_add(1).expect("overflow") } Node::File(_) | Node::Link { .. } => 1, }; d.size = index.checked_add(size).expect("overflow"); d.by_name.insert(name, index); d.by_index.insert(index, child); } d } pub fn len(&self) -> usize { let len = self.by_index.len(); let lem = self.by_name.len(); debug_assert_eq!(len, lem); len } } impl Node { pub fn find(&self, mut index: u32) -> Option<&Node> { let mut root = self; loop { let d = match (index, root) { (0, _) => { break Some(root); } (_, Node::Directory(d)) => { index -= 1; d } _ => { break None; } }; let (&child_index, child) = d.by_index.range(..=index).next_back()?; root = child; index = index.checked_sub(child_index).unwrap(); } } } }