blob: 4a0f50fcb1ec44669e4e1f146b514d6fe2f8ea77 [file] [log] [blame]
/*
* Copyright (c) 2025 Antmicro <www.antmicro.com>
*
* SPDX-License-Identifier: Apache-2.0
*/
#include <zephyr/init.h>
#include <zephyr/fs/fs.h>
#include <zephyr/fs/fs_sys.h>
#include <zephyr/fs/virtiofs.h>
#include <zephyr/posix/fcntl.h>
#include "../fs_impl.h"
#include <string.h>
#include "virtiofs.h"
#define MODE_FTYPE_MASK 0170000
#define MODE_FTYPE_DIR 040000
#define DT_DIR 4
#define DT_REG 8
struct virtiofs_file {
uint64_t fh;
uint64_t nodeid;
uint64_t offset;
uint32_t open_flags;
};
struct virtiofs_dir {
uint64_t fh;
uint64_t nodeid;
uint64_t offset;
};
K_MEM_SLAB_DEFINE_STATIC(
file_struct_slab, sizeof(struct virtiofs_file), CONFIG_VIRTIOFS_MAX_FILES, sizeof(void *)
);
K_MEM_SLAB_DEFINE_STATIC(
dir_struct_slab, sizeof(struct virtiofs_dir), CONFIG_VIRTIOFS_MAX_FILES, sizeof(void *)
);
static int zephyr_mode_to_posix(int m)
{
int mode = (m & FS_O_CREATE) ? O_CREAT : 0;
mode |= (m & FS_O_APPEND) ? O_APPEND : 0;
mode |= (m & FS_O_TRUNC) ? O_TRUNC : 0;
switch (m & FS_O_MODE_MASK) {
case FS_O_READ:
mode |= O_RDONLY;
break;
case FS_O_WRITE:
mode |= O_WRONLY;
break;
case FS_O_RDWR:
mode |= O_RDWR;
break;
default:
break;
}
return mode;
}
static const char *virtiofs_strip_prefix(const char *path, const struct fs_mount_t *mp)
{
const char *virtiofs_path = fs_impl_strip_prefix(path, mp);
if (virtiofs_path[0] == '/') {
virtiofs_path++;
}
return virtiofs_path;
}
static const char *strip_path(const char *fpath)
{
const char *c = fpath + strlen(fpath);
for (; c > fpath; c--) {
if (*c == '/') {
c++;
break;
}
}
return c;
}
/*
* despite the similarity of fuse/virtiofs to posix fs functions there are some notable differences:
* - open() is split into lookup+open in case of existing files and lookup+create in case of
* O_CREATE
* - opendir() is split into lookup+opendir
* - lookups are non-recursive, we have to traverse through each directory in the path
* - close()/closedir() is split into release+forget/releasedir+forget
* - read()/write()/readdir() takes offset as a parameter
* - there is sort of reverse stat() - settatr, that can be used to i.e. truncate the file
*/
static int virtiofs_zfs_open_existing(
struct fs_file_t *filp, struct fuse_entry_out lookup_ret, int flags)
{
struct fuse_open_out open_ret;
struct virtiofs_file *file;
int ret = virtiofs_open(
filp->mp->storage_dev, lookup_ret.nodeid, zephyr_mode_to_posix(flags), &open_ret,
FUSE_FILE
);
if (ret == 0) {
ret = k_mem_slab_alloc(&file_struct_slab, (void **)&file, K_NO_WAIT);
if (ret != 0) {
virtiofs_release(
filp->mp->storage_dev, lookup_ret.nodeid, open_ret.fh, FUSE_FILE
);
return ret;
}
file->fh = open_ret.fh;
file->nodeid = lookup_ret.nodeid;
file->offset = 0;
file->open_flags = flags;
filp->filep = file;
}
return ret;
}
static int virtiofs_zfs_open_create(
struct fs_file_t *filp, int flags, const char *path, uint64_t parent_inode)
{
struct fuse_create_out create_ret;
const char *fname = strip_path(path);
struct virtiofs_file *file;
int ret = virtiofs_create(
filp->mp->storage_dev, parent_inode, fname,
zephyr_mode_to_posix(flags), CONFIG_VIRTIOFS_CREATE_MODE_VALUE, &create_ret
);
if (ret == 0) {
ret = k_mem_slab_alloc(&file_struct_slab, (void **)&file, K_NO_WAIT);
if (ret != 0) {
virtiofs_release(
filp->mp->storage_dev, create_ret.entry_out.nodeid,
create_ret.open_out.fh, FUSE_FILE
);
return ret;
}
file->fh = create_ret.open_out.fh;
file->nodeid = create_ret.entry_out.nodeid;
file->offset = 0;
file->open_flags = flags;
filp->filep = file;
}
return ret;
}
static int virtiofs_zfs_open(struct fs_file_t *filp, const char *fs_path, fs_mode_t flags)
{
int ret = 0;
struct fuse_entry_out lookup_ret;
const char *path = virtiofs_strip_prefix(fs_path, filp->mp);
uint64_t parent_inode = FUSE_ROOT_INODE;
ret = virtiofs_lookup(
filp->mp->storage_dev, FUSE_ROOT_INODE, path, &lookup_ret, &parent_inode
);
if (ret == 0) {
ret = virtiofs_zfs_open_existing(filp, lookup_ret, flags & ~FS_O_CREATE);
} else if ((flags & FS_O_CREATE) && parent_inode != 0) {
ret = virtiofs_zfs_open_create(filp, flags, path, parent_inode);
} else {
if (parent_inode != 0) {
virtiofs_forget(filp->mp->storage_dev, parent_inode, 1);
}
return ret;
}
if (parent_inode != 0) {
virtiofs_forget(filp->mp->storage_dev, parent_inode, 1);
}
if (ret != 0) {
virtiofs_forget(filp->mp->storage_dev, lookup_ret.nodeid, 1);
}
return ret;
}
static int virtiofs_zfs_close(struct fs_file_t *filp)
{
struct virtiofs_file *file = filp->filep;
uint64_t nodeid = file->nodeid;
int ret = virtiofs_release(filp->mp->storage_dev, file->nodeid, file->fh, FUSE_FILE);
if (ret == 0) {
k_mem_slab_free(&file_struct_slab, file);
} else {
return ret;
}
virtiofs_forget(filp->mp->storage_dev, nodeid, 1);
return 0;
}
static ssize_t virtiofs_zfs_read(struct fs_file_t *filp, void *dest, size_t nbytes)
{
struct virtiofs_file *file = filp->filep;
int read_c = virtiofs_read(
filp->mp->storage_dev, file->nodeid, file->fh, file->offset, nbytes, dest
);
if (read_c >= 0) {
file->offset += read_c;
}
return read_c;
}
#define FUSE_SEEK_SET 0
#define FUSE_SEEK_CUR 1
#define FUSE_SEEK_END 2
static int zephyr_whence_to_posix(int whence)
{
switch (whence) {
case SEEK_SET:
return FUSE_SEEK_SET;
case SEEK_CUR:
return FUSE_SEEK_CUR;
case SEEK_END:
return FUSE_SEEK_END;
default:
return whence;
}
}
static int virtio_zfs_lseek(struct fs_file_t *filp, off_t off, int whence)
{
struct virtiofs_file *file = filp->filep;
struct fuse_lseek_out lseek_ret;
uint64_t off_arg = off;
whence = zephyr_whence_to_posix(whence);
/*
* SEEK_CUR is kind of broken with FUSE_LSEEK as reads/writes don't update the file
* offset on the host side so if we never used FUSE_LSEEK since opening the file, but
* did some reads/writes in the meantime and then used FUSE_LSEEK with SEEK_CUR+x,
* the returned offset would've been x instead of sum of read/written bytes + x. One
* solution is to pair each read/write with lseek(SEEK_CUR, read_c/write_c) to keep
* the offset updated on the host side, but we just don't use SEEK_CUR+x and instead
* use SEEK_SET with file->offset+x. Essentially the only thing FUSE_LSEEK provides
* for us is bounds checking and easier handling of SEEK_END (otherwise we would have
* to use other fuse call to determine file size)
*/
if (whence == FUSE_SEEK_CUR) {
whence = FUSE_SEEK_SET;
off_arg = file->offset + off;
}
int ret = virtiofs_lseek(
filp->mp->storage_dev, file->nodeid, file->fh, off_arg, whence, &lseek_ret
);
if (ret != 0) {
return ret;
}
file->offset = lseek_ret.offset;
return file->offset;
}
static ssize_t virtio_zfs_write(struct fs_file_t *filp, const void *src, size_t nbytes)
{
struct virtiofs_file *file = filp->filep;
struct virtiofs_fs_data *fs_data = filp->mp->fs_data;
const uint8_t *curr_addr = src;
int write_c = 0;
while (nbytes > 0) {
/*
* max write size is limited to max_write from fuse_init_out received during fs
* initalization, so we have to split bigger writes into multiple smaller ones.
* If we try to write more the recent virtiofsd it will return 12 (Not enough
* space), but the older one will assert, rendering fs unusable until restart.
*/
size_t curr_size = MIN(nbytes, fs_data->max_write);
/*
* while FUSE_WRITE will always write to the end if O_APPEND was passed when opening
* file (ignoring offset param) the file offset itself will remain unmodified on
* zephyr side, so we have to update it here
*/
if (file->open_flags & FS_O_APPEND) {
virtio_zfs_lseek(filp, 0, SEEK_END);
}
int ret = virtiofs_write(
filp->mp->storage_dev, file->nodeid, file->fh, file->offset + write_c,
curr_size, curr_addr
);
if (ret >= 0) {
write_c += ret;
} else {
/*
* according to fs_write comment in fs.h zephyr handles partial
* failures like that
*/
if (write_c > 0) {
errno = ret;
file->offset += write_c;
return write_c;
} else {
return ret;
}
}
nbytes -= curr_size;
curr_addr += curr_size;
}
file->offset += write_c;
return write_c;
}
static off_t virtiofs_zfs_tell(struct fs_file_t *filp)
{
struct virtiofs_file *file = filp->filep;
return file->offset;
}
static int virtiofs_zfs_truncate(struct fs_file_t *filp, off_t length)
{
struct virtiofs_file *file = filp->filep;
struct fuse_setattr_in attrs;
struct fuse_attr_out setattr_ret;
attrs.fh = file->fh;
attrs.size = length;
attrs.valid = FATTR_SIZE;
return virtiofs_setattr(filp->mp->storage_dev, file->nodeid, &attrs, &setattr_ret);
}
static int virtiofs_zfs_sync(struct fs_file_t *filp)
{
struct virtiofs_file *file = filp->filep;
return virtiofs_fsync(filp->mp->storage_dev, file->nodeid, file->fh);
}
static int virtiofs_zfs_mkdir(struct fs_mount_t *mountp, const char *name)
{
const char *path = virtiofs_strip_prefix(name, mountp);
struct fuse_entry_out lookup_ret;
uint64_t parent_inode = FUSE_ROOT_INODE;
int ret = virtiofs_lookup(
mountp->storage_dev, FUSE_ROOT_INODE, path, &lookup_ret, &parent_inode
);
if (parent_inode != 0) {
ret = virtiofs_mkdir(
mountp->storage_dev, parent_inode, strip_path(name),
CONFIG_VIRTIOFS_CREATE_MODE_VALUE
);
virtiofs_forget(mountp->storage_dev, parent_inode, 1);
}
return ret;
}
static int virtiofs_zfs_opendir(struct fs_dir_t *dirp, const char *fs_path)
{
int ret = 0;
struct virtiofs_dir *dir;
struct fuse_entry_out lookup_ret;
const char *path = virtiofs_strip_prefix(fs_path, dirp->mp);
if (path[0] == '\0') {
/* looking up for "" or "/" yields nothing, so we have to use "." for root dir */
path = ".";
}
ret = virtiofs_lookup(dirp->mp->storage_dev, FUSE_ROOT_INODE, path, &lookup_ret, NULL);
if (ret == 0) {
struct fuse_open_out open_ret;
ret = virtiofs_open(
dirp->mp->storage_dev, lookup_ret.nodeid, O_RDONLY, &open_ret, FUSE_DIR
);
if (ret == 0) {
ret = k_mem_slab_alloc(&dir_struct_slab, (void **)&dir, K_NO_WAIT);
if (ret != 0) {
virtiofs_forget(dirp->mp->storage_dev, lookup_ret.nodeid, 1);
return ret;
}
dir->fh = open_ret.fh;
dir->nodeid = lookup_ret.nodeid;
dir->offset = 0;
dirp->dirp = dir;
}
} else {
virtiofs_forget(dirp->mp->storage_dev, lookup_ret.nodeid, 1);
}
return ret;
}
static int virtiofs_zfs_readdir(struct fs_dir_t *dirp, struct fs_dirent *entry)
{
struct virtiofs_dir *dir = dirp->dirp;
struct fuse_dirent de;
int read_c = virtiofs_readdir(
dirp->mp->storage_dev, dir->nodeid, dir->fh, dir->offset,
(uint8_t *)&de, sizeof(de), (uint8_t *)&entry->name, sizeof(entry->name)
);
/* end of dir */
if (read_c == 0) {
entry->name[0] = '\0';
return 0;
}
if (read_c < sizeof(de) || de.namelen >= sizeof(entry->name) - 1) {
return -EIO;
}
/*
* usually name is already null terminated, but sometimes name of the last entry
* in directory is not (maybe also in other cases), so we null terminate it here
*/
entry->name[de.namelen] = '\0';
dir->offset = de.off;
if (de.type == DT_REG) {
struct fuse_entry_out lookup_ret;
int ret = virtiofs_lookup(
dirp->mp->storage_dev, dir->nodeid, entry->name, &lookup_ret, NULL
);
if (ret != 0) {
return ret;
}
virtiofs_forget(dirp->mp->storage_dev, lookup_ret.nodeid, 1);
entry->type = FS_DIR_ENTRY_FILE;
entry->size = lookup_ret.attr.size;
} else if (de.type == DT_DIR) {
entry->type = FS_DIR_ENTRY_DIR;
entry->size = 0;
} else {
return -ENOTSUP;
}
return 0;
}
static int virtiofs_zfs_closedir(struct fs_dir_t *dirp)
{
struct virtiofs_dir *dir = dirp->dirp;
uint64_t nodeid = dir->nodeid;
int ret = virtiofs_release(dirp->mp->storage_dev, dir->nodeid, dir->fh, FUSE_DIR);
if (ret == 0) {
k_mem_slab_free(&dir_struct_slab, dir);
} else {
return ret;
}
virtiofs_forget(dirp->mp->storage_dev, nodeid, 1);
return 0;
}
static int virtiofs_zfs_mount(struct fs_mount_t *mountp)
{
struct fuse_init_out out;
int ret = virtiofs_init(mountp->storage_dev, &out);
if (ret == 0) {
struct virtiofs_fs_data *fs_data = mountp->fs_data;
fs_data->max_write = out.max_write;
}
return ret;
}
static int virtiofs_zfs_unmount(struct fs_mount_t *mountp)
{
return virtiofs_destroy(mountp->storage_dev);
}
static int virtiofs_zfs_stat(
struct fs_mount_t *mountp, const char *fs_path, struct fs_dirent *entry)
{
const char *path = virtiofs_strip_prefix(fs_path, mountp);
const char *name = strip_path(fs_path);
if (strlen(name) + 1 > sizeof(entry->name)) {
return -ENOBUFS;
}
struct fuse_entry_out lookup_ret;
int ret = virtiofs_lookup(mountp->storage_dev, FUSE_ROOT_INODE, path, &lookup_ret, NULL);
if (ret != 0) {
return ret;
}
strcpy((char *)&entry->name, name);
if ((lookup_ret.attr.mode & MODE_FTYPE_MASK) == MODE_FTYPE_DIR) {
entry->type = FS_DIR_ENTRY_DIR;
entry->size = 0;
} else {
entry->type = FS_DIR_ENTRY_FILE;
entry->size = lookup_ret.attr.size;
}
virtiofs_forget(mountp->storage_dev, lookup_ret.nodeid, 1);
return 0;
}
static int virtiofs_zfs_unlink(struct fs_mount_t *mountp, const char *name)
{
const char *path = virtiofs_strip_prefix(name, mountp);
struct fs_dirent d;
int ret = virtiofs_zfs_stat(mountp, name, &d);
if (ret != 0) {
return ret;
}
if (d.type == FS_DIR_ENTRY_FILE) {
#ifdef CONFIG_VIRTIOFS_VIRTIOFSD_UNLINK_QUIRK
struct fuse_entry_out lookup_ret;
/*
* Even if unlink doesn't take nodeid as a param it still fails with -EIO if the
* file wasn't looked up using some virtiofsd versions. It happens at least with
* the one from Debian's package (Debian 1:7.2+dfsg-7+deb12u7). Virtiofsd 1.12.0
* built from sources doesn't need it
*/
ret = virtiofs_lookup(
mountp->storage_dev, FUSE_ROOT_INODE, path, &lookup_ret, NULL
);
if (ret != 0) {
return ret;
}
#endif
ret = virtiofs_unlink(mountp->storage_dev, path, FUSE_FILE);
#ifdef CONFIG_VIRTIOFS_VIRTIOFSD_UNLINK_QUIRK
virtiofs_forget(mountp->storage_dev, lookup_ret.nodeid, 1);
#endif
} else {
ret = virtiofs_unlink(mountp->storage_dev, path, FUSE_DIR);
}
return ret;
}
static int virtiofs_zfs_rename(struct fs_mount_t *mountp, const char *from, const char *to)
{
const char *old_path = virtiofs_strip_prefix(from, mountp);
const char *new_path = virtiofs_strip_prefix(to, mountp);
uint64_t old_dir = FUSE_ROOT_INODE;
uint64_t new_dir = FUSE_ROOT_INODE;
struct fuse_entry_out old_lookup_ret;
struct fuse_entry_out new_lookup_ret;
int ret;
ret = virtiofs_lookup(
mountp->storage_dev, FUSE_ROOT_INODE, old_path, &old_lookup_ret, &old_dir
);
if (ret != 0) {
if (old_dir != 0 && old_dir != FUSE_ROOT_INODE) {
virtiofs_forget(mountp->storage_dev, old_dir, 1);
}
return ret;
}
ret = virtiofs_lookup(
mountp->storage_dev, FUSE_ROOT_INODE, new_path, &new_lookup_ret, &new_dir
);
/* there is no immediate parent of object's new path */
if (ret != 0 && new_dir == 0) {
virtiofs_forget(mountp->storage_dev, old_lookup_ret.nodeid, 1);
return ret;
}
ret = virtiofs_rename(
mountp->storage_dev,
old_dir, strip_path(old_path),
new_dir, strip_path(new_path)
);
virtiofs_forget(mountp->storage_dev, old_lookup_ret.nodeid, 1);
virtiofs_forget(mountp->storage_dev, new_lookup_ret.nodeid, 1);
virtiofs_forget(mountp->storage_dev, old_dir, 1);
virtiofs_forget(mountp->storage_dev, new_dir, 1);
return ret;
}
static int virtiofs_zfs_statvfs(
struct fs_mount_t *mountp, const char *fs_path, struct fs_statvfs *stat)
{
struct fuse_kstatfs statfs_out;
int ret = virtiofs_statfs(mountp->storage_dev, &statfs_out);
if (ret != 0) {
return ret;
}
stat->f_bsize = statfs_out.bsize;
stat->f_frsize = statfs_out.frsize;
stat->f_blocks = statfs_out.blocks;
stat->f_bfree = statfs_out.bfree;
return 0;
}
static const struct fs_file_system_t virtiofs_ops = {
.open = virtiofs_zfs_open,
.close = virtiofs_zfs_close,
.read = virtiofs_zfs_read,
.write = virtio_zfs_write,
.lseek = virtio_zfs_lseek,
.tell = virtiofs_zfs_tell,
.truncate = virtiofs_zfs_truncate,
.sync = virtiofs_zfs_sync,
.mkdir = virtiofs_zfs_mkdir,
.opendir = virtiofs_zfs_opendir,
.readdir = virtiofs_zfs_readdir,
.closedir = virtiofs_zfs_closedir,
.mount = virtiofs_zfs_mount,
.unmount = virtiofs_zfs_unmount,
.unlink = virtiofs_zfs_unlink,
.rename = virtiofs_zfs_rename,
.stat = virtiofs_zfs_stat,
.statvfs = virtiofs_zfs_statvfs
};
static int virtiofs_register(void)
{
return fs_register(FS_VIRTIOFS, &virtiofs_ops);
}
SYS_INIT(virtiofs_register, POST_KERNEL, 99);