ClSlaid 075a877284
patch: increase FUSE block size and add retry
1. this should make file managers read small file in one request more
   likely
2. implemented retry, max times 3

Signed-off-by: ClSlaid <cailue@bupt.edu.cn>
2023-10-28 11:14:16 +08:00

1170 lines
34 KiB
Rust

//! fuse server implement
//! we use fuse to provide file readers, warping data transfer to file interfaces
//!
//! # Name encoding
//!
//! There are different collection of characters forbidden in file names on different platforms:
//! - windows: `\/:*?"<>|`
//! - macos: `:/`
//! - linux: `/`
//!
//! what makes it troublesome is windows also used '\' as path separator.
//!
//! For now, we transfer all file names with windows separators, UTF-16 encoded.
//! *Need a way to transfer file names with '\' safely*.
//! Maybe we can use URL encoded file names and '/' seperators as a new standard, while keep the support to old schemes.
//!
//! # Note
//! - all files on FS should be read only, and mark the owner to be the current user
//! - any write operations, hard links, and symbolic links on the FS should be denied
use std::{
collections::{BTreeMap, HashMap},
ffi::OsString,
path::{Path, PathBuf},
sync::{
atomic::{AtomicBool, AtomicU64, Ordering},
mpsc::{Receiver, Sender},
Arc,
},
time::{Duration, SystemTime},
};
use fuser::{ReplyDirectory, FUSE_ROOT_ID};
use hbb_common::{
bytes::{Buf, Bytes},
log,
};
use parking_lot::{Condvar, Mutex};
use utf16string::WStr;
use crate::{send_data, ClipboardFile, CliprdrError};
#[cfg(target_os = "linux")]
use super::LDAP_EPOCH_DELTA;
/// fuse server ready retry max times
const READ_RETRY: i32 = 3;
/// block size for fuse, align to our asynchronic request size over FileContentsRequest.
const BLOCK_SIZE: u32 = 4 * 1024 * 1024;
/// read only permission
const PERM_READ: u16 = 0o444;
/// read and write permission
const PERM_RW: u16 = 0o644;
/// only self can read and readonly
const PERM_SELF_RO: u16 = 0o400;
/// rwx
const PERM_RWX: u16 = 0o755;
/// max length of file name
const MAX_NAME_LEN: usize = 255;
/// fuse client
/// this is a proxy to the fuse server
#[derive(Debug)]
pub struct FuseClient {
server: Arc<Mutex<FuseServer>>,
}
impl fuser::Filesystem for FuseClient {
fn init(
&mut self,
req: &fuser::Request<'_>,
config: &mut fuser::KernelConfig,
) -> Result<(), libc::c_int> {
let mut server = self.server.lock();
server.init(req, config)
}
fn lookup(
&mut self,
req: &fuser::Request<'_>,
parent: u64,
name: &std::ffi::OsStr,
reply: fuser::ReplyEntry,
) {
let mut server = self.server.lock();
server.lookup(req, parent, name, reply)
}
fn opendir(&mut self, req: &fuser::Request<'_>, ino: u64, flags: i32, reply: fuser::ReplyOpen) {
let mut server = self.server.lock();
server.opendir(req, ino, flags, reply)
}
fn readdir(
&mut self,
req: &fuser::Request<'_>,
ino: u64,
fh: u64,
offset: i64,
reply: fuser::ReplyDirectory,
) {
let mut server = self.server.lock();
server.readdir(req, ino, fh, offset, reply)
}
fn releasedir(
&mut self,
req: &fuser::Request<'_>,
ino: u64,
fh: u64,
_flags: i32,
reply: fuser::ReplyEmpty,
) {
let mut server = self.server.lock();
server.releasedir(req, ino, fh, _flags, reply)
}
fn open(&mut self, req: &fuser::Request<'_>, ino: u64, flags: i32, reply: fuser::ReplyOpen) {
let mut server = self.server.lock();
server.open(req, ino, flags, reply)
}
fn read(
&mut self,
req: &fuser::Request<'_>,
ino: u64,
fh: u64,
offset: i64,
size: u32,
flags: i32,
lock_owner: Option<u64>,
reply: fuser::ReplyData,
) {
let mut server = self.server.lock();
server.read(req, ino, fh, offset, size, flags, lock_owner, reply)
}
fn release(
&mut self,
req: &fuser::Request<'_>,
ino: u64,
fh: u64,
_flags: i32,
_lock_owner: Option<u64>,
_flush: bool,
reply: fuser::ReplyEmpty,
) {
let mut server = self.server.lock();
server.release(req, ino, fh, _flags, _lock_owner, _flush, reply)
}
fn getattr(&mut self, req: &fuser::Request<'_>, ino: u64, reply: fuser::ReplyAttr) {
let mut server = self.server.lock();
server.getattr(req, ino, reply)
}
}
/// fuse server
/// provides a read-only file system
#[derive(Debug)]
pub(crate) struct FuseServer {
generation: AtomicU64,
files: Vec<FuseNode>,
// file handle counter
file_handle_counter: AtomicU64,
// timeout
timeout: Duration,
// file read reply channel
rx: Receiver<ClipboardFile>,
}
impl FuseServer {
/// create a new fuse server
pub fn new(timeout: Duration) -> (Self, Sender<ClipboardFile>) {
let (tx, rx) = std::sync::mpsc::channel();
(
Self {
generation: AtomicU64::new(0),
files: Vec::new(),
file_handle_counter: AtomicU64::new(0),
timeout,
rx,
},
tx,
)
}
pub fn client(server: Arc<Mutex<Self>>) -> FuseClient {
FuseClient { server }
}
}
impl FuseServer {
pub fn load_file_list(&mut self, files: Vec<FileDescription>) -> Result<(), CliprdrError> {
let tree = FuseNode::build_tree(files)?;
self.files = tree;
self.generation.fetch_add(1, Ordering::Relaxed);
Ok(())
}
}
impl fuser::Filesystem for FuseServer {
fn init(
&mut self,
_req: &fuser::Request<'_>,
_config: &mut fuser::KernelConfig,
) -> Result<(), libc::c_int> {
if self.files.is_empty() {
// create a root file
let root = FuseNode::new_root();
self.files.push(root);
}
Ok(())
}
fn lookup(
&mut self,
_req: &fuser::Request<'_>,
parent: u64,
name: &std::ffi::OsStr,
reply: fuser::ReplyEntry,
) {
if name.len() > MAX_NAME_LEN {
log::debug!("fuse: name too long");
reply.error(libc::ENAMETOOLONG);
return;
}
let entries = &self.files;
let generation = self.generation.load(Ordering::Relaxed);
let parent_entry = match entries.get(parent as usize - 1) {
Some(f) => f,
None => {
log::error!("fuse: parent not found");
reply.error(libc::ENOENT);
return;
}
};
if parent_entry.attributes.kind != FileType::Directory {
log::error!("fuse: parent is not a directory");
reply.error(libc::ENOTDIR);
return;
}
let children_inodes = &parent_entry.children;
for inode in children_inodes.iter().copied() {
let child = &entries[inode as usize - 1];
let entry_name = OsString::from(&child.name);
if &entry_name.as_os_str() == &name {
let ttl = std::time::Duration::new(0, 0);
reply.entry(&ttl, &(&child.attributes).into(), generation);
log::debug!("fuse: found child");
return;
}
}
// error
reply.error(libc::ENOENT);
log::debug!("fuse: child not found");
}
fn opendir(
&mut self,
_req: &fuser::Request<'_>,
ino: u64,
_flags: i32,
reply: fuser::ReplyOpen,
) {
let files = &self.files;
let Some(entry) = files.get(ino as usize - 1) else {
reply.error(libc::ENOENT);
log::error!("fuse: opendir: entry not found");
return;
};
if entry.attributes.kind != FileType::Directory {
reply.error(libc::ENOTDIR);
log::error!("fuse: opendir: entry is not a directory");
return;
}
// in gc, deny open
if entry.marked() {
log::error!("fuse: opendir: entry is in gc");
reply.error(libc::EBUSY);
return;
}
let fh = self.alloc_fd();
entry.add_handler(fh);
reply.opened(fh, 0);
}
fn readdir(
&mut self,
_req: &fuser::Request<'_>,
ino: u64,
fh: u64,
offset: i64,
mut reply: ReplyDirectory,
) {
let files = &self.files;
let Some(entry) = files.get(ino as usize - 1) else {
reply.error(libc::ENOENT);
log::error!("fuse: readdir: entry not found");
return;
};
if !entry.have_handler(fh) {
reply.error(libc::EBADF);
log::error!("fuse: readdir: entry has no such handler");
return;
}
if entry.attributes.kind != FileType::Directory {
reply.error(libc::ENOTDIR);
log::error!("fuse: readdir: entry is not a directory");
return;
}
let offset = offset as usize;
let mut entries = Vec::new();
let self_entry = (ino, FileType::Directory, OsString::from("."));
entries.push(self_entry);
if let Some(parent_inode) = entry.parent {
entries.push((parent_inode, FileType::Directory, OsString::from("..")));
}
for inode in entry.children.iter().copied() {
let child = &files[inode as usize - 1];
let kind = child.attributes.kind;
let name = OsString::from(&child.name);
let child_entry = (inode, kind, name.to_owned());
entries.push(child_entry);
}
for (i, entry) in entries.into_iter().enumerate().skip(offset) {
if reply.add(entry.0, i as i64 + 1, entry.1.into(), entry.2) {
break;
}
}
reply.ok();
}
fn releasedir(
&mut self,
_req: &fuser::Request<'_>,
ino: u64,
fh: u64,
_flags: i32,
reply: fuser::ReplyEmpty,
) {
let files = &self.files;
let Some(entry) = files.get(ino as usize - 1) else {
reply.error(libc::ENOENT);
log::error!("fuse: releasedir: entry not found");
return;
};
if entry.attributes.kind != FileType::Directory {
reply.error(libc::ENOTDIR);
log::error!("fuse: releasedir: entry is not a directory");
return;
}
if !entry.have_handler(fh) {
reply.error(libc::EBADF);
log::error!("fuse: releasedir: entry has no such handler");
return;
}
let _ = entry.unregister_handler(fh);
reply.ok();
}
fn open(&mut self, _req: &fuser::Request<'_>, ino: u64, _flags: i32, reply: fuser::ReplyOpen) {
let files = &self.files;
let Some(entry) = files.get(ino as usize - 1) else {
reply.error(libc::ENOENT);
log::error!("fuse: open: entry not found");
return;
};
// todo: support link file
if entry.attributes.kind != FileType::File {
reply.error(libc::ENFILE);
log::error!("fuse: open: entry is not a file");
return;
}
// check gc
if entry.marked() {
reply.error(libc::EBUSY);
log::error!("fuse: open: entry is in gc");
return;
}
let fh = self.alloc_fd();
entry.add_handler(fh);
reply.opened(fh, 0);
}
// todo: implement retry
fn read(
&mut self,
_req: &fuser::Request<'_>,
ino: u64,
fh: u64,
offset: i64,
size: u32,
_flags: i32,
_lock_owner: Option<u64>,
reply: fuser::ReplyData,
) {
let files = &self.files;
let Some(entry) = files.get(ino as usize - 1) else {
reply.error(libc::ENOENT);
log::error!("fuse: read: entry not found");
return;
};
if !entry.have_handler(fh) {
reply.error(libc::EBADF);
log::error!("fuse: read: entry has no such handler");
return;
}
if entry.attributes.kind != FileType::File {
reply.error(libc::ENFILE);
log::error!("fuse: read: entry is not a file");
return;
}
if entry.marked() {
reply.error(libc::EBUSY);
log::error!("fuse: read: entry is in gc");
return;
}
let bytes = match self.read_node(entry, offset, size) {
Ok(b) => b,
Err(e) => {
log::error!("failed to read entry: {:?}", e);
reply.error(libc::EIO);
return;
}
};
reply.data(bytes.as_slice());
}
fn release(
&mut self,
_req: &fuser::Request<'_>,
ino: u64,
fh: u64,
_flags: i32,
_lock_owner: Option<u64>,
_flush: bool,
reply: fuser::ReplyEmpty,
) {
let files = &self.files;
let Some(entry) = files.get(ino as usize - 1) else {
reply.error(libc::ENOENT);
log::error!("fuse: release: entry not found");
return;
};
if entry.unregister_handler(fh).is_err() {
reply.error(libc::EBADF);
log::error!("fuse: release: entry has no such handler");
return;
}
reply.ok();
}
fn getattr(&mut self, _req: &fuser::Request<'_>, ino: u64, reply: fuser::ReplyAttr) {
let files = &self.files;
let Some(entry) = files.get(ino as usize - 1) else {
reply.error(libc::ENOENT);
log::error!("fuse: getattr: entry not found");
return;
};
let attr = (&entry.attributes).into();
reply.attr(&std::time::Duration::default(), &attr)
}
}
impl FuseServer {
// get files and directory path right in root of FUSE fs
pub fn list_root(&self) -> Vec<PathBuf> {
let files = &self.files;
let children = &files[0].children;
let mut paths = Vec::with_capacity(children.len());
for inode in children.iter().copied() {
let idx = inode as usize - 1;
paths.push(PathBuf::from(&files[idx].name));
}
paths
}
/// allocate a new file descriptor
fn alloc_fd(&self) -> u64 {
self.file_handle_counter.fetch_add(1, Ordering::Relaxed)
}
fn read_node(
&self,
node: &FuseNode,
offset: i64,
size: u32,
) -> Result<Vec<u8>, std::io::Error> {
// todo: async and concurrent read, generate stream_id per request
log::debug!(
"reading {:?} offset {} size {} on stream: {}",
node.name,
offset,
size,
node.stream_id
);
let cb_requested = unsafe {
// convert `size` from u32 to i32
// yet with same bit representation
std::mem::transmute::<u32, i32>(size)
};
let (n_position_high, n_position_low) =
((offset >> 32) as i32, (offset & (u32::MAX as i64)) as i32);
let request = ClipboardFile::FileContentsRequest {
stream_id: node.stream_id,
list_index: node.index as i32,
dw_flags: 2,
n_position_low,
n_position_high,
cb_requested,
have_clip_data_id: false,
clip_data_id: 0,
};
send_data(node.conn_id, request.clone());
log::debug!(
"waiting for read reply for {:?} on stream: {}",
node.name,
node.stream_id
);
let mut retry_times = 0;
loop {
let reply = self.rx.recv_timeout(self.timeout).map_err(|e| {
log::error!("failed to receive file list from channel: {:?}", e);
std::io::Error::new(std::io::ErrorKind::TimedOut, e)
})?;
match reply {
ClipboardFile::FileContentsResponse {
msg_flags,
stream_id,
requested_data,
} => {
if stream_id != node.stream_id {
log::debug!("stream id mismatch, ignore");
continue;
}
if msg_flags & 1 == 0 {
retry_times += 1;
if retry_times > READ_RETRY {
return Err(std::io::Error::new(
std::io::ErrorKind::Other,
"failure request",
));
}
send_data(node.conn_id, request.clone());
continue;
}
return Ok(requested_data);
}
_ => {
return Err(std::io::Error::new(
std::io::ErrorKind::Other,
"invalid reply",
))
}
}
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct FileDescription {
pub conn_id: i32,
pub name: PathBuf,
pub kind: FileType,
pub atime: SystemTime,
pub last_modified: SystemTime,
pub last_metadata_changed: SystemTime,
pub creation_time: SystemTime,
pub size: u64,
pub perm: u16,
}
impl FileDescription {
fn parse_file_descriptor(
bytes: &mut Bytes,
conn_id: i32,
) -> Result<FileDescription, CliprdrError> {
let flags = bytes.get_u32_le();
// skip reserved 32 bytes
bytes.advance(32);
let attributes = bytes.get_u32_le();
// in original specification, this is 16 bytes reserved
// we use the last 4 bytes to store the file mode
// skip reserved 12 bytes
bytes.advance(12);
let perm = bytes.get_u32_le() as u16;
// last write time from 1601-01-01 00:00:00, in 100ns
let last_write_time = bytes.get_u64_le();
// file size
let file_size_high = bytes.get_u32_le();
let file_size_low = bytes.get_u32_le();
// utf16 file name, double \0 terminated, in 520 bytes block
// read with another pointer, and advance the main pointer
let block = bytes.clone();
bytes.advance(520);
let block = &block[..520];
let wstr = WStr::from_utf16le(block).map_err(|e| {
log::error!("cannot convert file descriptor path: {:?}", e);
CliprdrError::ConversionFailure
})?;
let from_unix = flags & 0x08 != 0;
let valid_attributes = flags & 0x04 != 0;
if !valid_attributes {
return Err(CliprdrError::InvalidRequest {
description: "file description must have valid attributes".to_string(),
});
}
// todo: check normal, hidden, system, readonly, archive...
let directory = attributes & 0x10 != 0;
let normal = attributes == 0x80;
let hidden = attributes & 0x02 != 0;
let readonly = attributes & 0x01 != 0;
let perm = if from_unix {
// as is
perm
// cannot set as is...
} else if normal {
PERM_RWX
} else if readonly {
PERM_READ
} else if hidden {
PERM_SELF_RO
} else if directory {
PERM_RWX
} else {
PERM_RW
};
let kind = if directory {
FileType::Directory
} else {
FileType::File
};
let valid_size = flags & 0x40 != 0;
let size = if valid_size {
((file_size_high as u64) << 32) + file_size_low as u64
} else {
0
};
let valid_write_time = flags & 0x20 != 0;
let last_modified = if valid_write_time && last_write_time >= LDAP_EPOCH_DELTA {
let last_write_time = (last_write_time - LDAP_EPOCH_DELTA) * 100;
let last_write_time = Duration::from_nanos(last_write_time);
SystemTime::UNIX_EPOCH + last_write_time
} else {
SystemTime::UNIX_EPOCH
};
let name = wstr.to_utf8().replace('\\', "/");
let name = PathBuf::from(name.trim_end_matches('\0'));
let desc = FileDescription {
conn_id,
name,
kind,
atime: last_modified,
last_modified,
last_metadata_changed: last_modified,
creation_time: last_modified,
size,
perm,
};
Ok(desc)
}
/// parse file descriptions from a format data response PDU
/// which containing a CSPTR_FILEDESCRIPTORW indicated format data
pub fn parse_file_descriptors(
file_descriptor_pdu: Vec<u8>,
conn_id: i32,
) -> Result<Vec<Self>, CliprdrError> {
let mut data = Bytes::from(file_descriptor_pdu);
if data.remaining() < 4 {
return Err(CliprdrError::InvalidRequest {
description: "file descriptor request with infficient length".to_string(),
});
}
let count = data.get_u32_le() as usize;
if data.remaining() == 0 && count == 0 {
return Ok(Vec::new());
}
if data.remaining() != 592 * count {
return Err(CliprdrError::InvalidRequest {
description: "file descriptor request with invalid length".to_string(),
});
}
let mut files = Vec::with_capacity(count);
for _ in 0..count {
let desc = Self::parse_file_descriptor(&mut data, conn_id)?;
files.push(desc);
}
Ok(files)
}
}
/// a node in the FUSE file tree
#[derive(Debug)]
struct FuseNode {
/// connection id
pub conn_id: i32,
// todo: use stream_id to identify a FileContents request-reply
// instead of a whole file
/// stream id
pub stream_id: i32,
/// file index in peer's file list
/// NOTE:
/// it is NOT the same as inode, this is the index in the file list
pub index: usize,
/// parent inode
pub parent: Option<u64>,
/// file name
pub name: String,
/// file attributes
pub attributes: InodeAttributes,
/// children inodes
pub children: Vec<Inode>,
/// marked gc
pub file_handlers: FileHandles,
}
impl FuseNode {
pub fn from_description(inode: Inode, desc: FileDescription) -> Self {
Self {
conn_id: desc.conn_id,
stream_id: rand::random(),
index: inode as usize - 2,
name: desc.name.to_str().unwrap().to_owned(),
parent: None,
attributes: InodeAttributes::from_description(inode, desc),
children: Vec::new(),
file_handlers: FileHandles::new(),
}
}
pub fn new_root() -> Self {
Self {
conn_id: 0,
stream_id: rand::random(),
index: 0,
name: String::from("/"),
parent: None,
attributes: InodeAttributes::new_root(),
children: Vec::new(),
file_handlers: FileHandles::new(),
}
}
#[allow(unused)]
pub fn is_file(&self) -> bool {
self.attributes.kind == FileType::File
}
pub fn marked(&self) -> bool {
self.file_handlers.marked()
}
pub fn add_handler(&self, fh: u64) {
self.file_handlers.add_handler(fh)
}
pub fn unregister_handler(&self, fh: u64) -> Result<(), std::io::Error> {
self.file_handlers.unregister(fh)
}
pub fn have_handler(&self, fh: u64) -> bool {
self.file_handlers.have_handler(fh)
}
/// add a child inode
fn add_child(&mut self, inode: Inode) {
self.children.push(inode);
}
/// calculate the file tree from a pre-ordered file list
/// ## implement detail:
/// - a new root entry will be prepended to the list
/// - all file names will be trimed to the last component
pub fn build_tree(files: Vec<FileDescription>) -> Result<Vec<Self>, CliprdrError> {
// capacity set to file count + 1 (root)
let mut tree_list = Vec::with_capacity(files.len() + 1);
let root = Self::new_root();
tree_list.push(root);
// build the tree first
// root map, name -> inode
let mut sub_root_map = HashMap::new();
sub_root_map.insert(Path::new("/").to_path_buf(), FUSE_ROOT_ID);
sub_root_map.insert(Path::new("").to_path_buf(), FUSE_ROOT_ID);
for file in files.into_iter() {
let name = file.name.clone();
let inode = tree_list.len() as u64 + FUSE_ROOT_ID;
let parent_inode = match name.parent() {
Some(parent) => sub_root_map.get(parent).copied().unwrap_or(FUSE_ROOT_ID),
None => {
// parent should be root
FUSE_ROOT_ID
}
};
tree_list[parent_inode as usize - 1].add_child(inode);
let base_name = name.file_name().ok_or_else(|| {
let err = std::io::Error::new(
std::io::ErrorKind::InvalidData,
format!("invalid file name {}", file.name.display()),
);
CliprdrError::FileError {
path: file.name.clone(),
err,
}
})?;
let mut desc = file.clone();
if desc.kind == FileType::Directory {
sub_root_map.insert(desc.name.clone(), inode);
}
desc.name = Path::new(base_name).to_path_buf();
let mut fuse_node = FuseNode::from_description(inode, desc);
fuse_node.parent = Some(parent_inode);
tree_list.push(fuse_node);
}
Ok(tree_list)
}
}
pub type Inode = u64;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FileType {
File,
Directory,
// todo: support symlink
Symlink,
}
impl From<FileType> for fuser::FileType {
fn from(value: FileType) -> Self {
match value {
FileType::File => Self::RegularFile,
FileType::Directory => Self::Directory,
FileType::Symlink => Self::Symlink,
}
}
}
#[derive(Debug, Clone)]
pub struct InodeAttributes {
inode: Inode,
size: u64,
// file reference meta
// should be the only mutable field in this struct
last_accessed: std::time::SystemTime,
last_modified: std::time::SystemTime,
last_metadata_changed: std::time::SystemTime,
creation_time: std::time::SystemTime,
kind: FileType,
perm: u16,
// not implemented
_xattrs: BTreeMap<Vec<u8>, Vec<u8>>,
}
impl InodeAttributes {
pub fn new(inode: u64, size: u64, perm: u16, kind: FileType) -> Self {
Self {
inode,
size,
last_accessed: std::time::SystemTime::now(),
last_modified: std::time::SystemTime::now(),
last_metadata_changed: std::time::SystemTime::now(),
creation_time: std::time::SystemTime::now(),
kind,
perm,
_xattrs: BTreeMap::new(),
}
}
pub fn from_description(inode: u64, desc: FileDescription) -> Self {
Self {
inode,
size: desc.size,
last_modified: desc.last_modified,
last_metadata_changed: desc.last_metadata_changed,
creation_time: desc.creation_time,
last_accessed: SystemTime::now(),
kind: desc.kind,
perm: desc.perm,
_xattrs: BTreeMap::new(),
}
}
pub fn new_root() -> Self {
Self::new(FUSE_ROOT_ID, 0, PERM_RWX, FileType::Directory)
}
pub fn access(&mut self) {
self.last_accessed = std::time::SystemTime::now();
}
}
impl From<&InodeAttributes> for fuser::FileAttr {
fn from(value: &InodeAttributes) -> Self {
let blocks = if value.size % BLOCK_SIZE as u64 == 0 {
value.size / BLOCK_SIZE as u64
} else {
value.size / BLOCK_SIZE as u64 + 1
};
Self {
ino: value.inode,
size: value.size,
blocks,
atime: value.last_accessed,
mtime: value.last_modified,
ctime: value.last_metadata_changed,
crtime: value.creation_time,
kind: value.kind.into(),
// read only
perm: value.perm,
nlink: 1,
// set to current user
uid: unsafe { libc::getuid() },
// set to current user
gid: unsafe { libc::getgid() },
rdev: 0,
blksize: BLOCK_SIZE,
// todo: support macos flags
flags: 0,
}
}
}
#[derive(Debug)]
struct FileHandles {
waiter: Condvar,
handlers: Mutex<Vec<u64>>,
gc: AtomicBool,
}
impl FileHandles {
pub fn new() -> Self {
Self {
waiter: Condvar::new(),
// the vector in handlers is sorted, from small to big
// prove:
// - later allocated handler will be bigger than previous ones
// - new handlers will append to the end of the vector
// - dropping old handlers won't affect the ordering
handlers: Mutex::new(Vec::new()),
gc: AtomicBool::new(false),
}
}
pub fn add_handler(&self, fh: u64) {
if self.marked() {
panic!("adding new handler to a marked ref counter");
}
self.handlers.lock().push(fh);
}
pub fn marked(&self) -> bool {
self.gc.load(Ordering::Relaxed)
}
pub fn have_handler(&self, handler: u64) -> bool {
let handlers = self.handlers.lock();
handlers.binary_search(&handler).is_ok()
}
pub fn unregister(&self, handler: u64) -> Result<(), std::io::Error> {
let mut handlers = self.handlers.lock();
let Ok(idx) = handlers.binary_search(&handler) else {
let e = std::io::Error::new(std::io::ErrorKind::InvalidInput, "invalid handler");
return Err(e);
};
handlers.remove(idx);
self.waiter.notify_all();
Ok(())
}
}
#[cfg(test)]
mod fuse_test {
use std::str::FromStr;
use super::*;
// todo: more tests needed!
fn desc_gen(name: &str, kind: FileType) -> FileDescription {
FileDescription {
conn_id: 0,
name: PathBuf::from(name),
kind,
atime: SystemTime::UNIX_EPOCH,
last_modified: SystemTime::UNIX_EPOCH,
last_metadata_changed: SystemTime::UNIX_EPOCH,
creation_time: SystemTime::UNIX_EPOCH,
size: 0,
perm: 0,
}
}
fn generate_descriptions(prefix: &str) -> Vec<FileDescription> {
let (d0_path, f0_path, f1_path, d1_path, f2_path, f3_path) = if prefix.is_empty() {
(
"folder0".to_string(),
"folder0/file0".to_string(),
"folder0/file1".to_string(),
"folder1".to_string(),
"folder1/file2".to_string(),
"folder1/📄3".to_string(),
)
} else {
(
format!("{}/folder0", prefix),
format!("{}/folder0/file0", prefix),
format!("{}/folder0/file1", prefix),
format!("{}/folder1", prefix),
format!("{}/folder1/file2", prefix),
format!("{}/folder1/📄3", prefix),
)
};
let folder0 = desc_gen(&d0_path, FileType::Directory);
let file0 = desc_gen(&f0_path, FileType::File);
let file1 = desc_gen(&f1_path, FileType::File);
let folder1 = desc_gen(&d1_path, FileType::Directory);
let file2 = desc_gen(&f2_path, FileType::File);
let file3 = desc_gen(&f3_path, FileType::File);
vec![folder0, file0, file1, folder1, file2, file3]
}
fn build_tree(prefix: &str) {
let source_list = generate_descriptions(prefix);
let build_res = FuseNode::build_tree(source_list);
assert!(build_res.is_ok());
let tree_list = build_res.unwrap();
assert_eq!(tree_list.len(), 7);
assert_eq!(tree_list[0].name, "/");
assert_eq!(tree_list[1].name, "folder0");
assert_eq!(tree_list[2].name, "file0");
assert_eq!(tree_list[3].name, "file1");
assert_eq!(tree_list[4].name, "folder1");
assert_eq!(tree_list[5].name, "file2");
assert_eq!(tree_list[6].name, "📄3");
assert_eq!(tree_list[0].children, vec![2, 5]);
assert_eq!(tree_list[1].children, vec![3, 4]);
assert!(tree_list[2].children.is_empty());
assert!(tree_list[3].children.is_empty());
assert_eq!(tree_list[4].children, vec![6, 7]);
assert!(tree_list[5].children.is_empty());
assert!(tree_list[6].children.is_empty());
for (idx, node) in tree_list.iter().skip(1).enumerate() {
assert_eq!(idx, node.index)
}
}
fn build_single_file(prefix: &str) {
let raw_name = "衬衫的价格为 9 镑 15 便士.txt";
let f_name = if prefix == "" {
raw_name.to_string()
} else {
prefix.to_string() + "/" + raw_name
};
let desc = desc_gen(&f_name, FileType::File);
let tree = FuseNode::build_tree(vec![desc]).unwrap();
assert_eq!(tree.len(), 2);
assert_eq!(tree[0].name, "/");
assert_eq!(tree[0].children, vec![2]);
assert_eq!(tree[1].name, raw_name);
assert_eq!(tree[1].index, 0);
assert_eq!(tree[1].attributes.kind, FileType::File);
}
#[test]
fn test_parse_single() {
build_single_file("");
build_single_file("/");
build_single_file("test");
build_single_file("/test");
build_single_file("🗂");
build_single_file("/🗂");
}
#[test]
fn test_parse_tree() {
build_tree("");
build_tree("/");
build_tree("test");
build_tree("/test");
build_tree("/test/test");
build_tree("🗂");
build_tree("/🗂");
build_tree("🗂/test");
}
}