use crate::{DirEntry, FileStat, FileType, Pool};
use fuser::{
FileAttr, FileType as FuseFileType, Filesystem, ReplyAttr, ReplyData, ReplyDirectory,
ReplyEntry, ReplyOpen, ReplyWrite, Request, TimeOrNow,
};
use std::collections::HashMap;
use std::ffi::OsStr;
use std::sync::{Arc, Mutex};
use std::time::{Duration, SystemTime, UNIX_EPOCH};
const TTL: Duration = Duration::from_secs(1);
fn to_fuse_file_type(ft: FileType) -> FuseFileType {
match ft {
FileType::RegularFile => FuseFileType::RegularFile,
FileType::Directory => FuseFileType::Directory,
FileType::Symlink => FuseFileType::Symlink,
FileType::BlockDevice => FuseFileType::BlockDevice,
FileType::CharDevice => FuseFileType::CharDevice,
FileType::Fifo => FuseFileType::NamedPipe,
FileType::Socket => FuseFileType::Socket,
}
}
fn to_file_attr(stat: &FileStat, ino: u64) -> FileAttr {
let kind = to_fuse_file_type(stat.file_type);
let atime = UNIX_EPOCH + Duration::from_nanos(stat.atime as u64);
let mtime = UNIX_EPOCH + Duration::from_nanos(stat.mtime as u64);
let ctime = UNIX_EPOCH + Duration::from_nanos(stat.ctime as u64);
FileAttr {
ino,
size: stat.size,
blocks: (stat.size + 511) / 512, atime,
mtime,
ctime,
crtime: ctime, kind,
perm: stat.mode as u16,
nlink: stat.nlink as u32,
uid: stat.uid,
gid: stat.gid,
rdev: 0,
blksize: 4096,
flags: 0,
}
}
pub struct LcpfsFuse {
pool: Arc<Mutex<Pool>>,
inode_to_path: Mutex<HashMap<u64, String>>,
path_to_inode: Mutex<HashMap<String, u64>>,
next_inode: Mutex<u64>,
open_files: Mutex<HashMap<u64, (String, u64)>>,
next_fh: Mutex<u64>,
}
impl LcpfsFuse {
pub fn new(pool: Pool) -> Self {
let mut inode_to_path = HashMap::new();
let mut path_to_inode = HashMap::new();
inode_to_path.insert(1, "/".to_string());
path_to_inode.insert("/".to_string(), 1);
Self {
pool: Arc::new(Mutex::new(pool)),
inode_to_path: Mutex::new(inode_to_path),
path_to_inode: Mutex::new(path_to_inode),
next_inode: Mutex::new(2), open_files: Mutex::new(HashMap::new()),
next_fh: Mutex::new(1),
}
}
fn get_or_create_inode(&self, path: &str) -> u64 {
let mut path_to_inode = self.path_to_inode.lock().unwrap();
let mut inode_to_path = self.inode_to_path.lock().unwrap();
if let Some(&ino) = path_to_inode.get(path) {
return ino;
}
let mut next = self.next_inode.lock().unwrap();
let ino = *next;
*next += 1;
path_to_inode.insert(path.to_string(), ino);
inode_to_path.insert(ino, path.to_string());
ino
}
fn get_path(&self, ino: u64) -> Option<String> {
self.inode_to_path.lock().unwrap().get(&ino).cloned()
}
fn join_path(parent: &str, name: &str) -> String {
if parent == "/" {
format!("/{}", name)
} else {
format!("{}/{}", parent, name)
}
}
fn alloc_fh(&self, path: String, fd: u64) -> u64 {
let mut next = self.next_fh.lock().unwrap();
let fh = *next;
*next += 1;
self.open_files.lock().unwrap().insert(fh, (path, fd));
fh
}
fn get_fd(&self, fh: u64) -> Option<u64> {
self.open_files.lock().unwrap().get(&fh).map(|(_, fd)| *fd)
}
fn remove_fh(&self, fh: u64) -> Option<(String, u64)> {
self.open_files.lock().unwrap().remove(&fh)
}
}
impl Filesystem for LcpfsFuse {
fn lookup(&mut self, _req: &Request<'_>, parent: u64, name: &OsStr, reply: ReplyEntry) {
let Some(parent_path) = self.get_path(parent) else {
reply.error(libc::ENOENT);
return;
};
let name_str = name.to_string_lossy();
let path = Self::join_path(&parent_path, &name_str);
let pool = self.pool.lock().unwrap();
match pool.stat(&path) {
Ok(stat) => {
let ino = self.get_or_create_inode(&path);
let attr = to_file_attr(&stat, ino);
reply.entry(&TTL, &attr, 0);
}
Err(_) => {
reply.error(libc::ENOENT);
}
}
}
fn getattr(&mut self, _req: &Request<'_>, ino: u64, _fh: Option<u64>, reply: ReplyAttr) {
let Some(path) = self.get_path(ino) else {
reply.error(libc::ENOENT);
return;
};
let pool = self.pool.lock().unwrap();
match pool.stat(&path) {
Ok(stat) => {
let attr = to_file_attr(&stat, ino);
reply.attr(&TTL, &attr);
}
Err(_) => {
reply.error(libc::ENOENT);
}
}
}
fn setattr(
&mut self,
_req: &Request<'_>,
ino: u64,
mode: Option<u32>,
uid: Option<u32>,
gid: Option<u32>,
size: Option<u64>,
_atime: Option<TimeOrNow>,
_mtime: Option<TimeOrNow>,
_ctime: Option<SystemTime>,
_fh: Option<u64>,
_crtime: Option<SystemTime>,
_chgtime: Option<SystemTime>,
_bkuptime: Option<SystemTime>,
_flags: Option<u32>,
reply: ReplyAttr,
) {
let Some(path) = self.get_path(ino) else {
reply.error(libc::ENOENT);
return;
};
let mut pool = self.pool.lock().unwrap();
if let Some(new_mode) = mode {
if pool.chmod(&path, new_mode).is_err() {
reply.error(libc::EIO);
return;
}
}
if uid.is_some() || gid.is_some() {
let new_uid = uid.unwrap_or(u32::MAX);
let new_gid = gid.unwrap_or(u32::MAX);
if pool.chown(&path, new_uid, new_gid).is_err() {
reply.error(libc::EIO);
return;
}
}
if let Some(new_size) = size {
if pool.truncate(&path, new_size).is_err() {
reply.error(libc::EIO);
return;
}
}
match pool.stat(&path) {
Ok(stat) => {
let attr = to_file_attr(&stat, ino);
reply.attr(&TTL, &attr);
}
Err(_) => {
reply.error(libc::EIO);
}
}
}
fn readlink(&mut self, _req: &Request<'_>, ino: u64, reply: ReplyData) {
let Some(path) = self.get_path(ino) else {
reply.error(libc::ENOENT);
return;
};
let pool = self.pool.lock().unwrap();
match pool.readlink(&path) {
Ok(target) => {
reply.data(target.as_bytes());
}
Err(_) => {
reply.error(libc::EIO);
}
}
}
fn mknod(
&mut self,
_req: &Request<'_>,
parent: u64,
name: &OsStr,
mode: u32,
_umask: u32,
_rdev: u32,
reply: ReplyEntry,
) {
let Some(parent_path) = self.get_path(parent) else {
reply.error(libc::ENOENT);
return;
};
let name_str = name.to_string_lossy();
let path = Self::join_path(&parent_path, &name_str);
let mut pool = self.pool.lock().unwrap();
match pool.create(&path, mode) {
Ok(fd) => {
let _ = pool.close(fd);
match pool.stat(&path) {
Ok(stat) => {
let ino = self.get_or_create_inode(&path);
let attr = to_file_attr(&stat, ino);
reply.entry(&TTL, &attr, 0);
}
Err(_) => {
reply.error(libc::EIO);
}
}
}
Err(_) => {
reply.error(libc::EIO);
}
}
}
fn mkdir(
&mut self,
_req: &Request<'_>,
parent: u64,
name: &OsStr,
mode: u32,
_umask: u32,
reply: ReplyEntry,
) {
let Some(parent_path) = self.get_path(parent) else {
reply.error(libc::ENOENT);
return;
};
let name_str = name.to_string_lossy();
let path = Self::join_path(&parent_path, &name_str);
let mut pool = self.pool.lock().unwrap();
match pool.mkdir(&path, mode) {
Ok(()) => match pool.stat(&path) {
Ok(stat) => {
let ino = self.get_or_create_inode(&path);
let attr = to_file_attr(&stat, ino);
reply.entry(&TTL, &attr, 0);
}
Err(_) => {
reply.error(libc::EIO);
}
},
Err(_) => {
reply.error(libc::EIO);
}
}
}
fn unlink(&mut self, _req: &Request<'_>, parent: u64, name: &OsStr, reply: fuser::ReplyEmpty) {
let Some(parent_path) = self.get_path(parent) else {
reply.error(libc::ENOENT);
return;
};
let name_str = name.to_string_lossy();
let path = Self::join_path(&parent_path, &name_str);
let mut pool = self.pool.lock().unwrap();
match pool.unlink(&path) {
Ok(()) => reply.ok(),
Err(_) => reply.error(libc::EIO),
}
}
fn rmdir(&mut self, _req: &Request<'_>, parent: u64, name: &OsStr, reply: fuser::ReplyEmpty) {
let Some(parent_path) = self.get_path(parent) else {
reply.error(libc::ENOENT);
return;
};
let name_str = name.to_string_lossy();
let path = Self::join_path(&parent_path, &name_str);
let mut pool = self.pool.lock().unwrap();
match pool.rmdir(&path) {
Ok(()) => reply.ok(),
Err(_) => reply.error(libc::EIO),
}
}
fn symlink(
&mut self,
_req: &Request<'_>,
parent: u64,
name: &OsStr,
link: &std::path::Path,
reply: ReplyEntry,
) {
let Some(parent_path) = self.get_path(parent) else {
reply.error(libc::ENOENT);
return;
};
let name_str = name.to_string_lossy();
let path = Self::join_path(&parent_path, &name_str);
let target = link.to_string_lossy();
let mut pool = self.pool.lock().unwrap();
match pool.symlink(&path, &target) {
Ok(()) => match pool.stat(&path) {
Ok(stat) => {
let ino = self.get_or_create_inode(&path);
let attr = to_file_attr(&stat, ino);
reply.entry(&TTL, &attr, 0);
}
Err(_) => reply.error(libc::EIO),
},
Err(_) => reply.error(libc::EIO),
}
}
fn rename(
&mut self,
_req: &Request<'_>,
parent: u64,
name: &OsStr,
newparent: u64,
newname: &OsStr,
_flags: u32,
reply: fuser::ReplyEmpty,
) {
let Some(parent_path) = self.get_path(parent) else {
reply.error(libc::ENOENT);
return;
};
let Some(newparent_path) = self.get_path(newparent) else {
reply.error(libc::ENOENT);
return;
};
let old_path = Self::join_path(&parent_path, &name.to_string_lossy());
let new_path = Self::join_path(&newparent_path, &newname.to_string_lossy());
let mut pool = self.pool.lock().unwrap();
match pool.rename(&old_path, &new_path) {
Ok(()) => reply.ok(),
Err(_) => reply.error(libc::EIO),
}
}
fn link(
&mut self,
_req: &Request<'_>,
ino: u64,
newparent: u64,
newname: &OsStr,
reply: ReplyEntry,
) {
let Some(existing_path) = self.get_path(ino) else {
reply.error(libc::ENOENT);
return;
};
let Some(parent_path) = self.get_path(newparent) else {
reply.error(libc::ENOENT);
return;
};
let new_path = Self::join_path(&parent_path, &newname.to_string_lossy());
let mut pool = self.pool.lock().unwrap();
match pool.link(&existing_path, &new_path) {
Ok(()) => match pool.stat(&new_path) {
Ok(stat) => {
let new_ino = self.get_or_create_inode(&new_path);
let attr = to_file_attr(&stat, new_ino);
reply.entry(&TTL, &attr, 0);
}
Err(_) => reply.error(libc::EIO),
},
Err(_) => reply.error(libc::EIO),
}
}
fn open(&mut self, _req: &Request<'_>, ino: u64, flags: i32, reply: ReplyOpen) {
let Some(path) = self.get_path(ino) else {
reply.error(libc::ENOENT);
return;
};
let mut pool = self.pool.lock().unwrap();
match pool.open(&path, flags as u32) {
Ok(fd) => {
let fh = self.alloc_fh(path, fd);
reply.opened(fh, 0);
}
Err(_) => {
reply.error(libc::EIO);
}
}
}
fn read(
&mut self,
_req: &Request<'_>,
_ino: u64,
fh: u64,
offset: i64,
size: u32,
_flags: i32,
_lock_owner: Option<u64>,
reply: ReplyData,
) {
let Some(fd) = self.get_fd(fh) else {
reply.error(libc::EBADF);
return;
};
let mut pool = self.pool.lock().unwrap();
if pool.lseek(fd, offset, 0).is_err() {
reply.error(libc::EIO);
return;
}
let mut buf = vec![0u8; size as usize];
match pool.read(fd, &mut buf) {
Ok(n) => {
reply.data(&buf[..n]);
}
Err(_) => {
reply.error(libc::EIO);
}
}
}
fn write(
&mut self,
_req: &Request<'_>,
_ino: u64,
fh: u64,
offset: i64,
data: &[u8],
_write_flags: u32,
_flags: i32,
_lock_owner: Option<u64>,
reply: ReplyWrite,
) {
let Some(fd) = self.get_fd(fh) else {
reply.error(libc::EBADF);
return;
};
let mut pool = self.pool.lock().unwrap();
if pool.lseek(fd, offset, 0).is_err() {
reply.error(libc::EIO);
return;
}
match pool.write(fd, data) {
Ok(n) => {
reply.written(n as u32);
}
Err(_) => {
reply.error(libc::EIO);
}
}
}
fn release(
&mut self,
_req: &Request<'_>,
_ino: u64,
fh: u64,
_flags: i32,
_lock_owner: Option<u64>,
_flush: bool,
reply: fuser::ReplyEmpty,
) {
if let Some((_path, fd)) = self.remove_fh(fh) {
let mut pool = self.pool.lock().unwrap();
let _ = pool.close(fd);
}
reply.ok();
}
fn fsync(
&mut self,
_req: &Request<'_>,
_ino: u64,
fh: u64,
_datasync: bool,
reply: fuser::ReplyEmpty,
) {
if let Some(fd) = self.get_fd(fh) {
let mut pool = self.pool.lock().unwrap();
match pool.fsync(fd) {
Ok(()) => reply.ok(),
Err(_) => reply.error(libc::EIO),
}
} else {
reply.error(libc::EBADF);
}
}
fn opendir(&mut self, _req: &Request<'_>, ino: u64, _flags: i32, reply: ReplyOpen) {
if self.get_path(ino).is_some() {
reply.opened(ino, 0);
} else {
reply.error(libc::ENOENT);
}
}
fn readdir(
&mut self,
_req: &Request<'_>,
ino: u64,
_fh: u64,
offset: i64,
mut reply: ReplyDirectory,
) {
let Some(path) = self.get_path(ino) else {
reply.error(libc::ENOENT);
return;
};
let pool = self.pool.lock().unwrap();
match pool.readdir(&path) {
Ok(entries) => {
let mut all_entries: Vec<(u64, FuseFileType, String)> = vec![
(ino, FuseFileType::Directory, ".".to_string()),
(ino, FuseFileType::Directory, "..".to_string()), ];
for entry in entries {
let entry_path = Self::join_path(&path, &entry.name);
let entry_ino = self.get_or_create_inode(&entry_path);
let file_type = to_fuse_file_type(entry.file_type);
all_entries.push((entry_ino, file_type, entry.name));
}
for (i, (entry_ino, file_type, name)) in
all_entries.iter().enumerate().skip(offset as usize)
{
if reply.add(*entry_ino, (i + 1) as i64, *file_type, name) {
break;
}
}
reply.ok();
}
Err(_) => {
reply.error(libc::EIO);
}
}
}
fn releasedir(
&mut self,
_req: &Request<'_>,
_ino: u64,
_fh: u64,
_flags: i32,
reply: fuser::ReplyEmpty,
) {
reply.ok();
}
fn statfs(&mut self, _req: &Request<'_>, _ino: u64, reply: fuser::ReplyStatfs) {
let pool = self.pool.lock().unwrap();
if let Ok(stats) = pool.statfs() {
reply.statfs(
stats.total_blocks,
stats.free_blocks,
stats.available_blocks,
stats.total_files,
stats.free_files,
stats.block_size as u32,
stats.max_name_len as u32,
stats.fragment_size as u32,
);
} else {
reply.statfs(
1_000_000, 500_000, 500_000, 1_000_000, 500_000, 4096, 255, 4096, );
}
}
fn create(
&mut self,
_req: &Request<'_>,
parent: u64,
name: &OsStr,
mode: u32,
_umask: u32,
flags: i32,
reply: fuser::ReplyCreate,
) {
let Some(parent_path) = self.get_path(parent) else {
reply.error(libc::ENOENT);
return;
};
let name_str = name.to_string_lossy();
let path = Self::join_path(&parent_path, &name_str);
let mut pool = self.pool.lock().unwrap();
match pool.create(&path, mode) {
Ok(fd) => {
let _ = pool.close(fd);
match pool.open(&path, flags as u32) {
Ok(fd) => {
let fh = self.alloc_fh(path.clone(), fd);
match pool.stat(&path) {
Ok(stat) => {
let ino = self.get_or_create_inode(&path);
let attr = to_file_attr(&stat, ino);
reply.created(&TTL, &attr, 0, fh, 0);
}
Err(_) => reply.error(libc::EIO),
}
}
Err(_) => reply.error(libc::EIO),
}
}
Err(_) => reply.error(libc::EIO),
}
}
fn getxattr(
&mut self,
_req: &Request<'_>,
ino: u64,
name: &OsStr,
size: u32,
reply: fuser::ReplyXattr,
) {
let Some(path) = self.get_path(ino) else {
reply.error(libc::ENOENT);
return;
};
let pool = self.pool.lock().unwrap();
let name_str = name.to_string_lossy();
match pool.getxattr(&path, &name_str) {
Ok(value) => {
if size == 0 {
reply.size(value.len() as u32);
} else if size < value.len() as u32 {
reply.error(libc::ERANGE);
} else {
reply.data(&value);
}
}
Err(_) => reply.error(libc::ENODATA),
}
}
fn setxattr(
&mut self,
_req: &Request<'_>,
ino: u64,
name: &OsStr,
value: &[u8],
_flags: i32,
_position: u32,
reply: fuser::ReplyEmpty,
) {
let Some(path) = self.get_path(ino) else {
reply.error(libc::ENOENT);
return;
};
let mut pool = self.pool.lock().unwrap();
let name_str = name.to_string_lossy();
match pool.setxattr(&path, &name_str, value) {
Ok(()) => reply.ok(),
Err(_) => reply.error(libc::EIO),
}
}
fn listxattr(&mut self, _req: &Request<'_>, ino: u64, size: u32, reply: fuser::ReplyXattr) {
let Some(path) = self.get_path(ino) else {
reply.error(libc::ENOENT);
return;
};
let pool = self.pool.lock().unwrap();
match pool.listxattr(&path) {
Ok(names) => {
let mut data = Vec::new();
for name in names {
data.extend_from_slice(name.as_bytes());
data.push(0);
}
if size == 0 {
reply.size(data.len() as u32);
} else if size < data.len() as u32 {
reply.error(libc::ERANGE);
} else {
reply.data(&data);
}
}
Err(_) => reply.error(libc::EIO),
}
}
fn removexattr(
&mut self,
_req: &Request<'_>,
ino: u64,
name: &OsStr,
reply: fuser::ReplyEmpty,
) {
let Some(path) = self.get_path(ino) else {
reply.error(libc::ENOENT);
return;
};
let mut pool = self.pool.lock().unwrap();
let name_str = name.to_string_lossy();
match pool.removexattr(&path, &name_str) {
Ok(()) => reply.ok(),
Err(_) => reply.error(libc::EIO),
}
}
fn access(&mut self, _req: &Request<'_>, ino: u64, _mask: i32, reply: fuser::ReplyEmpty) {
if self.get_path(ino).is_some() {
reply.ok();
} else {
reply.error(libc::ENOENT);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_join_path() {
assert_eq!(LcpfsFuse::join_path("/", "foo"), "/foo");
assert_eq!(LcpfsFuse::join_path("/home", "user"), "/home/user");
assert_eq!(LcpfsFuse::join_path("/a/b", "c"), "/a/b/c");
}
}