#![expect(missing_docs)]
#![cfg(any(windows, target_os = "linux"))]
mod file;
mod inode;
pub mod resolver;
#[cfg(windows)]
mod section;
mod util;
pub mod virtio;
mod virtio_util;
#[cfg(windows)]
pub use section::SectionFs;
use file::VirtioFsFile;
use fuse::protocol::*;
use fuse::*;
use inode::VirtioFsInode;
pub use lxutil::LxVolumeOptions;
use parking_lot::RwLock;
use std::collections::HashMap;
use std::collections::hash_map::Entry;
use std::path::Path;
use std::path::PathBuf;
use std::sync::Arc;
use std::time::Duration;
const ATTRIBUTE_TIMEOUT: Duration = Duration::from_millis(1);
const ENTRY_TIMEOUT: Duration = Duration::from_secs(0);
pub struct VirtioFs {
inodes: RwLock<InodeMap>,
files: RwLock<HandleMap<Arc<VirtioFsFile>>>,
}
impl Fuse for VirtioFs {
fn init(&self, info: &mut SessionInfo) {
if info.capable() & FUSE_DO_READDIRPLUS != 0 {
info.want |= FUSE_DO_READDIRPLUS;
}
if info.capable() & FUSE_READDIRPLUS_AUTO != 0 {
info.want |= FUSE_READDIRPLUS_AUTO;
}
}
fn get_attr(&self, request: &Request, flags: u32, fh: u64) -> lx::Result<fuse_attr_out> {
let node_id = request.node_id();
let attr = if flags & FUSE_GETATTR_FH != 0 {
let file = self.get_file(fh)?;
file.get_attr()?
} else {
let inode = self.get_inode(node_id)?;
inode.get_attr()?
};
Ok(fuse_attr_out::new(ATTRIBUTE_TIMEOUT, attr))
}
fn set_attr(&self, request: &Request, arg: &fuse_setattr_in) -> lx::Result<fuse_attr_out> {
let node_id = request.node_id();
let attr = if arg.valid & FATTR_FH != 0 {
let file = self.get_file(arg.fh)?;
file.set_attr(arg, request.uid())?;
file.get_attr()?
} else {
let inode = self.get_inode(node_id)?;
inode.set_attr(arg, request.uid())?
};
Ok(fuse_attr_out::new(ATTRIBUTE_TIMEOUT, attr))
}
fn lookup(&self, request: &Request, name: &lx::LxStr) -> lx::Result<fuse_entry_out> {
let inode = self.get_inode(request.node_id())?;
self.lookup_helper(&inode, name)
}
fn forget(&self, node_id: u64, lookup_count: u64) {
let mut inodes = self.inodes.write();
if let Some(inode) = inodes.get(node_id) {
if inode.forget(node_id, lookup_count) == 0 {
tracing::trace!(node_id, "Removing inode");
inodes.remove(node_id);
}
}
}
fn open(&self, request: &Request, flags: u32) -> lx::Result<fuse_open_out> {
let inode = self.get_inode(request.node_id())?;
let file = inode.open(flags)?;
let fh = self.insert_file(file);
Ok(fuse_open_out::new(fh, FOPEN_DIRECT_IO))
}
fn create(
&self,
request: &Request,
name: &lx::LxStr,
arg: &fuse_create_in,
) -> lx::Result<CreateOut> {
let inode = self.get_inode(request.node_id())?;
let (new_inode, attr, file) =
inode.create(name, arg.flags, arg.mode, request.uid(), request.gid())?;
let (new_inode, node_id) = self.insert_inode(new_inode);
let file = VirtioFsFile::new(file, new_inode);
let fh = self.insert_file(file);
Ok(CreateOut {
entry: fuse_entry_out::new(node_id, ENTRY_TIMEOUT, ATTRIBUTE_TIMEOUT, attr),
open: fuse_open_out::new(fh, FOPEN_DIRECT_IO),
})
}
fn mkdir(
&self,
request: &Request,
name: &lx::LxStr,
arg: &fuse_mkdir_in,
) -> lx::Result<fuse_entry_out> {
let inode = self.get_inode(request.node_id())?;
let (new_inode, attr) = inode.mkdir(name, arg.mode, request.uid(), request.gid())?;
let (_, node_id) = self.insert_inode(new_inode);
Ok(fuse_entry_out::new(
node_id,
ENTRY_TIMEOUT,
ATTRIBUTE_TIMEOUT,
attr,
))
}
fn mknod(
&self,
request: &Request,
name: &lx::LxStr,
arg: &fuse_mknod_in,
) -> lx::Result<fuse_entry_out> {
let inode = self.get_inode(request.node_id())?;
let (new_inode, attr) =
inode.mknod(name, arg.mode, request.uid(), request.gid(), arg.rdev)?;
let (_, node_id) = self.insert_inode(new_inode);
Ok(fuse_entry_out::new(
node_id,
ENTRY_TIMEOUT,
ATTRIBUTE_TIMEOUT,
attr,
))
}
fn symlink(
&self,
request: &Request,
name: &lx::LxStr,
target: &lx::LxStr,
) -> lx::Result<fuse_entry_out> {
let inode = self.get_inode(request.node_id())?;
let (new_inode, attr) = inode.symlink(name, target, request.uid(), request.gid())?;
let (_, node_id) = self.insert_inode(new_inode);
Ok(fuse_entry_out::new(
node_id,
ENTRY_TIMEOUT,
ATTRIBUTE_TIMEOUT,
attr,
))
}
fn link(&self, request: &Request, name: &lx::LxStr, target: u64) -> lx::Result<fuse_entry_out> {
let inode = self.get_inode(request.node_id())?;
let target_inode = self.get_inode(target)?;
let attr = inode.link(name, &target_inode)?;
Ok(fuse_entry_out::new(
target,
ENTRY_TIMEOUT,
ATTRIBUTE_TIMEOUT,
attr,
))
}
fn read_link(&self, request: &Request) -> lx::Result<lx::LxString> {
let inode = self.get_inode(request.node_id())?;
inode.read_link()
}
fn read(&self, _request: &Request, arg: &fuse_read_in) -> lx::Result<Vec<u8>> {
let file = self.get_file(arg.fh)?;
let mut buffer = vec![0u8; arg.size as usize];
let size = file.read(&mut buffer, arg.offset)?;
buffer.truncate(size);
Ok(buffer)
}
fn write(&self, request: &Request, arg: &fuse_write_in, data: &[u8]) -> lx::Result<usize> {
let file = self.get_file(arg.fh)?;
file.write(data, arg.offset, request.uid())
}
fn release(&self, _request: &Request, arg: &fuse_release_in) -> lx::Result<()> {
self.remove_file(arg.fh);
Ok(())
}
fn open_dir(&self, request: &Request, flags: u32) -> lx::Result<fuse_open_out> {
self.open(request, flags)
}
fn read_dir(&self, _request: &Request, arg: &fuse_read_in) -> lx::Result<Vec<u8>> {
let file = self.get_file(arg.fh)?;
file.read_dir(self, arg.offset, arg.size, false)
}
fn read_dir_plus(&self, _request: &Request, arg: &fuse_read_in) -> lx::Result<Vec<u8>> {
let file = self.get_file(arg.fh)?;
file.read_dir(self, arg.offset, arg.size, true)
}
fn release_dir(&self, request: &Request, arg: &fuse_release_in) -> lx::Result<()> {
self.release(request, arg)
}
fn unlink(&self, request: &Request, name: &lx::LxStr) -> lx::Result<()> {
self.unlink_helper(request, name, 0)
}
fn rmdir(&self, request: &Request, name: &lx::LxStr) -> lx::Result<()> {
self.unlink_helper(request, name, lx::AT_REMOVEDIR)
}
fn rename(
&self,
request: &Request,
name: &lx::LxStr,
new_dir: u64,
new_name: &lx::LxStr,
flags: u32,
) -> lx::Result<()> {
let inode = self.get_inode(request.node_id())?;
let new_inode = self.get_inode(new_dir)?;
inode.rename(name, &new_inode, new_name, flags)
}
fn statfs(&self, request: &Request) -> lx::Result<fuse_kstatfs> {
let inode = self.get_inode(request.node_id())?;
inode.stat_fs()
}
fn fsync(&self, _request: &Request, fh: u64, flags: u32) -> lx::Result<()> {
let file = self.get_file(fh)?;
let data_only = flags & FUSE_FSYNC_FDATASYNC != 0;
file.fsync(data_only)
}
fn fsync_dir(&self, request: &Request, fh: u64, flags: u32) -> lx::Result<()> {
self.fsync(request, fh, flags)
}
fn get_xattr(&self, request: &Request, name: &lx::LxStr, size: u32) -> lx::Result<Vec<u8>> {
let inode = self.get_inode(request.node_id())?;
let mut value = vec![0u8; size as usize];
let size = inode.get_xattr(name, Some(&mut value))?;
value.truncate(size);
Ok(value)
}
fn get_xattr_size(&self, request: &Request, name: &lx::LxStr) -> lx::Result<u32> {
let inode = self.get_inode(request.node_id())?;
let size = inode.get_xattr(name, None)?;
let size = size.try_into().map_err(|_| lx::Error::E2BIG)?;
Ok(size)
}
fn set_xattr(
&self,
request: &Request,
name: &lx::LxStr,
value: &[u8],
flags: u32,
) -> lx::Result<()> {
let inode = self.get_inode(request.node_id())?;
inode.set_xattr(name, value, flags)
}
fn list_xattr(&self, request: &Request, size: u32) -> lx::Result<Vec<u8>> {
let inode = self.get_inode(request.node_id())?;
let mut list = vec![0u8; size as usize];
let size = inode.list_xattr(Some(&mut list))?;
list.truncate(size);
Ok(list)
}
fn list_xattr_size(&self, request: &Request) -> lx::Result<u32> {
let inode = self.get_inode(request.node_id())?;
let size = inode.list_xattr(None)?;
let size = size.try_into().map_err(|_| lx::Error::E2BIG)?;
Ok(size)
}
fn remove_xattr(&self, request: &Request, name: &lx::LxStr) -> lx::Result<()> {
let inode = self.get_inode(request.node_id())?;
inode.remove_xattr(name)
}
fn destroy(&self) {
self.files.write().clear();
self.inodes.write().clear();
}
}
impl VirtioFs {
pub fn new(
root_path: impl AsRef<Path>,
mount_options: Option<&LxVolumeOptions>,
) -> lx::Result<Self> {
let volume = if let Some(mount_options) = mount_options {
mount_options.new_volume(root_path)
} else {
lxutil::LxVolume::new(root_path)
}?;
let mut inodes = InodeMap::new(volume.supports_stable_file_id());
let (root_inode, _) = VirtioFsInode::new(Arc::new(volume), PathBuf::new())?;
assert!(inodes.insert(root_inode).1 == FUSE_ROOT_ID);
Ok(Self {
inodes: RwLock::new(inodes),
files: RwLock::new(HandleMap::new()),
})
}
fn lookup_helper(&self, inode: &VirtioFsInode, name: &lx::LxStr) -> lx::Result<fuse_entry_out> {
let (new_inode, attr) = inode.lookup_child(name)?;
let (_, new_inode_nr) = self.insert_inode(new_inode);
Ok(fuse_entry_out::new(
new_inode_nr,
ENTRY_TIMEOUT,
ATTRIBUTE_TIMEOUT,
attr,
))
}
fn unlink_helper(&self, request: &Request, name: &lx::LxStr, flags: i32) -> lx::Result<()> {
let inode = self.get_inode(request.node_id())?;
inode.unlink(name, flags)
}
fn get_inode(&self, node_id: u64) -> lx::Result<Arc<VirtioFsInode>> {
self.inodes.read().get(node_id).ok_or_else(|| {
tracing::warn!(node_id, "request for unknown inode");
lx::Error::EINVAL
})
}
fn insert_inode(&self, inode: VirtioFsInode) -> (Arc<VirtioFsInode>, u64) {
self.inodes.write().insert(inode)
}
fn get_file(&self, fh: u64) -> lx::Result<Arc<VirtioFsFile>> {
let files = self.files.read();
let file = files.get(fh).ok_or_else(|| {
tracing::warn!(fh, "Request for unknown file");
lx::Error::EBADF
})?;
Ok(Arc::clone(file))
}
fn insert_file(&self, file: VirtioFsFile) -> u64 {
self.files.write().insert(Arc::new(file))
}
fn remove_file(&self, fh: u64) {
self.files.write().remove(fh);
}
}
struct HandleMap<T> {
values: HashMap<u64, T>,
next_handle: u64,
}
impl<T> HandleMap<T> {
pub fn new() -> Self {
Self::starting_at(1)
}
pub fn starting_at(next_handle: u64) -> Self {
Self {
values: HashMap::new(),
next_handle,
}
}
pub fn insert(&mut self, value: T) -> u64 {
let handle = self.next_handle;
if self.values.insert(handle, value).is_some() {
panic!("Inode number reused.");
}
self.next_handle += 1;
handle
}
pub fn get(&self, handle: u64) -> Option<&T> {
self.values.get(&handle)
}
#[cfg_attr(not(windows), expect(dead_code))]
pub fn get_mut(&mut self, handle: u64) -> Option<&mut T> {
self.values.get_mut(&handle)
}
pub fn remove(&mut self, handle: u64) -> Option<T> {
self.values.remove(&handle)
}
pub fn clear(&mut self) {
self.values.clear();
self.next_handle = 1;
}
}
struct InodeMap {
inodes_by_node_id: HandleMap<Arc<VirtioFsInode>>,
inodes_by_inode_nr: Option<HashMap<lx::ino_t, (Arc<VirtioFsInode>, u64)>>,
}
impl InodeMap {
pub fn new(supports_stable_file_id: bool) -> Self {
Self {
inodes_by_node_id: HandleMap::new(),
inodes_by_inode_nr: if supports_stable_file_id {
Some(HashMap::new())
} else {
None
},
}
}
pub fn get(&self, node_id: u64) -> Option<Arc<VirtioFsInode>> {
let inode = self.inodes_by_node_id.get(node_id)?;
Some(Arc::clone(inode))
}
pub fn insert(&mut self, inode: VirtioFsInode) -> (Arc<VirtioFsInode>, u64) {
if let Some(inodes_by_inode_nr) = self.inodes_by_inode_nr.as_mut() {
match inodes_by_inode_nr.entry(inode.inode_nr()) {
Entry::Occupied(entry) => {
let new_path = inode.clone_path();
let (inode, node_id) = entry.get();
inode.lookup(new_path);
return (Arc::clone(inode), *node_id);
}
Entry::Vacant(entry) => {
let inode = Arc::new(inode);
let node_id = self.inodes_by_node_id.insert(Arc::clone(&inode));
entry.insert((Arc::clone(&inode), node_id));
return (inode, node_id);
}
}
}
let inode = Arc::new(inode);
let node_id = self.inodes_by_node_id.insert(Arc::clone(&inode));
(inode, node_id)
}
pub fn remove(&mut self, node_id: u64) {
let inode = self.inodes_by_node_id.remove(node_id).unwrap();
if let Some(inodes_by_inode_nr) = self.inodes_by_inode_nr.as_mut() {
inodes_by_inode_nr.remove(&inode.inode_nr());
}
}
pub fn clear(&mut self) {
let root_inode = Arc::clone(self.inodes_by_node_id.get(FUSE_ROOT_ID).unwrap());
self.inodes_by_node_id.clear();
assert!(self.inodes_by_node_id.insert(Arc::clone(&root_inode)) == FUSE_ROOT_ID);
if let Some(inodes_by_inode_nr) = self.inodes_by_inode_nr.as_mut() {
inodes_by_inode_nr.clear();
inodes_by_inode_nr.insert(root_inode.inode_nr(), (root_inode, FUSE_ROOT_ID));
}
}
}