// SPDX-FileCopyrightText: edef // SPDX-License-Identifier: OSL-3.0 use { fossil::store, lazy_static::lazy_static, libc::{c_int, EINVAL, ENOENT, ENOSYS, EROFS}, log::debug, prost::Message, std::{ io::{self, Read}, 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(_) => 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, } } fn main() { env_logger::init(); let store = fossil::Store::open("fossil.db").unwrap(); let root = memtree::load_root(&store, { let mut stdin = io::stdin(); let mut bytes = Vec::new(); stdin.read_to_end(&mut bytes).unwrap(); store::Directory::decode(&*bytes).unwrap() }); 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()?) } } 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 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 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 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, ) { debug!( "[Not Implemented] rmdir(parent: {:#x?}, name: {:?})", parent, name, ); reply.error(ENOSYS); } 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 open(&mut self, _req: &fuser::Request<'_>, _ino: u64, _flags: i32, reply: fuser::ReplyOpen) { reply.opened(0, 0); } fn read( &mut self, _req: &fuser::Request<'_>, ino: u64, _fh: u64, offset: i64, size: u32, _flags: i32, _lock_owner: Option, reply: fuser::ReplyData, ) { match self.find(ino) { Some(memtree::Node::File(f)) => { let offset = offset as usize; let size = size as usize; let content = self.store.read_blob(f.ident); let mut buffer = content.get(offset..).unwrap_or_default(); if buffer.len() > size { buffer = &buffer[..size]; } reply.data(buffer); } Some(_) => reply.error(EINVAL), None => reply.error(ENOENT), } } 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 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, ) { 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 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 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 removexattr( &mut self, _req: &fuser::Request<'_>, _ino: u64, _name: &std::ffi::OsStr, reply: fuser::ReplyEmpty, ) { reply.error(EROFS); } 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 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 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 bmap( &mut self, _req: &fuser::Request<'_>, _ino: u64, _blocksize: u32, _idx: u64, reply: fuser::ReplyBmap, ) { 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 fallocate( &mut self, _req: &fuser::Request<'_>, _ino: u64, _fh: u64, _offset: i64, _length: i64, _mode: i32, reply: fuser::ReplyEmpty, ) { reply.error(EROFS); } 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); } 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, 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, pb: store::Directory) -> Directory { let mut children = BTreeMap::new(); for store::DirectoryNode { name, r#ref, size: _, } in pb.directories { let bytes = store.read_blob(fossil::digest_from_bytes(&r#ref)); let pb = store::Directory::decode(&*bytes).unwrap(); let child = load_root(store, pb); 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(); } } } }