use std::collections::HashMap;
use std::sync::{Arc, RwLock};
use std::time::{Duration, SystemTime, UNIX_EPOCH};
#[cfg(feature = "fuse")]
use std::ffi::OsStr;
#[cfg(feature = "fuse")]
use std::path::Path;
pub type Ino = u64;
pub const ROOT_INO: Ino = 1;
#[derive(Clone, Debug)]
pub struct FileAttr {
pub ino: Ino,
pub size: u64,
pub blocks: u64,
pub atime: SystemTime,
pub mtime: SystemTime,
pub ctime: SystemTime,
pub crtime: SystemTime,
pub kind: FileKind,
pub perm: u16,
pub nlink: u32,
pub uid: u32,
pub gid: u32,
pub rdev: u32,
pub blksize: u32,
pub flags: u32,
}
impl Default for FileAttr {
fn default() -> Self {
let now = SystemTime::now();
FileAttr {
ino: 0,
size: 0,
blocks: 0,
atime: now,
mtime: now,
ctime: now,
crtime: now,
kind: FileKind::RegularFile,
perm: 0o644,
nlink: 1,
uid: unsafe { libc::getuid() },
gid: unsafe { libc::getgid() },
rdev: 0,
blksize: 4096,
flags: 0,
}
}
}
#[cfg(feature = "fuse")]
impl From<FileAttr> for fuser::FileAttr {
fn from(attr: FileAttr) -> Self {
fuser::FileAttr {
ino: attr.ino,
size: attr.size,
blocks: attr.blocks,
atime: attr.atime,
mtime: attr.mtime,
ctime: attr.ctime,
crtime: attr.crtime,
kind: attr.kind.into(),
perm: attr.perm,
nlink: attr.nlink,
uid: attr.uid,
gid: attr.gid,
rdev: attr.rdev,
blksize: attr.blksize,
flags: attr.flags,
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
pub enum FileKind {
Directory,
#[default]
RegularFile,
Symlink,
Hardlink,
CharDevice,
BlockDevice,
Fifo,
Socket,
}
#[cfg(feature = "fuse")]
impl From<FileKind> for fuser::FileType {
fn from(kind: FileKind) -> Self {
match kind {
FileKind::Directory => fuser::FileType::Directory,
FileKind::RegularFile => fuser::FileType::RegularFile,
FileKind::Symlink => fuser::FileType::Symlink,
FileKind::Hardlink => fuser::FileType::RegularFile, FileKind::CharDevice => fuser::FileType::CharDevice,
FileKind::BlockDevice => fuser::FileType::BlockDevice,
FileKind::Fifo => fuser::FileType::NamedPipe,
FileKind::Socket => fuser::FileType::Socket,
}
}
}
#[derive(Clone, Debug)]
pub struct DirEntry {
pub ino: Ino,
pub name: String,
pub kind: FileKind,
}
#[derive(Clone, Debug)]
pub struct SymlinkEntry {
pub target: String,
}
#[derive(Clone)]
pub struct CachedFile {
pub data: Vec<u8>,
pub attr: FileAttr,
}
#[derive(Clone, Debug)]
pub struct DeviceNode {
pub major: u32,
pub minor: u32,
}
pub struct EngramFS {
inodes: Arc<RwLock<HashMap<Ino, FileAttr>>>,
inode_paths: Arc<RwLock<HashMap<Ino, String>>>,
path_inodes: Arc<RwLock<HashMap<String, Ino>>>,
directories: Arc<RwLock<HashMap<Ino, Vec<DirEntry>>>>,
file_cache: Arc<RwLock<HashMap<Ino, CachedFile>>>,
symlinks: Arc<RwLock<HashMap<Ino, String>>>,
devices: Arc<RwLock<HashMap<Ino, DeviceNode>>>,
next_ino: Arc<RwLock<Ino>>,
read_only: bool,
attr_ttl: Duration,
entry_ttl: Duration,
}
impl EngramFS {
pub fn new(read_only: bool) -> Self {
let mut fs = EngramFS {
inodes: Arc::new(RwLock::new(HashMap::new())),
inode_paths: Arc::new(RwLock::new(HashMap::new())),
path_inodes: Arc::new(RwLock::new(HashMap::new())),
directories: Arc::new(RwLock::new(HashMap::new())),
file_cache: Arc::new(RwLock::new(HashMap::new())),
symlinks: Arc::new(RwLock::new(HashMap::new())),
devices: Arc::new(RwLock::new(HashMap::new())),
next_ino: Arc::new(RwLock::new(2)), read_only,
attr_ttl: Duration::from_secs(1),
entry_ttl: Duration::from_secs(1),
};
fs.init_root();
fs
}
fn init_root(&mut self) {
let root_attr = FileAttr {
ino: ROOT_INO,
size: 0,
blocks: 0,
kind: FileKind::Directory,
perm: 0o755,
nlink: 2,
..Default::default()
};
self.inodes
.write()
.expect("Lock poisoned during init")
.insert(ROOT_INO, root_attr);
self.inode_paths
.write()
.expect("Lock poisoned during init")
.insert(ROOT_INO, "/".to_string());
self.path_inodes
.write()
.expect("Lock poisoned during init")
.insert("/".to_string(), ROOT_INO);
self.directories
.write()
.expect("Lock poisoned during init")
.insert(ROOT_INO, Vec::new());
}
fn alloc_ino(&self) -> Result<Ino, &'static str> {
let mut next = self
.next_ino
.write()
.map_err(|_| "Inode allocator lock poisoned")?;
let ino = *next;
*next += 1;
Ok(ino)
}
pub fn add_file(&self, path: &str, data: Vec<u8>) -> Result<Ino, &'static str> {
let path = normalize_path(path);
let path_inodes = self.path_inodes.read().unwrap_or_else(|poisoned| {
eprintln!("WARNING: path_inodes lock poisoned, recovering...");
poisoned.into_inner()
});
if path_inodes.contains_key(&path) {
return Err("File already exists");
}
drop(path_inodes);
let parent_path = parent_path(&path).ok_or("Invalid path")?;
let parent_ino = self.ensure_directory(&parent_path)?;
let ino = self.alloc_ino()?;
let size = data.len() as u64;
let attr = FileAttr {
ino,
size,
blocks: size.div_ceil(512),
kind: FileKind::RegularFile,
perm: 0o644,
nlink: 1,
..Default::default()
};
self.inodes
.write()
.map_err(|_| "Inodes lock poisoned")?
.insert(ino, attr.clone());
self.inode_paths
.write()
.map_err(|_| "Inode paths lock poisoned")?
.insert(ino, path.clone());
self.path_inodes
.write()
.map_err(|_| "Path inodes lock poisoned")?
.insert(path.clone(), ino);
self.file_cache
.write()
.map_err(|_| "File cache lock poisoned")?
.insert(ino, CachedFile { data, attr });
let filename = filename(&path).ok_or("Invalid filename")?;
self.directories
.write()
.map_err(|_| "Directories lock poisoned")?
.get_mut(&parent_ino)
.ok_or("Parent directory not found")?
.push(DirEntry {
ino,
name: filename.to_string(),
kind: FileKind::RegularFile,
});
Ok(ino)
}
fn ensure_directory(&self, path: &str) -> Result<Ino, &'static str> {
let path = normalize_path(path);
if path == "/" {
return Ok(ROOT_INO);
}
let path_inodes = self.path_inodes.read().unwrap_or_else(|poisoned| {
eprintln!("WARNING: path_inodes lock poisoned in ensure_directory, recovering...");
poisoned.into_inner()
});
if let Some(&ino) = path_inodes.get(&path) {
return Ok(ino);
}
drop(path_inodes);
let parent_path = parent_path(&path).ok_or("Invalid path")?;
let parent_ino = self.ensure_directory(&parent_path)?;
let ino = self.alloc_ino()?;
let attr = FileAttr {
ino,
size: 0,
blocks: 0,
kind: FileKind::Directory,
perm: 0o755,
nlink: 2,
..Default::default()
};
self.inodes
.write()
.map_err(|_| "Inodes lock poisoned")?
.insert(ino, attr);
self.inode_paths
.write()
.map_err(|_| "Inode paths lock poisoned")?
.insert(ino, path.clone());
self.path_inodes
.write()
.map_err(|_| "Path inodes lock poisoned")?
.insert(path.clone(), ino);
self.directories
.write()
.map_err(|_| "Directories lock poisoned")?
.insert(ino, Vec::new());
let dirname = filename(&path).ok_or("Invalid dirname")?;
self.directories
.write()
.map_err(|_| "Directories lock poisoned")?
.get_mut(&parent_ino)
.ok_or("Parent not found")?
.push(DirEntry {
ino,
name: dirname.to_string(),
kind: FileKind::Directory,
});
if let Some(parent_attr) = self
.inodes
.write()
.map_err(|_| "Inodes lock poisoned")?
.get_mut(&parent_ino)
{
parent_attr.nlink += 1;
}
Ok(ino)
}
pub fn lookup_path(&self, path: &str) -> Option<Ino> {
let path = normalize_path(path);
let path_inodes = self.path_inodes.read().unwrap_or_else(|poisoned| {
eprintln!("WARNING: path_inodes lock poisoned in lookup_path, recovering...");
poisoned.into_inner()
});
path_inodes.get(&path).copied()
}
pub fn get_attr(&self, ino: Ino) -> Option<FileAttr> {
let inodes = self.inodes.read().unwrap_or_else(|poisoned| {
eprintln!("WARNING: inodes lock poisoned in get_attr, recovering...");
poisoned.into_inner()
});
inodes.get(&ino).cloned()
}
pub fn read_data(&self, ino: Ino, offset: u64, size: u32) -> Option<Vec<u8>> {
let cache = self.file_cache.read().unwrap_or_else(|poisoned| {
eprintln!("WARNING: file_cache lock poisoned in read_data, recovering...");
poisoned.into_inner()
});
let cached = cache.get(&ino)?;
let start = offset as usize;
let end = std::cmp::min(start + size as usize, cached.data.len());
if start >= cached.data.len() {
return Some(Vec::new());
}
Some(cached.data[start..end].to_vec())
}
pub fn read_dir(&self, ino: Ino) -> Option<Vec<DirEntry>> {
let directories = self.directories.read().unwrap_or_else(|poisoned| {
eprintln!("WARNING: directories lock poisoned in read_dir, recovering...");
poisoned.into_inner()
});
directories.get(&ino).cloned()
}
pub fn lookup_entry(&self, parent_ino: Ino, name: &str) -> Option<Ino> {
let dirs = self.directories.read().unwrap_or_else(|poisoned| {
eprintln!("WARNING: directories lock poisoned in lookup_entry, recovering...");
poisoned.into_inner()
});
let entries = dirs.get(&parent_ino)?;
entries.iter().find(|e| e.name == name).map(|e| e.ino)
}
pub fn get_parent(&self, ino: Ino) -> Option<Ino> {
if ino == ROOT_INO {
return Some(ROOT_INO); }
let paths = self.inode_paths.read().unwrap_or_else(|poisoned| {
eprintln!("WARNING: inode_paths lock poisoned in get_parent, recovering...");
poisoned.into_inner()
});
let path = paths.get(&ino)?;
let parent = parent_path(path)?;
let path_inodes = self.path_inodes.read().unwrap_or_else(|poisoned| {
eprintln!("WARNING: path_inodes lock poisoned in get_parent, recovering...");
poisoned.into_inner()
});
path_inodes.get(&parent).copied()
}
pub fn file_count(&self) -> usize {
let cache = self.file_cache.read().unwrap_or_else(|poisoned| {
eprintln!("WARNING: file_cache lock poisoned in file_count, recovering...");
poisoned.into_inner()
});
cache.len()
}
pub fn total_size(&self) -> u64 {
let cache = self.file_cache.read().unwrap_or_else(|poisoned| {
eprintln!("WARNING: file_cache lock poisoned in total_size, recovering...");
poisoned.into_inner()
});
cache.values().map(|f| f.attr.size).sum()
}
pub fn is_read_only(&self) -> bool {
self.read_only
}
pub fn attr_ttl(&self) -> Duration {
self.attr_ttl
}
pub fn entry_ttl(&self) -> Duration {
self.entry_ttl
}
pub fn add_symlink(&self, path: &str, target: String) -> Result<Ino, &'static str> {
let path = normalize_path(path);
let path_inodes = self.path_inodes.read().unwrap_or_else(|poisoned| {
eprintln!("WARNING: path_inodes lock poisoned in add_symlink, recovering...");
poisoned.into_inner()
});
if path_inodes.contains_key(&path) {
return Err("Symlink already exists");
}
drop(path_inodes);
let parent_path = parent_path(&path).ok_or("Invalid path")?;
let parent_ino = self.ensure_directory(&parent_path)?;
let ino = self.alloc_ino()?;
let size = target.len() as u64;
let attr = FileAttr {
ino,
size,
blocks: 0,
kind: FileKind::Symlink,
perm: 0o777, nlink: 1,
..Default::default()
};
self.inodes
.write()
.map_err(|_| "Inodes lock poisoned")?
.insert(ino, attr);
self.inode_paths
.write()
.map_err(|_| "Inode paths lock poisoned")?
.insert(ino, path.clone());
self.path_inodes
.write()
.map_err(|_| "Path inodes lock poisoned")?
.insert(path.clone(), ino);
self.symlinks
.write()
.map_err(|_| "Symlinks lock poisoned")?
.insert(ino, target);
let filename = filename(&path).ok_or("Invalid filename")?;
self.directories
.write()
.map_err(|_| "Directories lock poisoned")?
.get_mut(&parent_ino)
.ok_or("Parent directory not found")?
.push(DirEntry {
ino,
name: filename.to_string(),
kind: FileKind::Symlink,
});
Ok(ino)
}
pub fn add_device(
&self,
path: &str,
is_char: bool,
major: u32,
minor: u32,
data: Vec<u8>,
) -> Result<Ino, &'static str> {
let path = normalize_path(path);
let path_inodes = self.path_inodes.read().unwrap_or_else(|poisoned| {
eprintln!("WARNING: path_inodes lock poisoned in add_device, recovering...");
poisoned.into_inner()
});
if path_inodes.contains_key(&path) {
return Err("Device already exists");
}
drop(path_inodes);
let parent_path = parent_path(&path).ok_or("Invalid path")?;
let parent_ino = self.ensure_directory(&parent_path)?;
let ino = self.alloc_ino()?;
let size = data.len() as u64;
let kind = if is_char {
FileKind::CharDevice
} else {
FileKind::BlockDevice
};
let attr = FileAttr {
ino,
size,
blocks: size.div_ceil(512),
kind,
perm: 0o666,
nlink: 1,
rdev: (major << 8) | minor, ..Default::default()
};
self.inodes
.write()
.map_err(|_| "Inodes lock poisoned")?
.insert(ino, attr.clone());
self.inode_paths
.write()
.map_err(|_| "Inode paths lock poisoned")?
.insert(ino, path.clone());
self.path_inodes
.write()
.map_err(|_| "Path inodes lock poisoned")?
.insert(path.clone(), ino);
self.devices
.write()
.map_err(|_| "Devices lock poisoned")?
.insert(ino, DeviceNode { major, minor });
self.file_cache
.write()
.map_err(|_| "File cache lock poisoned")?
.insert(ino, CachedFile { data, attr });
let filename = filename(&path).ok_or("Invalid filename")?;
self.directories
.write()
.map_err(|_| "Directories lock poisoned")?
.get_mut(&parent_ino)
.ok_or("Parent directory not found")?
.push(DirEntry {
ino,
name: filename.to_string(),
kind,
});
Ok(ino)
}
pub fn add_fifo(&self, path: &str) -> Result<Ino, &'static str> {
self.add_special_file(path, FileKind::Fifo)
}
pub fn add_socket(&self, path: &str) -> Result<Ino, &'static str> {
self.add_special_file(path, FileKind::Socket)
}
fn add_special_file(&self, path: &str, kind: FileKind) -> Result<Ino, &'static str> {
let path = normalize_path(path);
let path_inodes = self.path_inodes.read().unwrap_or_else(|poisoned| {
eprintln!("WARNING: path_inodes lock poisoned in add_special_file, recovering...");
poisoned.into_inner()
});
if path_inodes.contains_key(&path) {
return Err("Special file already exists");
}
drop(path_inodes);
let parent_path = parent_path(&path).ok_or("Invalid path")?;
let parent_ino = self.ensure_directory(&parent_path)?;
let ino = self.alloc_ino()?;
let attr = FileAttr {
ino,
size: 0,
blocks: 0,
kind,
perm: 0o666,
nlink: 1,
..Default::default()
};
self.inodes
.write()
.map_err(|_| "Inodes lock poisoned")?
.insert(ino, attr);
self.inode_paths
.write()
.map_err(|_| "Inode paths lock poisoned")?
.insert(ino, path.clone());
self.path_inodes
.write()
.map_err(|_| "Path inodes lock poisoned")?
.insert(path.clone(), ino);
let filename = filename(&path).ok_or("Invalid filename")?;
self.directories
.write()
.map_err(|_| "Directories lock poisoned")?
.get_mut(&parent_ino)
.ok_or("Parent directory not found")?
.push(DirEntry {
ino,
name: filename.to_string(),
kind,
});
Ok(ino)
}
pub fn read_symlink(&self, ino: Ino) -> Option<String> {
let symlinks = self.symlinks.read().unwrap_or_else(|poisoned| {
eprintln!("WARNING: symlinks lock poisoned in read_symlink, recovering...");
poisoned.into_inner()
});
symlinks.get(&ino).cloned()
}
pub fn get_device(&self, ino: Ino) -> Option<DeviceNode> {
let devices = self.devices.read().unwrap_or_else(|poisoned| {
eprintln!("WARNING: devices lock poisoned in get_device, recovering...");
poisoned.into_inner()
});
devices.get(&ino).cloned()
}
pub fn symlink_count(&self) -> usize {
let symlinks = self.symlinks.read().unwrap_or_else(|poisoned| {
eprintln!("WARNING: symlinks lock poisoned in symlink_count, recovering...");
poisoned.into_inner()
});
symlinks.len()
}
pub fn device_count(&self) -> usize {
let devices = self.devices.read().unwrap_or_else(|poisoned| {
eprintln!("WARNING: devices lock poisoned in device_count, recovering...");
poisoned.into_inner()
});
devices.len()
}
}
#[cfg(feature = "fuse")]
impl fuser::Filesystem for EngramFS {
fn init(
&mut self,
_req: &fuser::Request<'_>,
_config: &mut fuser::KernelConfig,
) -> Result<(), libc::c_int> {
eprintln!(
"EngramFS initialized: {} files, {} bytes total",
self.file_count(),
self.total_size()
);
Ok(())
}
fn destroy(&mut self) {
eprintln!("EngramFS unmounted");
}
fn lookup(
&mut self,
_req: &fuser::Request<'_>,
parent: u64,
name: &OsStr,
reply: fuser::ReplyEntry,
) {
let name = match name.to_str() {
Some(n) => n,
None => {
reply.error(libc::ENOENT);
return;
}
};
match self.lookup_entry(parent, name) {
Some(ino) => {
if let Some(attr) = self.get_attr(ino) {
let fuser_attr: fuser::FileAttr = attr.into();
reply.entry(&self.entry_ttl, &fuser_attr, 0);
} else {
reply.error(libc::ENOENT);
}
}
None => {
reply.error(libc::ENOENT);
}
}
}
fn getattr(
&mut self,
_req: &fuser::Request<'_>,
ino: u64,
_fh: Option<u64>,
reply: fuser::ReplyAttr,
) {
match self.get_attr(ino) {
Some(attr) => {
let fuser_attr: fuser::FileAttr = attr.into();
reply.attr(&self.attr_ttl, &fuser_attr);
}
None => {
reply.error(libc::ENOENT);
}
}
}
fn read(
&mut self,
_req: &fuser::Request<'_>,
ino: u64,
_fh: u64,
offset: i64,
size: u32,
_flags: i32,
_lock_owner: Option<u64>,
reply: fuser::ReplyData,
) {
match self.read_data(ino, offset as u64, size) {
Some(data) => {
reply.data(&data);
}
None => {
reply.error(libc::ENOENT);
}
}
}
fn open(&mut self, _req: &fuser::Request<'_>, ino: u64, flags: i32, reply: fuser::ReplyOpen) {
if self.get_attr(ino).is_none() {
reply.error(libc::ENOENT);
return;
}
if self.read_only {
let write_flags = libc::O_WRONLY | libc::O_RDWR | libc::O_APPEND | libc::O_TRUNC;
if flags & write_flags != 0 {
reply.error(libc::EROFS);
return;
}
}
reply.opened(0, 0);
}
fn release(
&mut self,
_req: &fuser::Request<'_>,
_ino: u64,
_fh: u64,
_flags: i32,
_lock_owner: Option<u64>,
_flush: bool,
reply: fuser::ReplyEmpty,
) {
reply.ok();
}
fn opendir(
&mut self,
_req: &fuser::Request<'_>,
ino: u64,
_flags: i32,
reply: fuser::ReplyOpen,
) {
match self.get_attr(ino) {
Some(attr) if attr.kind == FileKind::Directory => {
reply.opened(0, 0);
}
Some(_) => {
reply.error(libc::ENOTDIR);
}
None => {
reply.error(libc::ENOENT);
}
}
}
fn readdir(
&mut self,
_req: &fuser::Request<'_>,
ino: u64,
_fh: u64,
offset: i64,
mut reply: fuser::ReplyDirectory,
) {
let mut entries: Vec<(u64, fuser::FileType, String)> = Vec::new();
entries.push((ino, fuser::FileType::Directory, ".".to_string()));
let parent_ino = self.get_parent(ino).unwrap_or(ino);
entries.push((parent_ino, fuser::FileType::Directory, "..".to_string()));
if let Some(dir_entries) = self.read_dir(ino) {
for entry in dir_entries {
entries.push((entry.ino, entry.kind.into(), entry.name));
}
}
for (i, (ino, kind, name)) in entries.into_iter().enumerate().skip(offset as usize) {
if reply.add(ino, (i + 1) as i64, kind, &name) {
break;
}
}
reply.ok();
}
fn releasedir(
&mut self,
_req: &fuser::Request<'_>,
_ino: u64,
_fh: u64,
_flags: i32,
reply: fuser::ReplyEmpty,
) {
reply.ok();
}
fn statfs(&mut self, _req: &fuser::Request<'_>, _ino: u64, reply: fuser::ReplyStatfs) {
let total_files = self.file_count() as u64;
let total_size = self.total_size();
let block_size = 4096u64;
let total_blocks = total_size.div_ceil(block_size);
reply.statfs(
total_blocks, 0, 0, total_files, 0, block_size as u32, 255, block_size as u32, );
}
fn access(&mut self, _req: &fuser::Request<'_>, ino: u64, mask: i32, reply: fuser::ReplyEmpty) {
if self.get_attr(ino).is_none() {
reply.error(libc::ENOENT);
return;
}
if self.read_only && (mask & libc::W_OK != 0) {
reply.error(libc::EROFS);
return;
}
reply.ok();
}
fn readlink(&mut self, _req: &fuser::Request<'_>, ino: u64, reply: fuser::ReplyData) {
match self.get_attr(ino) {
Some(attr) if attr.kind == FileKind::Symlink => {
match self.read_symlink(ino) {
Some(target) => {
reply.data(target.as_bytes());
}
None => {
eprintln!("WARNING: Symlink {} has no target stored", ino);
reply.error(libc::EIO);
}
}
}
Some(_) => {
reply.error(libc::EINVAL); }
None => {
reply.error(libc::ENOENT);
}
}
}
fn symlink(
&mut self,
_req: &fuser::Request<'_>,
parent: u64,
link_name: &OsStr,
target: &std::path::Path,
reply: fuser::ReplyEntry,
) {
if self.read_only {
reply.error(libc::EROFS);
return;
}
let link_name = match link_name.to_str() {
Some(n) => n,
None => {
reply.error(libc::EINVAL);
return;
}
};
let target = match target.to_str() {
Some(t) => t.to_string(),
None => {
reply.error(libc::EINVAL);
return;
}
};
let parent_path_str = match self.inode_paths.read() {
Ok(paths) => match paths.get(&parent) {
Some(p) => p.clone(),
None => {
reply.error(libc::ENOENT);
return;
}
},
Err(_) => {
reply.error(libc::EIO);
return;
}
};
let symlink_path = if parent_path_str == "/" {
format!("/{}", link_name)
} else {
format!("{}/{}", parent_path_str, link_name)
};
match self.add_symlink(&symlink_path, target) {
Ok(ino) => {
if let Some(attr) = self.get_attr(ino) {
let fuser_attr: fuser::FileAttr = attr.into();
reply.entry(&self.entry_ttl, &fuser_attr, 0);
} else {
reply.error(libc::EIO);
}
}
Err(_) => {
reply.error(libc::EIO);
}
}
}
fn mknod(
&mut self,
_req: &fuser::Request<'_>,
parent: u64,
name: &OsStr,
mode: u32,
_umask: u32,
rdev: u32,
reply: fuser::ReplyEntry,
) {
if self.read_only {
reply.error(libc::EROFS);
return;
}
let name = match name.to_str() {
Some(n) => n,
None => {
reply.error(libc::EINVAL);
return;
}
};
let parent_path_str = match self.inode_paths.read() {
Ok(paths) => match paths.get(&parent) {
Some(p) => p.clone(),
None => {
reply.error(libc::ENOENT);
return;
}
},
Err(_) => {
reply.error(libc::EIO);
return;
}
};
let file_path = if parent_path_str == "/" {
format!("/{}", name)
} else {
format!("{}/{}", parent_path_str, name)
};
let file_type = mode & libc::S_IFMT;
let major = (rdev >> 8) & 0xff;
let minor = rdev & 0xff;
let result = match file_type {
libc::S_IFCHR => self.add_device(&file_path, true, major, minor, Vec::new()),
libc::S_IFBLK => self.add_device(&file_path, false, major, minor, Vec::new()),
libc::S_IFIFO => self.add_fifo(&file_path),
libc::S_IFSOCK => self.add_socket(&file_path),
_ => {
reply.error(libc::EINVAL);
return;
}
};
match result {
Ok(ino) => {
if let Some(attr) = self.get_attr(ino) {
let fuser_attr: fuser::FileAttr = attr.into();
reply.entry(&self.entry_ttl, &fuser_attr, 0);
} else {
reply.error(libc::EIO);
}
}
Err(_) => {
reply.error(libc::EIO);
}
}
}
}
#[cfg(feature = "fuse")]
#[derive(Clone, Debug)]
pub struct MountOptions {
pub read_only: bool,
pub allow_other: bool,
pub allow_root: bool,
pub fsname: String,
}
#[cfg(feature = "fuse")]
impl Default for MountOptions {
fn default() -> Self {
MountOptions {
read_only: true,
allow_other: false,
allow_root: true,
fsname: "engram".to_string(),
}
}
}
#[cfg(feature = "fuse")]
pub fn mount<P: AsRef<Path>>(
fs: EngramFS,
mountpoint: P,
options: MountOptions,
) -> Result<(), std::io::Error> {
use fuser::MountOption;
let mut mount_options = vec![
MountOption::FSName(options.fsname),
MountOption::AutoUnmount,
MountOption::DefaultPermissions,
];
if options.read_only {
mount_options.push(MountOption::RO);
}
if options.allow_other {
mount_options.push(MountOption::AllowOther);
} else if options.allow_root {
mount_options.push(MountOption::AllowRoot);
}
fuser::mount2(fs, mountpoint.as_ref(), &mount_options)
}
#[cfg(feature = "fuse")]
pub fn mount_with_signals<P: AsRef<Path>>(
fs: EngramFS,
mountpoint: P,
options: MountOptions,
) -> Result<(), std::io::Error> {
use crate::fs::signal::{install_signal_handlers, ShutdownSignal};
use fuser::MountOption;
use std::sync::Arc;
let shutdown = Arc::new(ShutdownSignal::new());
install_signal_handlers(shutdown.clone())?;
let mut mount_options = vec![
MountOption::FSName(options.fsname),
MountOption::AutoUnmount,
MountOption::DefaultPermissions,
];
if options.read_only {
mount_options.push(MountOption::RO);
}
if options.allow_other {
mount_options.push(MountOption::AllowOther);
} else if options.allow_root {
mount_options.push(MountOption::AllowRoot);
}
let session = fuser::spawn_mount2(fs, mountpoint.as_ref(), &mount_options)?;
eprintln!("EngramFS mounted. Press Ctrl+C to unmount gracefully.");
loop {
if shutdown.is_signaled() {
eprintln!(
"\nReceived {} - unmounting gracefully...",
shutdown.signal_name()
);
drop(session);
break;
}
std::thread::sleep(std::time::Duration::from_millis(100));
}
eprintln!("EngramFS unmounted cleanly.");
Ok(())
}
#[cfg(feature = "fuse")]
pub fn spawn_mount<P: AsRef<Path>>(
fs: EngramFS,
mountpoint: P,
options: MountOptions,
) -> Result<fuser::BackgroundSession, std::io::Error> {
use fuser::MountOption;
let mut mount_options = vec![
MountOption::FSName(options.fsname),
MountOption::AutoUnmount,
MountOption::DefaultPermissions,
];
if options.read_only {
mount_options.push(MountOption::RO);
}
if options.allow_other {
mount_options.push(MountOption::AllowOther);
} else if options.allow_root {
mount_options.push(MountOption::AllowRoot);
}
fuser::spawn_mount2(fs, mountpoint.as_ref(), &mount_options)
}
pub struct EngramFSBuilder {
fs: EngramFS,
}
impl EngramFSBuilder {
pub fn new() -> Self {
EngramFSBuilder {
fs: EngramFS::new(true), }
}
pub fn add_file(self, path: &str, data: Vec<u8>) -> Self {
let _ = self.fs.add_file(path, data);
self
}
pub fn read_only(mut self, read_only: bool) -> Self {
self.fs.read_only = read_only;
self
}
pub fn build(self) -> EngramFS {
self.fs
}
}
impl Default for EngramFSBuilder {
fn default() -> Self {
Self::new()
}
}
fn normalize_path(path: &str) -> String {
let path = if path.starts_with('/') {
path.to_string()
} else {
format!("/{}", path)
};
if path.len() > 1 && path.ends_with('/') {
path[..path.len() - 1].to_string()
} else {
path
}
}
fn parent_path(path: &str) -> Option<String> {
let path = normalize_path(path);
if path == "/" {
return None;
}
match path.rfind('/') {
Some(0) => Some("/".to_string()),
Some(pos) => Some(path[..pos].to_string()),
None => None,
}
}
fn filename(path: &str) -> Option<&str> {
let path = path.trim_end_matches('/');
path.rsplit('/').next()
}
#[allow(dead_code)]
fn system_time_to_unix(time: SystemTime) -> u64 {
time.duration_since(UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or(0)
}
#[derive(Clone, Debug, Default)]
pub struct MountStats {
pub reads: u64,
pub read_bytes: u64,
pub lookups: u64,
pub readdirs: u64,
pub cache_hits: u64,
pub cache_misses: u64,
pub decode_time_us: u64,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_normalize_path() {
assert_eq!(normalize_path("foo"), "/foo");
assert_eq!(normalize_path("/foo"), "/foo");
assert_eq!(normalize_path("/foo/"), "/foo");
assert_eq!(normalize_path("/"), "/");
}
#[test]
fn test_parent_path() {
assert_eq!(parent_path("/foo/bar"), Some("/foo".to_string()));
assert_eq!(parent_path("/foo"), Some("/".to_string()));
assert_eq!(parent_path("/"), None);
}
#[test]
fn test_filename() {
assert_eq!(filename("/foo/bar"), Some("bar"));
assert_eq!(filename("/foo"), Some("foo"));
assert_eq!(filename("/foo/bar/"), Some("bar"));
}
#[test]
fn test_add_file() {
let fs = EngramFS::new(true);
let ino = fs.add_file("/test.txt", b"hello world".to_vec()).unwrap();
assert!(ino > ROOT_INO);
let data = fs.read_data(ino, 0, 100).unwrap();
assert_eq!(data, b"hello world");
}
#[test]
fn test_nested_directories() {
let fs = EngramFS::new(true);
fs.add_file("/a/b/c/file.txt", b"deep".to_vec()).unwrap();
assert!(fs.lookup_path("/a").is_some());
assert!(fs.lookup_path("/a/b").is_some());
assert!(fs.lookup_path("/a/b/c").is_some());
assert!(fs.lookup_path("/a/b/c/file.txt").is_some());
}
#[test]
fn test_readdir() {
let fs = EngramFS::new(true);
fs.add_file("/foo.txt", b"foo".to_vec()).unwrap();
fs.add_file("/bar.txt", b"bar".to_vec()).unwrap();
fs.add_file("/subdir/baz.txt", b"baz".to_vec()).unwrap();
let root_entries = fs.read_dir(ROOT_INO).unwrap();
assert_eq!(root_entries.len(), 3);
let names: Vec<_> = root_entries.iter().map(|e| e.name.as_str()).collect();
assert!(names.contains(&"foo.txt"));
assert!(names.contains(&"bar.txt"));
assert!(names.contains(&"subdir"));
}
#[test]
fn test_read_partial() {
let fs = EngramFS::new(true);
let data = b"0123456789";
let ino = fs.add_file("/test.txt", data.to_vec()).unwrap();
let partial = fs.read_data(ino, 3, 4).unwrap();
assert_eq!(partial, b"3456");
let past_end = fs.read_data(ino, 20, 10).unwrap();
assert!(past_end.is_empty());
}
#[test]
fn test_builder() {
let fs = EngramFSBuilder::new()
.add_file("/a.txt", b"a".to_vec())
.add_file("/b.txt", b"b".to_vec())
.build();
assert_eq!(fs.file_count(), 2);
}
#[test]
fn test_get_parent() {
let fs = EngramFS::new(true);
fs.add_file("/a/b/c.txt", b"test".to_vec()).unwrap();
let c_ino = fs.lookup_path("/a/b/c.txt").unwrap();
let b_ino = fs.lookup_path("/a/b").unwrap();
let a_ino = fs.lookup_path("/a").unwrap();
assert_eq!(fs.get_parent(c_ino), Some(b_ino));
assert_eq!(fs.get_parent(b_ino), Some(a_ino));
assert_eq!(fs.get_parent(a_ino), Some(ROOT_INO));
assert_eq!(fs.get_parent(ROOT_INO), Some(ROOT_INO));
}
#[test]
fn test_default_attrs() {
let attr = FileAttr::default();
assert_eq!(attr.perm, 0o644);
assert_eq!(attr.nlink, 1);
assert_eq!(attr.blksize, 4096);
}
#[test]
fn test_file_kind_conversion() {
#[cfg(feature = "fuse")]
{
let dir: fuser::FileType = FileKind::Directory.into();
assert_eq!(dir, fuser::FileType::Directory);
let file: fuser::FileType = FileKind::RegularFile.into();
assert_eq!(file, fuser::FileType::RegularFile);
}
}
#[test]
fn test_lock_poisoning_recovery() {
use std::sync::Arc;
use std::thread;
let fs = Arc::new(EngramFS::new(true));
fs.add_file("/test.txt", b"hello".to_vec()).unwrap();
let ino = fs.lookup_path("/test.txt").unwrap();
let data = fs.read_data(ino, 0, 5);
assert!(data.is_some());
assert_eq!(data.unwrap(), b"hello");
let found_ino = fs.lookup_path("/test.txt");
assert_eq!(found_ino, Some(ino));
let attr = fs.get_attr(ino);
assert!(attr.is_some());
assert_eq!(attr.unwrap().size, 5);
let fs_clone = Arc::clone(&fs);
let handle = thread::spawn(move || {
for _ in 0..10 {
let _ = fs_clone.read_data(ino, 0, 5);
let _ = fs_clone.lookup_path("/test.txt");
}
});
for _ in 0..10 {
let _ = fs.read_data(ino, 0, 5);
let _ = fs.get_attr(ino);
}
handle.join().unwrap();
assert_eq!(fs.file_count(), 1);
assert_eq!(fs.total_size(), 5);
}
#[test]
fn test_write_lock_error_propagation() {
let fs = EngramFS::new(false);
let result = fs.add_file("/test.txt", b"content".to_vec());
assert!(result.is_ok());
assert!(fs.lookup_path("/test.txt").is_some());
assert_eq!(fs.file_count(), 1);
}
}