@@ -11,6 +11,7 @@
#include <blk.h>
#include <env.h>
+#include <fs.h>
#include <membuf.h>
#include <part.h>
#include <malloc.h>
@@ -33,6 +34,9 @@ static struct blk_desc *ext4l_blk_dev;
static struct disk_partition ext4l_partition;
static int ext4l_mounted;
+/* Count of open directory streams (prevents unmount while iterating) */
+static int ext4l_open_dirs;
+
/* Global super_block pointer for filesystem operations */
static struct super_block *ext4l_sb;
@@ -634,7 +638,213 @@ int ext4l_ls(const char *dirname)
void ext4l_close(void)
{
+ if (ext4l_open_dirs > 0)
+ return;
+
ext4l_dev_desc = NULL;
ext4l_sb = NULL;
ext4l_clear_blk_dev();
}
+
+/**
+ * struct ext4l_dir - ext4l directory stream state
+ * @parent: base fs_dir_stream structure
+ * @dirent: directory entry to return to caller
+ * @dir_inode: pointer to directory inode
+ * @file: file structure for ext4_readdir
+ * @entry_found: flag set by actor when entry is captured
+ * @last_ino: inode number of last returned entry (to skip on next call)
+ * @skip_last: true if we need to skip the last_ino entry
+ *
+ * The filesystem stays mounted while directory streams are open (ext4l_close
+ * checks ext4l_open_dirs), so we can keep direct pointers to inodes.
+ */
+struct ext4l_dir {
+ struct fs_dir_stream parent;
+ struct fs_dirent dirent;
+ struct inode *dir_inode;
+ struct file file;
+ bool entry_found;
+ u64 last_ino;
+ bool skip_last;
+};
+
+/**
+ * struct ext4l_readdir_ctx - Extended dir_context with back-pointer
+ * @ctx: base dir_context structure (must be first)
+ * @dir: pointer to ext4l_dir for state updates
+ */
+struct ext4l_readdir_ctx {
+ struct dir_context ctx;
+ struct ext4l_dir *dir;
+};
+
+/**
+ * ext4l_opendir_actor() - dir_context actor that captures single entry
+ *
+ * This actor is called by ext4_readdir for each directory entry. It captures
+ * the first entry found (skipping the previously returned entry if needed)
+ * and returns non-zero to stop iteration.
+ */
+static int ext4l_opendir_actor(struct dir_context *ctx, const char *name,
+ int namelen, loff_t offset, u64 ino,
+ unsigned int d_type)
+{
+ struct ext4l_readdir_ctx *rctx;
+ struct ext4l_dir *dir;
+ struct fs_dirent *dent;
+ struct inode *inode;
+
+ rctx = container_of(ctx, struct ext4l_readdir_ctx, ctx);
+ dir = rctx->dir;
+
+ /*
+ * Skip the entry we returned last time. The htree code may call us
+ * with the same entry again due to its extra_fname handling.
+ */
+ if (dir->skip_last && ino == dir->last_ino) {
+ dir->skip_last = false;
+ return 0; /* Continue to next entry */
+ }
+
+ dent = &dir->dirent;
+
+ /* Copy name */
+ if (namelen >= FS_DIRENT_NAME_LEN)
+ namelen = FS_DIRENT_NAME_LEN - 1;
+ memcpy(dent->name, name, namelen);
+ dent->name[namelen] = '\0';
+
+ /* Set type based on d_type hint */
+ switch (d_type) {
+ case DT_DIR:
+ dent->type = FS_DT_DIR;
+ break;
+ case DT_LNK:
+ dent->type = FS_DT_LNK;
+ break;
+ default:
+ dent->type = FS_DT_REG;
+ break;
+ }
+
+ /* Look up inode to get size and other attributes */
+ inode = ext4_iget(ext4l_sb, ino, 0);
+ if (!IS_ERR(inode)) {
+ dent->size = inode->i_size;
+ /* Refine type from inode mode if needed */
+ if (S_ISDIR(inode->i_mode))
+ dent->type = FS_DT_DIR;
+ else if (S_ISLNK(inode->i_mode))
+ dent->type = FS_DT_LNK;
+ else
+ dent->type = FS_DT_REG;
+ } else {
+ dent->size = 0;
+ }
+
+ dir->entry_found = true;
+ dir->last_ino = ino;
+
+ /*
+ * Return non-zero to stop iteration after one entry.
+ * dir_emit() returns (actor(...) == 0), so:
+ * actor returns 0 -> dir_emit returns 1 (continue)
+ * actor returns non-zero -> dir_emit returns 0 (stop)
+ */
+ return 1;
+}
+
+int ext4l_opendir(const char *filename, struct fs_dir_stream **dirsp)
+{
+ struct ext4l_dir *dir;
+ struct inode *inode;
+ int ret;
+
+ if (!ext4l_mounted)
+ return -ENODEV;
+
+ ret = ext4l_resolve_path(filename, &inode);
+ if (ret)
+ return ret;
+
+ if (!S_ISDIR(inode->i_mode))
+ return -ENOTDIR;
+
+ dir = calloc(1, sizeof(*dir));
+ if (!dir)
+ return -ENOMEM;
+
+ dir->dir_inode = inode;
+ dir->entry_found = false;
+
+ /* Set up file structure for ext4_readdir */
+ dir->file.f_inode = inode;
+ dir->file.f_mapping = inode->i_mapping;
+ dir->file.private_data = kzalloc(sizeof(struct dir_private_info),
+ GFP_KERNEL);
+ if (!dir->file.private_data) {
+ free(dir);
+ return -ENOMEM;
+ }
+
+ /* Increment open dir count to prevent unmount */
+ ext4l_open_dirs++;
+
+ *dirsp = (struct fs_dir_stream *)dir;
+
+ return 0;
+}
+
+int ext4l_readdir(struct fs_dir_stream *dirs, struct fs_dirent **dentp)
+{
+ struct ext4l_dir *dir = (struct ext4l_dir *)dirs;
+ struct ext4l_readdir_ctx ctx;
+ int ret;
+
+ if (!ext4l_mounted)
+ return -ENODEV;
+
+ memset(&dir->dirent, '\0', sizeof(dir->dirent));
+ dir->entry_found = false;
+
+ /* Skip the entry we returned last time (htree may re-emit it) */
+ if (dir->last_ino)
+ dir->skip_last = true;
+
+ /* Set up extended dir_context for this iteration */
+ memset(&ctx, '\0', sizeof(ctx));
+ ctx.ctx.actor = ext4l_opendir_actor;
+ ctx.ctx.pos = dir->file.f_pos;
+ ctx.dir = dir;
+
+ ret = ext4_readdir(&dir->file, &ctx.ctx);
+
+ /* Update file position for next call */
+ dir->file.f_pos = ctx.ctx.pos;
+
+ if (ret < 0)
+ return ret;
+
+ if (!dir->entry_found)
+ return -ENOENT;
+
+ *dentp = &dir->dirent;
+
+ return 0;
+}
+
+void ext4l_closedir(struct fs_dir_stream *dirs)
+{
+ struct ext4l_dir *dir = (struct ext4l_dir *)dirs;
+
+ if (dir) {
+ if (dir->file.private_data)
+ ext4_htree_free_dir_info(dir->file.private_data);
+ free(dir);
+ }
+
+ /* Decrement open dir count */
+ if (ext4l_open_dirs > 0)
+ ext4l_open_dirs--;
+}
@@ -271,7 +271,9 @@ static struct fstype_info fstypes[] = {
.read = fs_read_unsupported,
.write = fs_write_unsupported,
.uuid = fs_uuid_unsupported,
- .opendir = fs_opendir_unsupported,
+ .opendir = ext4l_opendir,
+ .readdir = ext4l_readdir,
+ .closedir = ext4l_closedir,
.unlink = fs_unlink_unsupported,
.mkdir = fs_mkdir_unsupported,
.ln = fs_ln_unsupported,
@@ -11,6 +11,8 @@
struct blk_desc;
struct disk_partition;
+struct fs_dir_stream;
+struct fs_dirent;
/**
* ext4l_probe() - Probe a block device for an ext4 filesystem
@@ -44,4 +46,30 @@ int ext4l_ls(const char *dirname);
*/
int ext4l_get_uuid(u8 *uuid);
+/**
+ * ext4l_opendir() - Open a directory for iteration
+ *
+ * @filename: Directory path
+ * @dirsp: Returns directory stream pointer
+ * Return: 0 on success, -ENODEV if not mounted, -ENOTDIR if not a directory,
+ * -ENOMEM on allocation failure
+ */
+int ext4l_opendir(const char *filename, struct fs_dir_stream **dirsp);
+
+/**
+ * ext4l_readdir() - Read the next directory entry
+ *
+ * @dirs: Directory stream from ext4l_opendir
+ * @dentp: Returns pointer to directory entry
+ * Return: 0 on success, -ENODEV if not mounted, -ENOENT at end of directory
+ */
+int ext4l_readdir(struct fs_dir_stream *dirs, struct fs_dirent **dentp);
+
+/**
+ * ext4l_closedir() - Close a directory stream
+ *
+ * @dirs: Directory stream to close
+ */
+void ext4l_closedir(struct fs_dir_stream *dirs);
+
#endif /* __EXT4L_H__ */
@@ -98,6 +98,7 @@ struct file {
void *private_data;
struct file_ra_state f_ra;
struct path f_path;
+ loff_t f_pos;
};
/* Get inode from file */
@@ -111,3 +111,88 @@ static int fs_test_ext4l_ls_norun(struct unit_test_state *uts)
}
FS_TEST_ARGS(fs_test_ext4l_ls_norun, UTF_SCAN_FDT | UTF_CONSOLE | UTF_MANUAL,
{ "fs_image", UT_ARG_STR });
+
+/**
+ * fs_test_ext4l_opendir_norun() - Test ext4l opendir/readdir/closedir
+ *
+ * Verifies that the ext4l driver can iterate through directory entries using
+ * the opendir/readdir/closedir interface. It checks:
+ * - Regular files (testfile.txt)
+ * - Subdirectories (subdir)
+ * - Symlinks (link.txt)
+ * - Files in subdirectories (subdir/nested.txt)
+ *
+ * Arguments:
+ * fs_image: Path to the ext4 filesystem image
+ */
+static int fs_test_ext4l_opendir_norun(struct unit_test_state *uts)
+{
+ const char *fs_image = ut_str(EXT4L_ARG_IMAGE);
+ struct fs_dir_stream *dirs;
+ struct fs_dirent *dent;
+ bool found_testfile = false;
+ bool found_subdir = false;
+ bool found_symlink = false;
+ bool found_nested = false;
+ int count = 0;
+
+ ut_assertnonnull(fs_image);
+ ut_assertok(run_commandf("host bind 0 %s", fs_image));
+ ut_assertok(fs_set_blk_dev("host", "0", FS_TYPE_ANY));
+
+ /* Open root directory */
+ ut_assertok(ext4l_opendir("/", &dirs));
+ ut_assertnonnull(dirs);
+
+ /* Iterate through entries */
+ while (!ext4l_readdir(dirs, &dent)) {
+ ut_assertnonnull(dent);
+ count++;
+ if (!strcmp(dent->name, "testfile.txt")) {
+ found_testfile = true;
+ ut_asserteq(FS_DT_REG, dent->type);
+ ut_asserteq(12, dent->size);
+ } else if (!strcmp(dent->name, "subdir")) {
+ found_subdir = true;
+ ut_asserteq(FS_DT_DIR, dent->type);
+ } else if (!strcmp(dent->name, "link.txt")) {
+ found_symlink = true;
+ ut_asserteq(FS_DT_LNK, dent->type);
+ }
+ }
+
+ ext4l_closedir(dirs);
+
+ /* Verify we found expected entries */
+ ut_assert(found_testfile);
+ ut_assert(found_subdir);
+ ut_assert(found_symlink);
+ /* At least ., .., testfile.txt, subdir, link.txt */
+ ut_assert(count >= 5);
+
+ /* Now test reading the subdirectory */
+ ut_assertok(fs_set_blk_dev("host", "0", FS_TYPE_ANY));
+ ut_assertok(ext4l_opendir("/subdir", &dirs));
+ ut_assertnonnull(dirs);
+
+ count = 0;
+ while (!ext4l_readdir(dirs, &dent)) {
+ ut_assertnonnull(dent);
+ count++;
+ if (!strcmp(dent->name, "nested.txt")) {
+ found_nested = true;
+ ut_asserteq(FS_DT_REG, dent->type);
+ ut_asserteq(12, dent->size);
+ }
+ }
+
+ ext4l_closedir(dirs);
+
+ ut_assert(found_nested);
+ /* At least ., .., nested.txt */
+ ut_assert(count >= 3);
+
+ return 0;
+}
+FS_TEST_ARGS(fs_test_ext4l_opendir_norun, UTF_SCAN_FDT | UTF_CONSOLE |
+ UTF_MANUAL, { "fs_image", UT_ARG_STR });
@@ -37,14 +37,27 @@ class TestExt4l:
shell=True)
check_call(f'mkfs.ext4 -q {image_path}', shell=True)
- # Add a test file using debugfs (no mount required)
+ # Add test files using debugfs (no mount required)
with NamedTemporaryFile(mode='w', delete=False) as tmp:
tmp.write('hello world\n')
tmp_path = tmp.name
try:
+ # Add a regular file
check_call(f'debugfs -w {image_path} '
f'-R "write {tmp_path} testfile.txt" 2>/dev/null',
shell=True)
+ # Add a subdirectory
+ check_call(f'debugfs -w {image_path} '
+ f'-R "mkdir subdir" 2>/dev/null',
+ shell=True)
+ # Add a file in the subdirectory
+ check_call(f'debugfs -w {image_path} '
+ f'-R "write {tmp_path} subdir/nested.txt" 2>/dev/null',
+ shell=True)
+ # Add a symlink
+ check_call(f'debugfs -w {image_path} '
+ f'-R "symlink link.txt testfile.txt" 2>/dev/null',
+ shell=True)
finally:
os.unlink(tmp_path)
except CalledProcessError:
@@ -76,3 +89,10 @@ class TestExt4l:
output = ubman.run_command(
f'ut -f fs fs_test_ext4l_ls_norun fs_image={ext4_image}')
assert 'failures: 0' in output
+
+ def test_opendir(self, ubman, ext4_image):
+ """Test that ext4l can iterate directory entries."""
+ with ubman.log.section('Test ext4l opendir'):
+ output = ubman.run_command(
+ f'ut -f fs fs_test_ext4l_opendir_norun fs_image={ext4_image}')
+ assert 'failures: 0' in output