Skip to content

made readdir stateful #1738

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Jul 21, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 5 additions & 6 deletions src/fd/mod.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
use alloc::boxed::Box;
use alloc::sync::Arc;
use alloc::vec::Vec;
use core::future::{self, Future};
use core::mem::MaybeUninit;
use core::task::Poll::{Pending, Ready};
Expand All @@ -13,7 +12,7 @@ use smoltcp::wire::{IpEndpoint, IpListenEndpoint};
use crate::arch::kernel::core_local::core_scheduler;
use crate::errno::Errno;
use crate::executor::block_on;
use crate::fs::{DirectoryEntry, FileAttr, SeekWhence};
use crate::fs::{FileAttr, SeekWhence};
use crate::io;

mod eventfd;
Expand Down Expand Up @@ -179,10 +178,10 @@ pub(crate) trait ObjectInterface: Sync + Send + core::fmt::Debug {
Err(Errno::Inval)
}

/// 'readdir' returns a pointer to a dirent structure
/// representing the next directory entry in the directory stream
/// pointed to by the file descriptor
async fn readdir(&self) -> io::Result<Vec<DirectoryEntry>> {
/// `getdents` fills the given buffer `_buf` with [`Dirent64`](crate::syscalls::Dirent64)
/// formatted entries of a directory, imitating the Linux `getdents64` syscall.
/// On success, the number of bytes read is returned. On end of directory, 0 is returned. On error, -1 is returned
async fn getdents(&self, _buf: &mut [MaybeUninit<u8>]) -> io::Result<usize> {
Err(Errno::Inval)
}

Expand Down
95 changes: 67 additions & 28 deletions src/fs/fuse.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,12 @@ use alloc::string::String;
use alloc::sync::Arc;
use alloc::vec::Vec;
use core::marker::PhantomData;
use core::mem::MaybeUninit;
use core::mem::{MaybeUninit, align_of, offset_of, size_of};
use core::sync::atomic::{AtomicU64, Ordering};
use core::task::Poll;
use core::{future, mem};

use align_address::Align;
use async_lock::Mutex;
use async_trait::async_trait;
use fuse_abi::linux::*;
Expand All @@ -28,6 +29,7 @@ use crate::fs::{
SeekWhence, VfsNode,
};
use crate::mm::device_alloc::DeviceAlloc;
use crate::syscalls::Dirent64;
use crate::time::{time_t, timespec};
use crate::{arch, io};

Expand Down Expand Up @@ -811,20 +813,24 @@ impl Clone for FuseFileHandle {
}
}

#[derive(Debug, Clone)]
#[derive(Debug)]
pub struct FuseDirectoryHandle {
name: Option<String>,
read_position: Mutex<usize>,
}

impl FuseDirectoryHandle {
pub fn new(name: Option<String>) -> Self {
Self { name }
Self {
name,
read_position: Mutex::new(0),
}
}
}

#[async_trait]
impl ObjectInterface for FuseDirectoryHandle {
async fn readdir(&self) -> io::Result<Vec<DirectoryEntry>> {
async fn getdents(&self, buf: &mut [MaybeUninit<u8>]) -> io::Result<usize> {
let path: CString = if let Some(name) = &self.name {
CString::new("/".to_string() + name).unwrap()
} else {
Expand All @@ -849,7 +855,8 @@ impl ObjectInterface for FuseDirectoryHandle {

// Linux seems to allocate a single page to store the dirfile
let len = MAX_READ_LEN as u32;
let mut offset: usize = 0;
let rsp_offset: &mut usize = &mut *self.read_position.lock().await;
let mut buf_offset: usize = 0;

// read content of the directory
let (mut cmd, rsp_payload_len) = ops::Read::create(fuse_nid, fuse_fh, len, 0);
Expand All @@ -859,44 +866,63 @@ impl ObjectInterface for FuseDirectoryHandle {
.lock()
.send_command(cmd, rsp_payload_len)?;

let len: usize = if rsp.headers.out_header.len as usize - mem::size_of::<fuse_out_header>()
>= usize::try_from(len).unwrap()
{
len.try_into().unwrap()
} else {
(rsp.headers.out_header.len as usize) - mem::size_of::<fuse_out_header>()
};
let len = usize::min(
MAX_READ_LEN,
rsp.headers.out_header.len as usize - mem::size_of::<fuse_out_header>(),
);

if len <= core::mem::size_of::<fuse_dirent>() {
debug!("FUSE no new dirs");
return Err(Errno::Noent);
}

let mut entries: Vec<DirectoryEntry> = Vec::new();
while (rsp.headers.out_header.len as usize) - offset > core::mem::size_of::<fuse_dirent>() {
let mut ret = 0;

while (rsp.headers.out_header.len as usize) - *rsp_offset > size_of::<fuse_dirent>() {
let dirent = unsafe {
&*rsp
.payload
.as_ref()
.unwrap()
.as_ptr()
.byte_add(offset)
.byte_add(*rsp_offset)
.cast::<fuse_dirent>()
};

offset += core::mem::size_of::<fuse_dirent>() + dirent.namelen as usize;
// Align to dirent struct
offset = ((offset) + U64_SIZE - 1) & (!(U64_SIZE - 1));
let dirent_len = offset_of!(Dirent64, d_name) + dirent.namelen as usize + 1;
let next_dirent = (buf_offset + dirent_len).align_up(align_of::<Dirent64>());

let name: &'static [u8] = unsafe {
core::slice::from_raw_parts(
dirent.name.as_ptr().cast(),
dirent.namelen.try_into().unwrap(),
)
};
entries.push(DirectoryEntry::new(unsafe {
core::str::from_utf8_unchecked(name).to_string()
}));
if next_dirent > buf.len() {
// target buffer full -> we return the nr. of bytes written (like linux does)
break;
}

// could be replaced with slice_as_ptr once maybe_uninit_slice is stabilized.
let target_dirent = buf[buf_offset].as_mut_ptr().cast::<Dirent64>();
unsafe {
target_dirent.write(Dirent64 {
d_ino: dirent.ino,
d_off: 0,
d_reclen: (dirent_len.align_up(align_of::<Dirent64>()))
.try_into()
.unwrap(),
d_type: (dirent.type_ as u8).try_into().unwrap(),
d_name: PhantomData {},
});
let nameptr = core::ptr::from_mut(&mut (*(target_dirent)).d_name).cast::<u8>();
core::ptr::copy_nonoverlapping(
dirent.name.as_ptr().cast::<u8>(),
nameptr,
dirent.namelen as usize,
);
nameptr.add(dirent.namelen as usize).write(0); // zero termination
}

*rsp_offset += core::mem::size_of::<fuse_dirent>() + dirent.namelen as usize;
// Align to dirent struct
*rsp_offset = ((*rsp_offset) + U64_SIZE - 1) & (!(U64_SIZE - 1));
buf_offset = next_dirent;
ret = buf_offset;
}

let (cmd, rsp_payload_len) = ops::Release::create(fuse_nid, fuse_fh);
Expand All @@ -905,7 +931,20 @@ impl ObjectInterface for FuseDirectoryHandle {
.lock()
.send_command(cmd, rsp_payload_len)?;

Ok(entries)
Ok(ret)
}

/// lseek for a directory entry is the equivalent for seekdir on linux. But on Hermit this is
/// logically the same operation, so we can just use the same fn in the backend.
/// Any other offset than 0 is not supported. (Mostly because it doesn't make any sense, as
/// userspace applications have no way of knowing valid offsets)
async fn lseek(&self, offset: isize, whence: SeekWhence) -> io::Result<isize> {
if whence != SeekWhence::Set && offset != 0 {
error!("Invalid offset for directory lseek ({offset})");
return Err(Errno::Inval);
}
*self.read_position.lock().await = offset as usize;
Ok(offset)
}
}

Expand Down
74 changes: 65 additions & 9 deletions src/fs/mem.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,18 @@ use alloc::collections::BTreeMap;
use alloc::string::String;
use alloc::sync::Arc;
use alloc::vec::Vec;
use core::mem::MaybeUninit;
use core::marker::PhantomData;
use core::mem::{MaybeUninit, offset_of};

use align_address::Align;
use async_lock::{Mutex, RwLock};
use async_trait::async_trait;

use crate::errno::Errno;
use crate::executor::block_on;
use crate::fd::{AccessPermission, ObjectInterface, OpenOption, PollEvent};
use crate::fs::{DirectoryEntry, FileAttr, NodeKind, SeekWhence, VfsNode};
use crate::fs::{DirectoryEntry, FileAttr, FileType, NodeKind, SeekWhence, VfsNode};
use crate::syscalls::Dirent64;
use crate::time::timespec;
use crate::{arch, io};

Expand Down Expand Up @@ -375,11 +378,12 @@ impl RamFile {
}
}

#[derive(Debug, Clone)]
#[derive(Debug)]
pub struct MemDirectoryInterface {
/// Directory entries
inner:
Arc<RwLock<BTreeMap<String, Box<dyn VfsNode + core::marker::Send + core::marker::Sync>>>>,
read_idx: Mutex<usize>,
}

impl MemDirectoryInterface {
Expand All @@ -388,19 +392,71 @@ impl MemDirectoryInterface {
RwLock<BTreeMap<String, Box<dyn VfsNode + core::marker::Send + core::marker::Sync>>>,
>,
) -> Self {
Self { inner }
Self {
inner,
read_idx: Mutex::new(0),
}
}
}

#[async_trait]
impl ObjectInterface for MemDirectoryInterface {
async fn readdir(&self) -> io::Result<Vec<DirectoryEntry>> {
let mut entries: Vec<DirectoryEntry> = Vec::new();
for name in self.inner.read().await.keys() {
entries.push(DirectoryEntry::new(name.clone()));
async fn getdents(&self, buf: &mut [MaybeUninit<u8>]) -> io::Result<usize> {
let mut buf_offset: usize = 0;
let mut ret = 0;
let mut read_idx = self.read_idx.lock().await;
for name in self.inner.read().await.keys().skip(*read_idx) {
let namelen = name.len();

let dirent_len = offset_of!(Dirent64, d_name) + namelen + 1;
let next_dirent = (buf_offset + dirent_len).align_up(align_of::<Dirent64>());

if next_dirent > buf.len() {
// target buffer full -> we return the nr. of bytes written (like linux does)
break;
}

*read_idx += 1;

// could be replaced with slice_as_ptr once maybe_uninit_slice is stabilized.
let target_dirent = buf[buf_offset].as_mut_ptr().cast::<Dirent64>();

unsafe {
target_dirent.write(Dirent64 {
d_ino: 1, // TODO: we don't have inodes in the mem filesystem. Maybe this could lead to problems
d_off: 0,
d_reclen: (dirent_len.align_up(align_of::<Dirent64>()))
.try_into()
.unwrap(),
d_type: FileType::Unknown, // TODO: Proper filetype
d_name: PhantomData {},
});
let nameptr = core::ptr::from_mut(&mut (*(target_dirent)).d_name).cast::<u8>();
core::ptr::copy_nonoverlapping(
name.as_bytes().as_ptr().cast::<u8>(),
nameptr,
namelen,
);
nameptr.add(namelen).write(0); // zero termination
}

buf_offset = next_dirent;
ret = buf_offset;
}
Ok(ret)
}

Ok(entries)
/// lseek for a directory entry is the equivalent for seekdir on linux. But on Hermit this is
/// logically the same operation, so we can just use the same fn in the backend.
/// Any other offset than 0 is not supported. (Mostly because it doesn't make any sense, as
/// userspace applications have no way of knowing valid offsets)
async fn lseek(&self, offset: isize, whence: SeekWhence) -> io::Result<isize> {
if whence != SeekWhence::Set && offset != 0 {
error!("Invalid offset for directory lseek ({offset})");
return Err(Errno::Inval);
}
*self.read_idx.lock().await = offset as usize;
Ok(offset)
}
}

Expand Down
5 changes: 3 additions & 2 deletions src/fs/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -131,8 +131,9 @@ impl DirectoryReader {

#[async_trait]
impl ObjectInterface for DirectoryReader {
async fn readdir(&self) -> io::Result<Vec<DirectoryEntry>> {
Ok(self.0.clone())
async fn getdents(&self, _buf: &mut [core::mem::MaybeUninit<u8>]) -> io::Result<usize> {
let _ = &self.0; // Dummy statement to avoid warning for the moment
unimplemented!()
}
}

Expand Down
Loading