From: Simon Glass <simon.glass@canonical.com> Implement directory-iteration for the ext4l filesystem driver, allowing callers to iterate through directory entries one at a time. Add ext4l_opendir() which opens a directory and returns a stream handle, ext4l_readdir() which returns the next directory entry, and ext4l_closedir() which closes the stream and frees resources. The implementation uses a struct dir_context to capture single entries from ext4_readdir(), with logic to skip previously returned entries since the htree code may re-emit them. Update struct file to include a position. Wire these functions into fs_legacy.c for the ext4l filesystem type. Co-developed-by: Claude Opus 4.5 <noreply@anthropic.com> Signed-off-by: Simon Glass <simon.glass@canonical.com> --- fs/ext4l/interface.c | 210 ++++++++++++++++++++++++++++ fs/fs_legacy.c | 4 +- include/ext4l.h | 28 ++++ include/linux/fs.h | 1 + test/fs/ext4l.c | 85 +++++++++++ test/py/tests/test_fs/test_ext4l.py | 22 ++- 6 files changed, 348 insertions(+), 2 deletions(-) diff --git a/fs/ext4l/interface.c b/fs/ext4l/interface.c index 6e146f246bd..e7f09fd45dc 100644 --- a/fs/ext4l/interface.c +++ b/fs/ext4l/interface.c @@ -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--; +} diff --git a/fs/fs_legacy.c b/fs/fs_legacy.c index 6ca9d6e647a..7d293468ea8 100644 --- a/fs/fs_legacy.c +++ b/fs/fs_legacy.c @@ -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, diff --git a/include/ext4l.h b/include/ext4l.h index 333d9db139c..6d8eba84f4e 100644 --- a/include/ext4l.h +++ b/include/ext4l.h @@ -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__ */ diff --git a/include/linux/fs.h b/include/linux/fs.h index ef28c12c022..090ee192061 100644 --- a/include/linux/fs.h +++ b/include/linux/fs.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 */ diff --git a/test/fs/ext4l.c b/test/fs/ext4l.c index 4c477ce3338..d9ed21407e7 100644 --- a/test/fs/ext4l.c +++ b/test/fs/ext4l.c @@ -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 }); diff --git a/test/py/tests/test_fs/test_ext4l.py b/test/py/tests/test_fs/test_ext4l.py index 93b6d4d34e8..073b02a80ce 100644 --- a/test/py/tests/test_fs/test_ext4l.py +++ b/test/py/tests/test_fs/test_ext4l.py @@ -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 -- 2.43.0