ext4l: Add open/read/close directory

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>
This commit is contained in:
Simon Glass
2025-12-22 17:04:34 -07:00
parent 9806965e3e
commit b9558d8a06
6 changed files with 348 additions and 2 deletions

View File

@@ -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--;
}

View File

@@ -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,

View File

@@ -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__ */

View File

@@ -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 */

View 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 });

View File

@@ -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