2019-08-08 10:49:18 +00:00
|
|
|
//! Low level FUSE implementation for pxar.
|
|
|
|
//!
|
|
|
|
//! Allows to mount the archive as read-only filesystem to inspect its contents.
|
|
|
|
|
2019-08-22 12:46:23 +00:00
|
|
|
use std::ffi::{CString, OsStr};
|
2019-08-08 10:49:18 +00:00
|
|
|
use std::fs::File;
|
2019-08-13 13:37:46 +00:00
|
|
|
use std::io::BufReader;
|
2019-08-08 10:49:18 +00:00
|
|
|
use std::os::unix::ffi::OsStrExt;
|
|
|
|
use std::path::Path;
|
2019-08-13 13:37:46 +00:00
|
|
|
use std::sync::Mutex;
|
2019-08-08 10:49:18 +00:00
|
|
|
|
2019-08-22 12:46:23 +00:00
|
|
|
use failure::{bail, format_err, Error};
|
|
|
|
use libc::{c_char, c_int, c_void, size_t};
|
2019-08-08 10:49:18 +00:00
|
|
|
use libc;
|
|
|
|
|
2019-08-13 13:37:46 +00:00
|
|
|
use super::decoder::Decoder;
|
2019-09-03 12:14:30 +00:00
|
|
|
use super::format_definition::PxarGoodbyeItem;
|
2019-08-13 13:37:46 +00:00
|
|
|
|
|
|
|
/// Node ID of the root inode
|
|
|
|
/// This is the only one whose ID is not equal to the offset in the file.
|
|
|
|
/// This is ok since offset 1 is part of the entry header and will therefore
|
|
|
|
/// not occur again, but remapping to the correct offset of 0 is required.
|
2019-09-03 12:14:30 +00:00
|
|
|
const FUSE_ROOT_ID: u64 = 1;
|
2019-08-13 13:37:46 +00:00
|
|
|
|
|
|
|
fn decoder_callback(path: &Path) -> Result<(), Error> {
|
|
|
|
println!("{:#?}", path);
|
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
|
|
|
|
/// FFI types for easier readability
|
|
|
|
type Request = *mut c_void;
|
|
|
|
type MutPtr = *mut c_void;
|
|
|
|
type ConstPtr = *const c_void;
|
|
|
|
type StrPtr = *const c_char;
|
|
|
|
|
2019-08-08 10:49:18 +00:00
|
|
|
#[link(name = "fuse3")]
|
|
|
|
extern "C" {
|
2019-08-22 12:46:23 +00:00
|
|
|
fn fuse_session_new(
|
|
|
|
args: *const FuseArgs,
|
|
|
|
oprs: *const Operations,
|
|
|
|
size: size_t,
|
|
|
|
op: ConstPtr,
|
|
|
|
) -> MutPtr;
|
2019-08-13 13:37:46 +00:00
|
|
|
fn fuse_set_signal_handlers(session: ConstPtr) -> c_int;
|
|
|
|
fn fuse_remove_signal_handlers(session: ConstPtr);
|
2019-08-08 10:49:18 +00:00
|
|
|
fn fuse_daemonize(foreground: c_int) -> c_int;
|
2019-08-13 13:37:46 +00:00
|
|
|
fn fuse_session_mount(session: ConstPtr, mountpoint: StrPtr) -> c_int;
|
|
|
|
fn fuse_session_unmount(session: ConstPtr);
|
|
|
|
fn fuse_session_loop(session: ConstPtr) -> c_int;
|
2019-08-22 14:39:04 +00:00
|
|
|
fn fuse_session_loop_mt_31(session: ConstPtr, clone_fd: c_int) -> c_int;
|
2019-08-13 13:37:46 +00:00
|
|
|
fn fuse_session_destroy(session: ConstPtr);
|
2019-08-22 12:46:23 +00:00
|
|
|
// fn fuse_reply_attr(req: Request, attr: *const libc::stat, timeout: f64) -> c_int;
|
2019-08-13 13:37:46 +00:00
|
|
|
fn fuse_reply_err(req: Request, errno: c_int) -> c_int;
|
|
|
|
fn fuse_req_userdata(req: Request) -> MutPtr;
|
2019-08-08 10:49:18 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/// Command line arguments passed to fuse.
|
|
|
|
#[repr(C)]
|
|
|
|
#[derive(Debug)]
|
|
|
|
struct FuseArgs {
|
|
|
|
argc: c_int,
|
2019-08-13 13:37:46 +00:00
|
|
|
argv: *const StrPtr,
|
2019-08-08 10:49:18 +00:00
|
|
|
allocated: c_int,
|
|
|
|
}
|
|
|
|
|
|
|
|
/// `Session` stores a pointer to the session context and is used to mount the
|
|
|
|
/// archive to the given mountpoint.
|
|
|
|
pub struct Session {
|
2019-08-13 13:37:46 +00:00
|
|
|
ptr: MutPtr,
|
2019-08-08 10:49:18 +00:00
|
|
|
verbose: bool,
|
|
|
|
}
|
|
|
|
|
|
|
|
/// `Operations` defines the callback function table of supported operations.
|
|
|
|
#[repr(C)]
|
2019-08-13 13:37:46 +00:00
|
|
|
#[derive(Default)]
|
2019-08-22 12:46:23 +00:00
|
|
|
#[rustfmt::skip]
|
2019-08-08 10:49:18 +00:00
|
|
|
struct Operations {
|
2019-08-13 13:37:46 +00:00
|
|
|
// The order in which the functions are listed matters, as the offset in the
|
|
|
|
// struct defines what function the fuse driver uses.
|
|
|
|
// It should therefore not be altered!
|
|
|
|
init: Option<extern fn(userdata: MutPtr)>,
|
|
|
|
destroy: Option<extern fn(userdata: MutPtr)>,
|
|
|
|
lookup: Option<extern fn(req: Request, parent: u64, name: StrPtr)>,
|
|
|
|
forget: Option<extern fn(req: Request, inode: u64, nlookup: u64)>,
|
|
|
|
getattr: Option<extern fn(req: Request, inode: u64, fileinfo: MutPtr)>,
|
|
|
|
setattr: Option<extern fn(req: Request, inode: u64, attr: MutPtr, to_set: c_int, fileinfo: MutPtr)>,
|
|
|
|
readlink: Option<extern fn(req: Request, inode: u64)>,
|
|
|
|
mknod: Option<extern fn(req: Request, parent: u64, name: StrPtr, mode: c_int, rdev: c_int)>,
|
|
|
|
mkdir: Option<extern fn(req: Request, parent: u64, name: StrPtr, mode: c_int)>,
|
|
|
|
unlink: Option<extern fn(req: Request, parent: u64, name: StrPtr)>,
|
|
|
|
rmdir: Option<extern fn(req: Request, parent: u64, name: StrPtr)>,
|
|
|
|
symlink: Option<extern fn(req: Request, link: StrPtr, parent: u64, name: StrPtr)>,
|
|
|
|
rename: Option<extern fn(req: Request, parent: u64, name: StrPtr, newparent: u64, newname: StrPtr, flags: c_int)>,
|
|
|
|
link: Option<extern fn(req: Request, inode: u64, newparent: u64, newname: StrPtr)>,
|
|
|
|
open: Option<extern fn(req: Request, indoe: u64, fileinfo: MutPtr)>,
|
|
|
|
read: Option<extern fn(req: Request, inode: u64, size: size_t, offset: c_int, fileinfo: MutPtr)>,
|
|
|
|
write: Option<extern fn(req: Request, inode: u64, buffer: StrPtr, size: size_t, offset: c_void, fileinfo: MutPtr)>,
|
|
|
|
flush: Option<extern fn(req: Request, inode: u64, fileinfo: MutPtr)>,
|
|
|
|
release: Option<extern fn(req: Request, inode: u64, fileinfo: MutPtr)>,
|
|
|
|
fsync: Option<extern fn(req: Request, inode: u64, datasync: c_int, fileinfo: MutPtr)>,
|
|
|
|
opendir: Option<extern fn(req: Request, inode: u64, fileinfo: MutPtr)>,
|
|
|
|
readdir: Option<extern fn(req: Request, inode: u64, size: size_t, offset: c_int, fileinfo: MutPtr)>,
|
|
|
|
releasedir: Option<extern fn(req: Request, inode: u64, fileinfo: MutPtr)>,
|
|
|
|
fsyncdir: Option<extern fn(req: Request, inode: u64, datasync: c_int, fileinfo: MutPtr)>,
|
|
|
|
statfs: Option<extern fn(req: Request, inode: u64)>,
|
|
|
|
setxattr: Option<extern fn(req: Request, inode: u64, name: StrPtr, value: StrPtr, size: size_t, flags: c_int)>,
|
|
|
|
getxattr: Option<extern fn(req: Request, inode: u64, name: StrPtr, size: size_t)>,
|
|
|
|
listxattr: Option<extern fn(req: Request, inode: u64, size: size_t)>,
|
|
|
|
removexattr: Option<extern fn(req: Request, inode: u64, name: StrPtr)>,
|
|
|
|
access: Option<extern fn(req: Request, inode: u64, mask: i32)>,
|
|
|
|
create: Option<extern fn(req: Request, parent: u64, name: StrPtr, mode: c_int, fileinfo: MutPtr)>,
|
|
|
|
getlk: Option<extern fn(req: Request, inode: u64, fileinfo: MutPtr, lock: MutPtr)>,
|
|
|
|
setlk: Option<extern fn(req: Request, inode: u64, fileinfo: MutPtr, lock: MutPtr, sleep: c_int)>,
|
|
|
|
bmap: Option<extern fn(req: Request, inode: u64, blocksize: size_t, idx: u64)>,
|
|
|
|
ioctl: Option<extern fn(req: Request, inode: u64, cmd: c_int, arg: MutPtr, fileinfo: MutPtr, flags: c_int, in_buf: ConstPtr, in_bufsz: size_t, out_bufsz: size_t)>,
|
|
|
|
poll: Option<extern fn(req: Request, inode: u64, fileinfo: MutPtr, pollhandle: MutPtr)>,
|
|
|
|
write_buf: Option<extern fn(req: Request, inode: u64, bufv: MutPtr, offset: c_int, fileinfo: MutPtr)>,
|
|
|
|
retrieve_reply: Option<extern fn(req: Request, cookie: ConstPtr, inode: u64, offset: c_int, bufv: MutPtr)>,
|
|
|
|
forget_multi: Option<extern fn(req: Request, count: size_t, forgets: MutPtr)>,
|
|
|
|
flock: Option<extern fn(req: Request, inode: u64, fileinfo: MutPtr, op: c_int)>,
|
|
|
|
fallocate: Option<extern fn(req: Request, inode: u64, mode: c_int, offset: c_int, length: c_int, fileinfo: MutPtr)>,
|
|
|
|
readdirplus: Option<extern fn(req: Request, inode: u64, size: size_t, offset: c_int, fileinfo: MutPtr)>,
|
|
|
|
copy_file_range: Option<extern fn(req: Request, ino_in: u64, off_in: c_int, fi_in: MutPtr, ino_out: u64, off_out: c_int, fi_out: MutPtr, len: size_t, flags: c_int)>,
|
2019-08-08 10:49:18 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
impl Session {
|
|
|
|
/// Create a new low level fuse session.
|
|
|
|
///
|
|
|
|
/// `Session` is created using the provided mount options and sets the
|
|
|
|
/// default signal handlers.
|
|
|
|
/// Options have to be provided as comma separated OsStr, e.g.
|
|
|
|
/// ("ro,default_permissions").
|
2019-08-22 12:46:23 +00:00
|
|
|
pub fn new(archive_path: &Path, options: &OsStr, verbose: bool) -> Result<Self, Error> {
|
2019-08-08 10:49:18 +00:00
|
|
|
let file = File::open(archive_path)?;
|
|
|
|
// First argument should be the executable name
|
2019-08-13 13:37:47 +00:00
|
|
|
let mut arguments = vec![
|
2019-08-08 10:49:18 +00:00
|
|
|
CString::new("pxar-mount").unwrap(),
|
|
|
|
CString::new("-o").unwrap(),
|
|
|
|
CString::new(options.as_bytes())?,
|
|
|
|
];
|
2019-08-13 13:37:47 +00:00
|
|
|
if verbose {
|
|
|
|
arguments.push(CString::new("--debug").unwrap());
|
|
|
|
}
|
2019-08-08 10:49:18 +00:00
|
|
|
|
|
|
|
let arg_ptrs: Vec<_> = arguments.iter().map(|opt| opt.as_ptr()).collect();
|
|
|
|
let args = FuseArgs {
|
|
|
|
argc: arg_ptrs.len() as i32,
|
|
|
|
argv: arg_ptrs.as_ptr(),
|
|
|
|
allocated: 0,
|
|
|
|
};
|
|
|
|
|
|
|
|
// Register the callback funcitons for the session
|
2019-08-22 12:46:23 +00:00
|
|
|
let mut oprs = Operations::default();
|
|
|
|
oprs.init = Some(init);
|
|
|
|
oprs.destroy = Some(destroy);
|
|
|
|
oprs.lookup = Some(lookup);
|
|
|
|
oprs.getattr = Some(getattr);
|
|
|
|
oprs.open = Some(open);
|
|
|
|
oprs.read = Some(read);
|
|
|
|
oprs.opendir = Some(opendir);
|
|
|
|
oprs.readdir = Some(readdir);
|
2019-08-08 10:49:18 +00:00
|
|
|
|
2019-08-13 13:37:46 +00:00
|
|
|
// By storing the decoder as userdata of the session, each request may
|
|
|
|
// access it.
|
|
|
|
let reader = BufReader::new(file);
|
|
|
|
let decoder = Decoder::new(reader, decoder_callback as fn(&Path) -> Result<(), Error>)?;
|
|
|
|
let session_decoder = Box::new(Mutex::new(decoder));
|
2019-08-22 12:46:23 +00:00
|
|
|
let session_ptr = unsafe {
|
|
|
|
fuse_session_new(
|
|
|
|
&args as *const FuseArgs,
|
|
|
|
&oprs as *const Operations,
|
|
|
|
std::mem::size_of::<Operations>(),
|
|
|
|
// Ownership of session_decoder is passed to the session here.
|
|
|
|
// It has to be reclaimed before dropping the session to free
|
|
|
|
// the decoder and close the underlying file. This is done inside
|
|
|
|
// the destroy callback function.
|
|
|
|
Box::into_raw(session_decoder) as ConstPtr,
|
|
|
|
)
|
|
|
|
};
|
2019-08-08 10:49:18 +00:00
|
|
|
|
|
|
|
if session_ptr.is_null() {
|
|
|
|
bail!("error while creating new fuse session");
|
|
|
|
}
|
|
|
|
|
|
|
|
if unsafe { fuse_set_signal_handlers(session_ptr) } != 0 {
|
|
|
|
bail!("error while setting signal handlers");
|
|
|
|
}
|
|
|
|
|
|
|
|
Ok(Self {
|
|
|
|
ptr: session_ptr,
|
|
|
|
verbose: verbose,
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Actually mount the filesystem for this session on the provided mountpoint
|
|
|
|
/// and daemonize process.
|
|
|
|
pub fn mount(&mut self, mountpoint: &Path) -> Result<(), Error> {
|
|
|
|
if self.verbose {
|
|
|
|
println!("Mounting archive to {:#?}", mountpoint);
|
|
|
|
}
|
|
|
|
let mountpoint = mountpoint.canonicalize()?;
|
|
|
|
let path_cstr = CString::new(mountpoint.as_os_str().as_bytes())
|
|
|
|
.map_err(|err| format_err!("invalid mountpoint - {}", err))?;
|
|
|
|
if unsafe { fuse_session_mount(self.ptr, path_cstr.as_ptr()) } != 0 {
|
|
|
|
bail!("mounting on {:#?} failed", mountpoint);
|
|
|
|
}
|
|
|
|
|
|
|
|
// Do not send process to background if verbose flag is set
|
|
|
|
if !self.verbose && unsafe { fuse_daemonize(0) } != 0 {
|
|
|
|
bail!("could not send process to background");
|
|
|
|
}
|
|
|
|
|
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Execute session loop which handles requests from kernel.
|
2019-08-22 14:39:04 +00:00
|
|
|
///
|
|
|
|
/// The multi_threaded flag controls if the session loop runs in
|
|
|
|
/// singlethreaded or multithreaded.
|
|
|
|
/// Singlethreaded mode is intended for debugging.
|
|
|
|
pub fn run_loop(&mut self, multi_threaded: bool) -> Result<(), Error> {
|
2019-08-08 10:49:18 +00:00
|
|
|
if self.verbose {
|
|
|
|
println!("Executing fuse session loop");
|
|
|
|
}
|
2019-08-22 14:39:04 +00:00
|
|
|
let result = match multi_threaded {
|
|
|
|
true => unsafe { fuse_session_loop_mt_31(self.ptr, 1) },
|
|
|
|
false => unsafe { fuse_session_loop(self.ptr) },
|
|
|
|
};
|
2019-08-08 10:49:18 +00:00
|
|
|
if result < 0 {
|
|
|
|
bail!("fuse session loop exited with - {}", result);
|
|
|
|
}
|
|
|
|
if result > 0 {
|
|
|
|
eprintln!("fuse session loop recieved signal - {}", result);
|
|
|
|
}
|
|
|
|
|
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
impl Drop for Session {
|
|
|
|
fn drop(&mut self) {
|
|
|
|
unsafe {
|
|
|
|
fuse_session_unmount(self.ptr);
|
|
|
|
fuse_remove_signal_handlers(self.ptr);
|
|
|
|
fuse_session_destroy(self.ptr);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-09-03 12:14:30 +00:00
|
|
|
const GOODBYE_ITEM_SIZE: u64 = std::mem::size_of::<PxarGoodbyeItem>() as u64;
|
|
|
|
|
|
|
|
/// Converts the inode to the file offset
|
|
|
|
///
|
|
|
|
/// Since the inodes are defined as the file offset,
|
|
|
|
/// this simply returns the inode value.
|
|
|
|
/// The only exception to this is the inode of root,
|
|
|
|
/// which is defined as `FUSE_ROOT_ID` by libfuse and therefore mapped
|
|
|
|
/// to the roots goodbye table tail.
|
|
|
|
fn inode_to_offset(inode: u64, root_end: u64) -> u64 {
|
|
|
|
if inode == FUSE_ROOT_ID {
|
|
|
|
root_end - GOODBYE_ITEM_SIZE
|
|
|
|
} else {
|
|
|
|
inode
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Converts the file offset to an inode
|
|
|
|
///
|
|
|
|
/// Since the inodes are defined as the file offset,
|
|
|
|
/// this simply returns the offset value.
|
|
|
|
/// The only exception to this is the inode of root,
|
|
|
|
/// which is defined as `FUSE_ROOT_ID` by libfuse, so the roots goodbye tail
|
|
|
|
/// offset gets mapped to `FUSE_ROOT_ID`.
|
|
|
|
pub(crate) fn offset_to_inode(offset: u64, root_end: u64) -> u64 {
|
|
|
|
if offset == root_end - GOODBYE_ITEM_SIZE {
|
|
|
|
FUSE_ROOT_ID
|
|
|
|
} else {
|
|
|
|
offset
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-08-13 13:37:46 +00:00
|
|
|
/// Creates a context providing an exclusive mutable reference to the decoder.
|
|
|
|
///
|
|
|
|
/// Each callback function needing access to the decoder can easily get an
|
|
|
|
/// exclusive handle by running the code inside this context.
|
|
|
|
/// Responses with error code can easily be generated by returning with the
|
|
|
|
/// error code.
|
2019-08-22 12:46:23 +00:00
|
|
|
fn run_in_context<F>(req: Request, code: F)
|
|
|
|
where
|
|
|
|
F: FnOnce(&mut Decoder<BufReader<File>, fn(&Path) -> Result<(), Error>>) -> Result<(), i32>,
|
|
|
|
{
|
|
|
|
let ptr = unsafe {
|
|
|
|
fuse_req_userdata(req)
|
|
|
|
as *mut Mutex<Decoder<BufReader<File>, fn(&Path) -> Result<(), Error>>>
|
|
|
|
};
|
2019-08-13 13:37:46 +00:00
|
|
|
let boxed_decoder = unsafe { Box::from_raw(ptr) };
|
2019-08-22 12:46:23 +00:00
|
|
|
let result = boxed_decoder
|
|
|
|
.lock()
|
2019-08-13 13:37:46 +00:00
|
|
|
.map(|mut decoder| code(&mut decoder))
|
|
|
|
.unwrap_or(Err(libc::ENOENT));
|
|
|
|
|
|
|
|
if let Err(err) = result {
|
2019-08-22 12:46:23 +00:00
|
|
|
unsafe {
|
|
|
|
let _res = fuse_reply_err(req, err);
|
|
|
|
}
|
2019-08-13 13:37:46 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Release ownership of boxed decoder, do not drop it.
|
|
|
|
let _ = Box::into_raw(boxed_decoder);
|
|
|
|
}
|
|
|
|
|
2019-08-08 10:49:18 +00:00
|
|
|
/// Callback functions for fuse kernel driver.
|
2019-08-13 13:37:46 +00:00
|
|
|
extern "C" fn init(_decoder: MutPtr) {
|
|
|
|
// Notting to do here for now
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Cleanup the userdata created while creating the session, which is the decoder
|
|
|
|
extern "C" fn destroy(decoder: MutPtr) {
|
|
|
|
// Get ownership of the decoder and drop it when Box goes out of scope.
|
2019-08-22 12:46:23 +00:00
|
|
|
unsafe {
|
|
|
|
Box::from_raw(decoder);
|
|
|
|
}
|
2019-08-13 13:37:46 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
extern "C" fn lookup(req: Request, _parent: u64, _name: StrPtr) {
|
2019-08-14 12:33:58 +00:00
|
|
|
run_in_context(req, |_decoder| {
|
2019-08-13 13:37:46 +00:00
|
|
|
// code goes here
|
|
|
|
|
|
|
|
Err(libc::ENOENT)
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
extern "C" fn getattr(req: Request, _inode: u64, _fileinfo: MutPtr) {
|
2019-08-14 12:33:58 +00:00
|
|
|
run_in_context(req, |_decoder| {
|
2019-08-13 13:37:46 +00:00
|
|
|
// code goes here
|
|
|
|
|
|
|
|
Err(libc::ENOENT)
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
extern "C" fn open(req: Request, _inode: u64, _fileinfo: MutPtr) {
|
2019-08-14 12:33:58 +00:00
|
|
|
run_in_context(req, |_decoder| {
|
2019-08-13 13:37:46 +00:00
|
|
|
// code goes here
|
|
|
|
|
|
|
|
Err(libc::ENOENT)
|
|
|
|
});
|
2019-08-08 10:49:18 +00:00
|
|
|
}
|
|
|
|
|
2019-08-13 13:37:46 +00:00
|
|
|
extern "C" fn read(req: Request, _inode: u64, _size: size_t, _offset: c_int, _fileinfo: MutPtr) {
|
2019-08-14 12:33:58 +00:00
|
|
|
run_in_context(req, |_decoder| {
|
2019-08-13 13:37:46 +00:00
|
|
|
// code goes here
|
|
|
|
|
|
|
|
Err(libc::ENOENT)
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
extern "C" fn opendir(req: Request, _inode: u64, _fileinfo: MutPtr) {
|
2019-08-14 12:33:58 +00:00
|
|
|
run_in_context(req, |_decoder| {
|
2019-08-13 13:37:46 +00:00
|
|
|
// code goes here
|
|
|
|
|
|
|
|
Err(libc::ENOENT)
|
|
|
|
});
|
2019-08-08 10:49:18 +00:00
|
|
|
}
|
2019-08-13 13:37:46 +00:00
|
|
|
|
|
|
|
extern "C" fn readdir(req: Request, _inode: u64, _size: size_t, _offset: c_int, _fileinfo: MutPtr) {
|
2019-08-14 12:33:58 +00:00
|
|
|
run_in_context(req, |_decoder| {
|
2019-08-13 13:37:46 +00:00
|
|
|
// code goes here
|
|
|
|
|
|
|
|
Err(libc::ENOENT)
|
|
|
|
});
|
|
|
|
}
|