use std::convert::TryInto; use std::env; use std::ffi::OsStr; use fuse::{ FileAttr, FileType, Filesystem, ReplyAttr, ReplyData, ReplyDirectory, ReplyEntry, Request, }; use libc::ENOENT; use sha1::Sha1; use time::Timespec; const FILES_COUNT: u64 = 1_000; const FILE_SIZE: u64 = 1_048_576; // bytes (1 megabyte) const BLOCK_SIZE: usize = 20; // bytes (the size of SHA-1 digest) const TTL: Timespec = Timespec { sec: 1, nsec: 0 }; const ROOT_DIR_ATTR: FileAttr = FileAttr { ino: 1, size: 0, blocks: 0, atime: UNIX_EPOCH, // 1970-01-01 00:00:00 mtime: UNIX_EPOCH, ctime: UNIX_EPOCH, crtime: UNIX_EPOCH, kind: FileType::Directory, perm: 0o755, nlink: 2, uid: 0, gid: 0, rdev: 0, flags: 0, }; const UNIX_EPOCH: Timespec = Timespec { sec: 0, nsec: 0 }; const FILE_ATTR: FileAttr = FileAttr { ino: 2, size: FILE_SIZE, blocks: 1, atime: UNIX_EPOCH, // 1970-01-01 00:00:00 mtime: UNIX_EPOCH, ctime: UNIX_EPOCH, crtime: UNIX_EPOCH, kind: FileType::RegularFile, perm: 0o644, nlink: 1, uid: 0, gid: 0, rdev: 0, flags: 0, }; struct PlentyFS { /// Initial value of our bespoke RNG. seed: u64, } impl Filesystem for PlentyFS { fn lookup(&mut self, _req: &Request, parent: u64, name: &OsStr, reply: ReplyEntry) { if parent != 1 { reply.error(ENOENT); return; } match name.to_str().and_then(|s| s.parse::().ok()) { Some(file_number) => { if file_number < FILES_COUNT { let file_attr = FileAttr { ino: file_number + 2, ..FILE_ATTR }; reply.entry(&TTL, &file_attr, 0); } else { reply.error(ENOENT); } } None => reply.error(ENOENT), } } fn getattr(&mut self, _req: &Request, ino: u64, reply: ReplyAttr) { if ino == 1 { reply.attr(&TTL, &ROOT_DIR_ATTR); } else if ino >= 2 && ino <= (FILES_COUNT + 1) { let file_attr = FileAttr { ino, ..FILE_ATTR }; reply.attr(&TTL, &file_attr); } else { reply.error(ENOENT); } } fn read( &mut self, _req: &Request, ino: u64, _fh: u64, offset: i64, size: u32, reply: ReplyData, ) { if ino >= 2 && ino <= (FILES_COUNT + 1) { let seed = generate_file_seed(self.seed, ino); let first_block = (offset as u64) / (BLOCK_SIZE as u64); let blocks_count = (size as u64 + 2 * BLOCK_SIZE as u64) / (BLOCK_SIZE as u64); let mut data = vec![]; for block_no in first_block..(first_block + blocks_count) { data.extend(&generate_block_data(seed, block_no)); } let inside_offset = (offset as usize) % (BLOCK_SIZE as usize); reply.data(&data[inside_offset..(inside_offset + size as usize)]); } else { reply.error(ENOENT); } } fn readdir( &mut self, _req: &Request, ino: u64, _fh: u64, mut offset: i64, mut reply: ReplyDirectory, ) { if ino != 1 { reply.error(ENOENT); return; } if offset == 0 { reply.add(1, 1, FileType::Directory, "."); } if offset <= 1 { reply.add(1, 1, FileType::Directory, "."); } if offset >= 2 { offset -= 2; } for file_number in (0..FILES_COUNT).skip(offset as usize) { let inode = file_number + 2; let next_entry_offset = 2 + file_number + 1; reply.add( inode, next_entry_offset as i64, FileType::RegularFile, file_number.to_string(), ); } reply.ok(); } } fn main() { let mountpoint = env::args_os().nth(1).unwrap(); let options = ["-o", "ro", "-o", "fsname=plentyfs"] .iter() .map(|o| o.as_ref()) .collect::>(); // TODO: replace PID by a proper source of entropy. Add an option for the user to set their own // seed for reproducibility. fuse::mount( PlentyFS { seed: std::process::id() as u64, }, &mountpoint, &options, ) .unwrap(); } fn generate_file_seed(root_seed: u64, inode: u64) -> u64 { let file_salt = "filesalt".as_bytes(); let (file_salt, _) = file_salt.split_at(std::mem::size_of::()); // TODO: refactor to make panics impossible. let file_salt: [u8; 8] = file_salt.try_into().unwrap(); let file_salt = u64::from_le_bytes(file_salt); // TODO: replace XOR with a better mixing technique. root_seed ^ inode ^ file_salt } fn generate_block_data(seed: u64, block_no: u64) -> [u8; BLOCK_SIZE] { let mut sha1 = Sha1::from(seed.to_le_bytes()); sha1.update(&block_no.to_le_bytes()); sha1.digest().bytes() }