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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user