// 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, collections::{btree_map, hash_map, BTreeMap, HashMap}, 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, } fn main() { env_logger::init(); let args = Args::parse(); let store = fossil::Store::open(args.store).unwrap(); fuser::mount2( Filesystem::open(store), "mnt", &[fuser::MountOption::DefaultPermissions], ) .unwrap(); } struct Filesystem { store: fossil::Store, roots: BTreeMap, roots_by_ident: HashMap, inode_tail: u64, } impl Filesystem { fn open(store: fossil::Store) -> Filesystem { Filesystem { store, roots: BTreeMap::new(), roots_by_ident: HashMap::new(), inode_tail: 0x1000, } } fn find(&self, ino: u64) -> Option { if ino == 1 { // the mountpoint is special-cased in relevant callers unreachable!(); } let (&root_ino, root) = self.roots.range(..=ino).next_back()?; let index: u32 = ino.checked_sub(root_ino)?.try_into().ok()?; memtree::Node::Directory(root).find(index) } fn lookup_root(&mut self, name: &std::ffi::OsStr) -> Option<(u64, &memtree::Directory)> { let name = name.to_str()?; let ident = fossil::digest_from_str(name).ok()?; Some(match self.roots_by_ident.entry(ident) { hash_map::Entry::Occupied(e) => { let &inode = e.get(); (inode, &self.roots[&inode]) } hash_map::Entry::Vacant(e) => { let root = memtree::load_root(&self.store, ident)?; let inode = self.inode_tail; self.inode_tail += inode + 1 + root.size() as u64; e.insert(inode); let root = match self.roots.entry(inode) { btree_map::Entry::Occupied(_) => unreachable!(), btree_map::Entry::Vacant(e2) => e2.insert(root), }; (inode, root) } }) } 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, ) { match parent { 1 => match self.lookup_root(name) { Some((ino, dir)) => { reply.entry( &Duration::ZERO, &file_attr(ino, memtree::Node::Directory(dir)), 0, ); } None => reply.error(ENOENT), }, _ => { 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(entry) => { let ino = parent + entry.index as u64 + 1; reply.entry(&Duration::ZERO, &file_attr(ino, entry.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) { match ino { 1 => { let node = memtree::Directory::default(); let node = memtree::Node::Directory(&node); reply.attr(&Duration::ZERO, &file_attr(ino, node)); } _ => { 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) .expect("file points at missing blob"); 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, ) { match ino { 1 => { reply.ok(); } _ => { let dir = match self.find(ino) { Some(memtree::Node::Directory(d)) => d, Some(_) => { reply.error(EINVAL); return; } None => { reply.error(ENOENT); return; } }; let children = dir.iter().map(|entry| { let kind = match entry.node() { memtree::Node::Directory(_) => fuser::FileType::Directory, memtree::Node::File(_) => fuser::FileType::RegularFile, memtree::Node::Link { .. } => fuser::FileType::Symlink, }; (ino + entry.index as u64 + 1, kind, entry.name.as_str()) }); let children = [ // XXX: The kernel doesn't actually *care* what inodes we provide, // it just overwrites them with the appropriate ones. We *do* have // to actually include these entries with a nonzero inode, however. (!0, fuser::FileType::Directory, "."), (!0, fuser::FileType::Directory, ".."), ] .into_iter() .chain(children); for (offset, (ino, kind, name)) in children.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)] enum NodeBuf { Directory(Directory), File(FileRef), Link { target: String }, } #[derive(Debug, Clone, Copy)] pub enum Node<'a> { Directory(&'a Directory), File(&'a FileRef), Link { target: &'a str }, } impl NodeBuf { fn as_ref(&self) -> Node { match self { NodeBuf::Directory(d) => Node::Directory(d), NodeBuf::File(f) => Node::File(f), NodeBuf::Link { target } => Node::Link { target }, } } } #[derive(Default)] pub struct Directory { children: Vec, size: u32, } pub struct DirectoryEntry { pub name: String, pub index: u32, node: NodeBuf, } impl DirectoryEntry { /// Get the directory entry's node. #[must_use] pub fn node(&self) -> Node { self.node.as_ref() } } impl Directory { pub fn iter(&self) -> impl Iterator { self.children.iter() } pub fn lookup<'a>(&self, name: &'a str) -> Option<&DirectoryEntry> { let idx = self .children .binary_search_by_key(&name, |e| &e.name) .ok()?; Some(&self.children[idx]) } fn by_max_index(&self, max_index: u32) -> Option<&DirectoryEntry> { let pos = match self.children.binary_search_by_key(&max_index, |e| e.index) { // exact match Ok(n) => n, // if the position is 0, then we're *empty* // if the position is n, then the parent node is at n-1 Err(n) => n.checked_sub(1)?, }; Some(&self.children[pos]) } /// Get the directory's size. #[must_use] pub fn size(&self) -> u32 { self.size } } 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(|e| ((&e.name, e.index), &e.node))) .finish() } } pub fn load_root(store: &fossil::Store, ident: Digest) -> Option { 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, NodeBuf::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, NodeBuf::File(child)); } for store::LinkNode { name, target } in pb.links { children.insert(name, NodeBuf::Link { target }); } Some(Directory::from_children(children)) } impl Directory { fn from_children(children: BTreeMap) -> Directory { let mut size: u32 = 0; let children = children .into_iter() .map(|(name, node)| { let index = size; let node_size = match node { NodeBuf::Directory(Directory { size, .. }) => { size.checked_add(1).expect("overflow") } NodeBuf::File(_) | NodeBuf::Link { .. } => 1, }; size = size.checked_add(node_size).expect("overflow"); DirectoryEntry { name, index, node } }) .collect(); Directory { children, size } } pub fn len(&self) -> usize { self.children.len() } } impl<'a> Node<'a> { pub fn find(self, mut index: u32) -> Option> { let mut root = self; loop { let d = match (index, root) { (0, _) => { break Some(root); } (_, Node::Directory(d)) if index <= d.size => { index -= 1; d } _ => { break None; } }; let child = d.by_max_index(index)?; root = child.node(); index = index.checked_sub(child.index).unwrap(); } } } }