629 lines
17 KiB
C
629 lines
17 KiB
C
// SPDX-License-Identifier: ISC
|
|
|
|
#include <zinnia/alloc.h>
|
|
#include <zinnia/assert.h>
|
|
#include <zinnia/atomic.h>
|
|
#include <zinnia/print.h>
|
|
#include <zinnia/vfs.h>
|
|
#include <stdckdint.h>
|
|
#include <string.h>
|
|
#include "internal.h"
|
|
|
|
errno_t vfs_file_new(struct vfs_node* node, const struct vfs_path* path, enum open_flags flags, struct vfs_file** out) {
|
|
assert(node != nullptr, "Every open file must be backed by an inode");
|
|
|
|
struct vfs_file* file = mem_alloc(sizeof(*file), 0);
|
|
if (!file)
|
|
return ENOMEM;
|
|
|
|
refcount_init(&file->rc);
|
|
file->ops = node->file_ops;
|
|
file->vnode = node;
|
|
vfs_node_ref(node);
|
|
atomic_init(&file->flags, flags);
|
|
mutex_init(&file->lock);
|
|
|
|
switch (node->type) {
|
|
case VNODE_REGULAR:
|
|
case VNODE_DIRECTORY:
|
|
file->position_kind = VFS_POSITION_ATOMIC;
|
|
break;
|
|
default:
|
|
file->position_kind = VFS_POSITION_POSITION;
|
|
break;
|
|
}
|
|
|
|
if (path) {
|
|
file->path = vfs_path_clone(*path);
|
|
file->has_path = true;
|
|
}
|
|
|
|
*out = file;
|
|
return 0;
|
|
}
|
|
|
|
void vfs_stream_open(struct vfs_file* file) {
|
|
file->position_kind = VFS_POSITION_STREAM;
|
|
}
|
|
|
|
// Resolves the absolute path of `path` as seen from `root` into a fitting allocation.
|
|
static char* make_absolute_path(struct vfs_path path, struct vfs_path* root) {
|
|
char* buf = mem_alloc(__PATH_MAX, ALLOC_NOZERO);
|
|
if (!buf)
|
|
return nullptr;
|
|
|
|
size_t len;
|
|
char* result = nullptr;
|
|
if (vfs_path_absolute(path, root, buf, __PATH_MAX, &len) == 0) {
|
|
result = mem_alloc(len + 1, ALLOC_NOZERO);
|
|
if (result)
|
|
memcpy(result, buf, len + 1);
|
|
}
|
|
|
|
mem_free(buf);
|
|
return result;
|
|
}
|
|
|
|
static errno_t open_special(
|
|
struct vfs_path* root,
|
|
struct vfs_path* file_path,
|
|
struct vfs_node* node,
|
|
enum open_flags flags,
|
|
struct vfs_file** out
|
|
) {
|
|
if ((node->type == VNODE_CHARDEV || node->type == VNODE_BLOCKDEV) && (!node->ops || !node->ops->device.open))
|
|
return ENXIO;
|
|
|
|
struct vfs_file* file;
|
|
errno_t status = vfs_file_new(node, file_path, flags, &file);
|
|
if (status)
|
|
return status;
|
|
file->absolute_path = make_absolute_path(*file_path, root);
|
|
|
|
status = node->type == VNODE_FIFO ? vfs_fifo_open(node, file) : node->ops->device.open(node, file);
|
|
if (status) {
|
|
file->ops = nullptr;
|
|
vfs_file_unref(file);
|
|
return status;
|
|
}
|
|
|
|
*out = file;
|
|
return 0;
|
|
}
|
|
|
|
static enum access_flags open_access(enum open_flags flags) {
|
|
if (flags & O_PATH)
|
|
return F_OK;
|
|
|
|
enum access_flags access = F_OK;
|
|
if (flags & O_RDONLY)
|
|
access |= R_OK;
|
|
if (flags & (O_WRONLY | O_TRUNC))
|
|
access |= W_OK;
|
|
return access;
|
|
}
|
|
|
|
static errno_t do_open_inode(
|
|
struct vfs_path* root,
|
|
struct vfs_path* file_path,
|
|
struct vfs_node* node,
|
|
enum open_flags flags,
|
|
bool created,
|
|
const struct ident* identity,
|
|
struct vfs_file** out
|
|
) {
|
|
// If we want to open as a directory, make sure this is actually a directory.
|
|
if ((flags & O_DIRECTORY) && node->type != VNODE_DIRECTORY)
|
|
return ENOTDIR;
|
|
|
|
if (node->type == VNODE_DIRECTORY && (flags & (O_WRONLY | O_CREAT | O_TRUNC)))
|
|
return EISDIR;
|
|
|
|
errno_t status = created ? 0 : vfs_node_try_access(node, identity, open_access(flags), false);
|
|
if (status)
|
|
return status;
|
|
|
|
switch (node->type) {
|
|
case VNODE_REGULAR: {
|
|
if (flags & O_TRUNC) {
|
|
if (!(flags & O_WRONLY))
|
|
return EINVAL;
|
|
if (!node->ops->regular.truncate)
|
|
return EPERM;
|
|
status = node->ops->regular.truncate(node, 0);
|
|
if (status)
|
|
return status;
|
|
}
|
|
|
|
struct vfs_file* file;
|
|
status = vfs_file_new(node, file_path, flags, &file);
|
|
if (status)
|
|
return status;
|
|
file->absolute_path = make_absolute_path(*file_path, root);
|
|
*out = file;
|
|
return 0;
|
|
}
|
|
case VNODE_DIRECTORY:
|
|
if (node->ops->dir.open)
|
|
return node->ops->dir.open(node, file_path, flags, identity, out);
|
|
return vfs_file_new(node, file_path, flags, out);
|
|
case VNODE_SYMLINK:
|
|
return ELOOP;
|
|
case VNODE_CHARDEV:
|
|
case VNODE_BLOCKDEV:
|
|
case VNODE_FIFO:
|
|
return open_special(root, file_path, node, flags, out);
|
|
case VNODE_SOCKET:
|
|
return ENXIO;
|
|
default:
|
|
return ENOTSUP;
|
|
}
|
|
}
|
|
|
|
errno_t vfs_file_open(
|
|
struct vfs_path root,
|
|
struct vfs_path cwd,
|
|
const char* path,
|
|
enum open_flags flags,
|
|
mode_t mode,
|
|
const struct ident* identity,
|
|
struct vfs_file** out
|
|
) {
|
|
if ((flags & O_DIRECTORY) && (flags & (O_CREAT | O_TMPFILE)))
|
|
return EINVAL;
|
|
|
|
const bool exclusive = (flags & (O_CREAT | O_EXCL)) == (O_CREAT | O_EXCL);
|
|
|
|
enum vfs_lookup_flags lookup_flags = 0;
|
|
// If we don't want to create a file when opening, the lookup must succeed.
|
|
// If we want to create a temporary unnamed file, `path` is the directory of the owning FS.
|
|
// In that case the path also has to exist.
|
|
if (!(flags & O_CREAT) || (flags & O_TMPFILE))
|
|
lookup_flags |= VFS_LOOKUP_MUST_EXIST;
|
|
if (exclusive)
|
|
lookup_flags |= VFS_LOOKUP_MUST_NOT_EXIST;
|
|
if (!(flags & O_NOFOLLOW) && !exclusive)
|
|
lookup_flags |= VFS_LOOKUP_FOLLOW_SYMLINKS;
|
|
|
|
struct vfs_path file_path;
|
|
errno_t status = vfs_path_lookup(root, cwd, path, identity, lookup_flags, &file_path);
|
|
if (status)
|
|
return status;
|
|
|
|
bool created = false;
|
|
struct vfs_node* node = vfs_entry_get_inode(file_path.entry);
|
|
if (!node) {
|
|
// If the lookup was successful, we expect that the entry is positive.
|
|
if (!(flags & O_CREAT)) {
|
|
log_warn("vfs: Tried opening a file without O_CREAT and backing inode, this is a bug!\n");
|
|
status = ENOENT;
|
|
goto out;
|
|
}
|
|
|
|
struct vfs_path parent_path;
|
|
status = vfs_path_lookup_parent(file_path, &parent_path);
|
|
if (status)
|
|
goto out;
|
|
struct vfs_node* parent = vfs_entry_get_inode(parent_path.entry);
|
|
vfs_path_drop(parent_path);
|
|
if (!parent) {
|
|
status = ENOENT;
|
|
goto out;
|
|
}
|
|
|
|
status = parent->type == VNODE_DIRECTORY ? 0 : ENOTDIR;
|
|
if (!status)
|
|
status = vfs_node_try_access(parent, identity, W_OK | X_OK, false);
|
|
if (!status)
|
|
status = parent->ops->dir.create ? parent->ops->dir.create(parent, file_path.entry, mode, identity) : EPERM;
|
|
vfs_node_unref(parent);
|
|
if (status)
|
|
goto out;
|
|
|
|
node = vfs_entry_get_inode(file_path.entry);
|
|
if (!node) {
|
|
status = EIO;
|
|
goto out;
|
|
}
|
|
created = true;
|
|
}
|
|
|
|
status = do_open_inode(&root, &file_path, node, flags, created, identity, out);
|
|
vfs_node_unref(node);
|
|
|
|
out:
|
|
vfs_path_drop(file_path);
|
|
return status;
|
|
}
|
|
|
|
static errno_t advance_offset(off_t* offset, ssize_t amount) {
|
|
if (amount > 0 && ckd_add(offset, *offset, amount))
|
|
return EOVERFLOW;
|
|
return 0;
|
|
}
|
|
|
|
errno_t vfs_file_read(struct vfs_file* file, iovec_iter_t* buffer, ssize_t* out_read) {
|
|
*out_read = 0;
|
|
if (iovec_iter_len(buffer) == 0)
|
|
return 0;
|
|
if (!file->ops || !file->ops->read)
|
|
return EINVAL;
|
|
|
|
errno_t status;
|
|
ssize_t read = 0;
|
|
switch (file->position_kind) {
|
|
case VFS_POSITION_STREAM:
|
|
status = file->ops->read(file, buffer, 0, &read);
|
|
break;
|
|
case VFS_POSITION_ATOMIC:
|
|
mutex_lock(&file->lock);
|
|
status = file->ops->read(file, buffer, file->position, &read);
|
|
if (!status)
|
|
status = advance_offset(&file->position, read);
|
|
mutex_unlock(&file->lock);
|
|
break;
|
|
case VFS_POSITION_POSITION: {
|
|
mutex_lock(&file->lock);
|
|
off_t pos = file->position;
|
|
mutex_unlock(&file->lock);
|
|
|
|
status = file->ops->read(file, buffer, pos, &read);
|
|
if (!status)
|
|
status = advance_offset(&pos, read);
|
|
if (!status) {
|
|
mutex_lock(&file->lock);
|
|
file->position = pos;
|
|
mutex_unlock(&file->lock);
|
|
}
|
|
break;
|
|
}
|
|
default:
|
|
return EINVAL;
|
|
}
|
|
|
|
if (status)
|
|
return status;
|
|
*out_read = read;
|
|
return 0;
|
|
}
|
|
|
|
errno_t vfs_file_pread(struct vfs_file* file, iovec_iter_t* buffer, uint64_t offset, ssize_t* out_read) {
|
|
*out_read = 0;
|
|
if (iovec_iter_len(buffer) == 0)
|
|
return 0;
|
|
if (file->position_kind == VFS_POSITION_STREAM)
|
|
return ESPIPE;
|
|
if (!file->ops || !file->ops->read)
|
|
return EINVAL;
|
|
|
|
return file->ops->read(file, buffer, offset, out_read);
|
|
}
|
|
|
|
static errno_t check_write_progress(ssize_t written) {
|
|
if (written == 0) {
|
|
log_warn("vfs: write reported zero progress for a non-empty buffer\n");
|
|
return EIO;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
static errno_t sync_after_write(struct vfs_file* file, ssize_t written) {
|
|
if (written <= 0)
|
|
return 0;
|
|
|
|
if ((atomic_load(&file->flags, ATOMIC_RELAXED) & O_SYNC) == O_SYNC)
|
|
return vfs_file_sync(file, false);
|
|
if ((atomic_load(&file->flags, ATOMIC_RELAXED) & O_DSYNC) == O_DSYNC)
|
|
return vfs_file_sync(file, true);
|
|
return 0;
|
|
}
|
|
|
|
errno_t vfs_file_write(struct vfs_file* file, iovec_iter_t* buffer, ssize_t* out_written) {
|
|
*out_written = 0;
|
|
if (iovec_iter_len(buffer) == 0)
|
|
return 0;
|
|
if (!file->ops || !file->ops->write)
|
|
return EINVAL;
|
|
|
|
// TODO: Appending writes through different files of the same inode aren't serialized.
|
|
const bool append = atomic_load(&file->flags, ATOMIC_RELAXED) & O_APPEND;
|
|
|
|
errno_t status;
|
|
ssize_t written = 0;
|
|
switch (file->position_kind) {
|
|
case VFS_POSITION_STREAM:
|
|
status = file->ops->write(file, buffer, 0, &written);
|
|
break;
|
|
case VFS_POSITION_ATOMIC:
|
|
mutex_lock(&file->lock);
|
|
if (append)
|
|
file->position = file->vnode->size;
|
|
status = file->ops->write(file, buffer, file->position, &written);
|
|
if (!status)
|
|
status = advance_offset(&file->position, written);
|
|
mutex_unlock(&file->lock);
|
|
break;
|
|
case VFS_POSITION_POSITION: {
|
|
mutex_lock(&file->lock);
|
|
off_t pos = append ? (off_t)file->vnode->size : file->position;
|
|
mutex_unlock(&file->lock);
|
|
|
|
status = file->ops->write(file, buffer, pos, &written);
|
|
if (!status)
|
|
status = advance_offset(&pos, written);
|
|
if (!status) {
|
|
mutex_lock(&file->lock);
|
|
file->position = pos;
|
|
mutex_unlock(&file->lock);
|
|
}
|
|
break;
|
|
}
|
|
default:
|
|
return EINVAL;
|
|
}
|
|
|
|
if (!status)
|
|
status = check_write_progress(written);
|
|
if (!status)
|
|
status = sync_after_write(file, written);
|
|
if (status)
|
|
return status;
|
|
|
|
*out_written = written;
|
|
return 0;
|
|
}
|
|
|
|
errno_t vfs_file_pwrite(struct vfs_file* file, iovec_iter_t* buffer, uint64_t offset, ssize_t* out_written) {
|
|
*out_written = 0;
|
|
if (iovec_iter_len(buffer) == 0)
|
|
return 0;
|
|
if (file->position_kind == VFS_POSITION_STREAM)
|
|
return ESPIPE;
|
|
if (!file->ops || !file->ops->write)
|
|
return EINVAL;
|
|
|
|
ssize_t written = 0;
|
|
errno_t status = file->ops->write(file, buffer, offset, &written);
|
|
if (!status)
|
|
status = check_write_progress(written);
|
|
if (!status)
|
|
status = sync_after_write(file, written);
|
|
if (status)
|
|
return status;
|
|
|
|
*out_written = written;
|
|
return 0;
|
|
}
|
|
|
|
errno_t vfs_file_ioctl(struct vfs_file* file, uintptr_t request, uintptr_t arg, uintptr_t* out_result) {
|
|
if (!file->ops || !file->ops->ioctl)
|
|
return ENOTTY;
|
|
return file->ops->ioctl(file, request, arg, out_result);
|
|
}
|
|
|
|
errno_t vfs_file_sync(struct vfs_file* file, bool data_only) {
|
|
if (!file->ops || !file->ops->sync)
|
|
return 0;
|
|
return file->ops->sync(file, data_only);
|
|
}
|
|
|
|
errno_t vfs_file_seek(struct vfs_file* file, enum vfs_seek_anchor anchor, off_t offset, off_t* out_position) {
|
|
if (file->position_kind == VFS_POSITION_STREAM)
|
|
return ESPIPE;
|
|
|
|
errno_t status = 0;
|
|
mutex_lock(&file->lock);
|
|
|
|
off_t base;
|
|
switch (anchor) {
|
|
case VFS_SEEK_START:
|
|
base = 0;
|
|
break;
|
|
case VFS_SEEK_CURRENT:
|
|
base = file->position;
|
|
break;
|
|
case VFS_SEEK_END:
|
|
base = file->vnode->size;
|
|
break;
|
|
default:
|
|
status = EINVAL;
|
|
goto out;
|
|
}
|
|
|
|
off_t new_position;
|
|
if (ckd_add(&new_position, base, offset)) {
|
|
status = EOVERFLOW;
|
|
goto out;
|
|
}
|
|
if (new_position < 0) {
|
|
status = EINVAL;
|
|
goto out;
|
|
}
|
|
|
|
file->position = new_position;
|
|
if (out_position)
|
|
*out_position = new_position;
|
|
|
|
out:
|
|
mutex_unlock(&file->lock);
|
|
return status;
|
|
}
|
|
|
|
off_t vfs_file_tell(struct vfs_file* file) {
|
|
mutex_lock(&file->lock);
|
|
off_t position = file->position;
|
|
mutex_unlock(&file->lock);
|
|
return position;
|
|
}
|
|
|
|
errno_t vfs_file_mmap(
|
|
struct vfs_file* file,
|
|
struct vma* vma,
|
|
uintptr_t addr,
|
|
size_t len,
|
|
enum prot_flags prot,
|
|
enum map_flags flags,
|
|
off_t offset,
|
|
uintptr_t* out_addr
|
|
) {
|
|
if (!file->ops || !file->ops->mmap)
|
|
return ENODEV;
|
|
return file->ops->mmap(file, vma, addr, len, prot, flags, offset, out_addr);
|
|
}
|
|
|
|
enum poll_flags vfs_poll(struct vfs_file* file, enum poll_flags events, struct poll_table* table) {
|
|
const enum poll_flags always = POLLERR | POLLHUP | POLLNVAL;
|
|
if (!file->ops || !file->ops->poll)
|
|
return events & (POLLIN | POLLOUT | POLLRDNORM | POLLWRNORM);
|
|
return file->ops->poll(file, events | POLLERR | POLLHUP, table) & (events | always);
|
|
}
|
|
|
|
errno_t vfs_file_get_object(struct vfs_file* file, struct vmo** out) {
|
|
struct vfs_node* node = file->vnode;
|
|
if (node->type != VNODE_REGULAR || !node->ops || !node->ops->regular.get_vmo)
|
|
return ENODEV;
|
|
return node->ops->regular.get_vmo(node, out);
|
|
}
|
|
|
|
static uint8_t dirent_type(enum vfs_node_type type) {
|
|
switch (type) {
|
|
case VNODE_REGULAR:
|
|
return __DT_REG;
|
|
case VNODE_DIRECTORY:
|
|
return __DT_DIR;
|
|
case VNODE_SYMLINK:
|
|
return __DT_LNK;
|
|
case VNODE_FIFO:
|
|
return __DT_FIFO;
|
|
case VNODE_BLOCKDEV:
|
|
return __DT_BLK;
|
|
case VNODE_CHARDEV:
|
|
return __DT_CHR;
|
|
case VNODE_SOCKET:
|
|
return __DT_SOCK;
|
|
default:
|
|
// Should be unreachable, but just as a failsafe in case someone messes up.
|
|
return __DT_UNKNOWN;
|
|
}
|
|
}
|
|
|
|
errno_t vfs_dir_get_cached_entries(
|
|
struct vfs_node* node,
|
|
struct vfs_entry* entry,
|
|
off_t offset,
|
|
struct __dirent* buffer,
|
|
size_t buffer_len,
|
|
const struct ident* identity,
|
|
size_t* out_count
|
|
) {
|
|
size_t read = 0;
|
|
off_t current = 0;
|
|
|
|
mutex_lock(&entry->lock);
|
|
for (struct vfs_entry* child = tailq_first(&entry->children); child && read < buffer_len;
|
|
child = tailq_next(child, child_link)) {
|
|
struct vfs_node* child_node = vfs_entry_get_inode(child);
|
|
if (!child_node)
|
|
continue;
|
|
|
|
if (current < offset) {
|
|
current++;
|
|
vfs_node_unref(child_node);
|
|
continue;
|
|
}
|
|
|
|
struct __dirent* d = &buffer[read];
|
|
d->d_ino = child_node->id;
|
|
d->d_off = current;
|
|
d->d_reclen = sizeof(*d);
|
|
d->d_type = dirent_type(child_node->type);
|
|
memcpy(d->d_name, child->name, child->name_len);
|
|
d->d_name[child->name_len] = '\0';
|
|
vfs_node_unref(child_node);
|
|
|
|
read++;
|
|
current++;
|
|
}
|
|
mutex_unlock(&entry->lock);
|
|
|
|
*out_count = read;
|
|
return 0;
|
|
}
|
|
|
|
static void fill_dot_entry(struct __dirent* d, ino_t ino, off_t off, const char* name) {
|
|
d->d_ino = ino;
|
|
d->d_off = off;
|
|
d->d_reclen = sizeof(*d);
|
|
d->d_type = __DT_DIR;
|
|
memset(d->d_name, 0, sizeof(d->d_name));
|
|
memcpy(d->d_name, name, strlen(name));
|
|
}
|
|
|
|
errno_t vfs_get_dir_entries(
|
|
struct vfs_file* file,
|
|
struct __dirent* buffer,
|
|
size_t buffer_len,
|
|
const struct ident* identity,
|
|
size_t* out_count
|
|
) {
|
|
*out_count = 0;
|
|
if (buffer_len == 0)
|
|
return 0;
|
|
|
|
struct vfs_node* node = file->vnode;
|
|
if (!file->has_path || node->type != VNODE_DIRECTORY)
|
|
return ENOTDIR;
|
|
if (file->position_kind == VFS_POSITION_STREAM)
|
|
return EBADF;
|
|
|
|
ino_t parent_ino = node->id;
|
|
struct vfs_path parent;
|
|
if (vfs_path_lookup_parent(file->path, &parent) == 0) {
|
|
struct vfs_node* parent_node = vfs_entry_get_inode(parent.entry);
|
|
if (parent_node) {
|
|
parent_ino = parent_node->id;
|
|
vfs_node_unref(parent_node);
|
|
}
|
|
vfs_path_drop(parent);
|
|
}
|
|
|
|
errno_t status = 0;
|
|
mutex_lock(&file->lock);
|
|
|
|
const off_t start_offset = file->position;
|
|
size_t num_read = 0;
|
|
|
|
// Read `.` and `..` entries.
|
|
if (start_offset == 0)
|
|
fill_dot_entry(&buffer[num_read++], node->id, 0, ".");
|
|
if ((start_offset == 1 || num_read == 1) && num_read < buffer_len)
|
|
fill_dot_entry(&buffer[num_read++], parent_ino, 1, "..");
|
|
|
|
// Read normal dir entries.
|
|
if (num_read < buffer_len && node->ops->dir.get_dir_entries) {
|
|
size_t count = 0;
|
|
status = node->ops->dir.get_dir_entries(
|
|
node,
|
|
file->path.entry,
|
|
start_offset > 2 ? start_offset - 2 : 0,
|
|
buffer + num_read,
|
|
buffer_len - num_read,
|
|
identity,
|
|
&count
|
|
);
|
|
if (status)
|
|
goto out;
|
|
num_read += count;
|
|
}
|
|
|
|
file->position += num_read;
|
|
for (size_t i = 0; i < num_read; i++)
|
|
buffer[i].d_off = start_offset + i + 1;
|
|
|
|
*out_count = num_read;
|
|
|
|
out:
|
|
mutex_unlock(&file->lock);
|
|
return status;
|
|
}
|