657 lines
18 KiB
C
657 lines
18 KiB
C
// SPDX-License-Identifier: ISC
|
|
|
|
#include <zinnia/clock.h>
|
|
#include <zinnia/vfs.h>
|
|
#include <string.h>
|
|
|
|
static bool is_read_only(struct vfs_node* node) {
|
|
return node->sb && (node->sb->flags & MNT_RDONLY);
|
|
}
|
|
|
|
errno_t vfs_node_try_access(
|
|
struct vfs_node* node,
|
|
const struct ident* identity,
|
|
enum access_flags access,
|
|
bool use_real
|
|
) {
|
|
mutex_lock(&node->lock);
|
|
const mode_t mode = node->mode;
|
|
const uid_t owner = node->uid;
|
|
const gid_t group = node->gid;
|
|
mutex_unlock(&node->lock);
|
|
|
|
if ((access & W_OK) && is_read_only(node) &&
|
|
(node->type == VNODE_REGULAR || node->type == VNODE_DIRECTORY || node->type == VNODE_SYMLINK))
|
|
return EROFS;
|
|
if ((access & X_OK) && node->type == VNODE_REGULAR && node->sb && (node->sb->flags & MNT_NOEXEC))
|
|
return EACCES;
|
|
|
|
if (ident_is_privileged(identity, use_real)) {
|
|
if ((access & X_OK) && node->type != VNODE_DIRECTORY && !(mode & (__S_IXUSR | __S_IXGRP | __S_IXOTH)))
|
|
return EACCES;
|
|
return 0;
|
|
}
|
|
|
|
mode_t granted;
|
|
if ((use_real ? identity->uid : identity->euid) == owner)
|
|
granted = (mode >> 6) & 7;
|
|
else if (ident_in_group(identity, group, use_real))
|
|
granted = (mode >> 3) & 7;
|
|
else
|
|
granted = mode & 7;
|
|
|
|
return (access & (R_OK | W_OK | X_OK) & ~granted) ? EACCES : 0;
|
|
}
|
|
|
|
static errno_t sync_node(struct vfs_node* node) {
|
|
return node->ops && node->ops->sync ? node->ops->sync(node) : 0;
|
|
}
|
|
|
|
static timespec_t now() {
|
|
return time_from_nano(clock_get_realtime_ns());
|
|
}
|
|
|
|
// Looks up the parent directory of `path` and checks that `identity` may write to it.
|
|
// Returns a new reference to the parent inode in `out`.
|
|
static errno_t get_writable_parent(struct vfs_path path, const struct ident* identity, struct vfs_node** out) {
|
|
struct vfs_path parent;
|
|
errno_t status = vfs_path_lookup_parent(path, &parent);
|
|
if (status)
|
|
return status;
|
|
|
|
struct vfs_node* node = vfs_entry_get_inode(parent.entry);
|
|
vfs_path_drop(parent);
|
|
if (!node)
|
|
return ENOENT;
|
|
|
|
if (node->type != VNODE_DIRECTORY) {
|
|
vfs_node_unref(node);
|
|
return ENOTDIR;
|
|
}
|
|
|
|
status = vfs_node_try_access(node, identity, W_OK | X_OK, false);
|
|
if (status) {
|
|
vfs_node_unref(node);
|
|
return status;
|
|
}
|
|
|
|
*out = node;
|
|
return 0;
|
|
}
|
|
|
|
errno_t vfs_mkdir(
|
|
struct vfs_path root,
|
|
struct vfs_path cwd,
|
|
const char* path,
|
|
mode_t mode,
|
|
const struct ident* identity
|
|
) {
|
|
struct vfs_path target;
|
|
errno_t status = vfs_path_lookup(root, cwd, path, identity, VFS_LOOKUP_MUST_NOT_EXIST, &target);
|
|
if (status)
|
|
return status;
|
|
|
|
struct vfs_node* parent;
|
|
status = get_writable_parent(target, identity, &parent);
|
|
if (status)
|
|
goto out;
|
|
|
|
status = parent->ops->dir.mkdir ? parent->ops->dir.mkdir(parent, &target, mode, identity) : EPERM;
|
|
vfs_node_unref(parent);
|
|
|
|
out:
|
|
vfs_path_drop(target);
|
|
return status;
|
|
}
|
|
|
|
errno_t vfs_symlink(
|
|
struct vfs_path root,
|
|
struct vfs_path cwd,
|
|
const char* path,
|
|
const char* target_path,
|
|
const struct ident* identity
|
|
) {
|
|
struct vfs_path target;
|
|
errno_t status = vfs_path_lookup(root, cwd, path, identity, VFS_LOOKUP_MUST_NOT_EXIST, &target);
|
|
if (status)
|
|
return status;
|
|
|
|
struct vfs_node* parent;
|
|
status = get_writable_parent(target, identity, &parent);
|
|
if (status)
|
|
goto out;
|
|
|
|
status = parent->ops->dir.symlink
|
|
? parent->ops->dir.symlink(parent, &target, target_path, strlen(target_path), identity)
|
|
: EPERM;
|
|
vfs_node_unref(parent);
|
|
|
|
out:
|
|
vfs_path_drop(target);
|
|
return status;
|
|
}
|
|
|
|
static bool last_component_is(const char* path, const char* name) {
|
|
size_t len = strlen(path);
|
|
while (len > 1 && path[len - 1] == '/')
|
|
len--;
|
|
size_t begin = len;
|
|
while (begin > 0 && path[begin - 1] != '/')
|
|
begin--;
|
|
return len - begin == strlen(name) && !memcmp(path + begin, name, len - begin);
|
|
}
|
|
|
|
static errno_t check_sticky(struct vfs_node* dir, struct vfs_node* victim, const struct ident* identity) {
|
|
if (identity->euid == 0)
|
|
return 0;
|
|
|
|
mutex_lock(&dir->lock);
|
|
const bool sticky = dir->mode & __S_ISVTX;
|
|
const uid_t dir_owner = dir->uid;
|
|
mutex_unlock(&dir->lock);
|
|
if (!sticky)
|
|
return 0;
|
|
|
|
mutex_lock(&victim->lock);
|
|
const uid_t owner = victim->uid;
|
|
mutex_unlock(&victim->lock);
|
|
return identity->euid == owner || identity->euid == dir_owner ? 0 : EPERM;
|
|
}
|
|
|
|
errno_t vfs_unlink(struct vfs_path root, struct vfs_path cwd, const char* path, const struct ident* identity) {
|
|
struct vfs_path target;
|
|
errno_t status = vfs_path_lookup(root, cwd, path, identity, VFS_LOOKUP_MUST_EXIST, &target);
|
|
if (status)
|
|
return status;
|
|
|
|
struct vfs_node* node = vfs_entry_get_inode(target.entry);
|
|
if (!node) {
|
|
status = ENOENT;
|
|
goto out;
|
|
}
|
|
if (node->type == VNODE_DIRECTORY) {
|
|
status = EISDIR;
|
|
goto out_node;
|
|
}
|
|
|
|
struct vfs_node* parent;
|
|
status = get_writable_parent(target, identity, &parent);
|
|
if (status)
|
|
goto out_node;
|
|
|
|
status = check_sticky(parent, node, identity);
|
|
if (!status)
|
|
status = parent->ops->dir.unlink ? parent->ops->dir.unlink(parent, &target, identity) : EPERM;
|
|
vfs_node_unref(parent);
|
|
|
|
out_node:
|
|
vfs_node_unref(node);
|
|
out:
|
|
vfs_path_drop(target);
|
|
return status;
|
|
}
|
|
|
|
// Probe for a single entry to check if the directory is empty.
|
|
static errno_t check_dir_empty(struct vfs_node* node, struct vfs_entry* entry, const struct ident* identity) {
|
|
struct __dirent probe;
|
|
size_t count = 0;
|
|
if (node->ops->dir.get_dir_entries) {
|
|
errno_t status = node->ops->dir.get_dir_entries(node, entry, 0, &probe, 1, identity, &count);
|
|
if (status)
|
|
return status;
|
|
}
|
|
return count > 0 ? ENOTEMPTY : 0;
|
|
}
|
|
|
|
errno_t vfs_rmdir(struct vfs_path root, struct vfs_path cwd, const char* path, const struct ident* identity) {
|
|
if (last_component_is(path, "."))
|
|
return EINVAL;
|
|
if (last_component_is(path, ".."))
|
|
return ENOTEMPTY;
|
|
|
|
struct vfs_path target;
|
|
errno_t status = vfs_path_lookup(root, cwd, path, identity, VFS_LOOKUP_MUST_EXIST, &target);
|
|
if (status)
|
|
return status;
|
|
|
|
struct vfs_node* node = vfs_entry_get_inode(target.entry);
|
|
if (!node) {
|
|
status = ENOENT;
|
|
goto out;
|
|
}
|
|
if (node->type != VNODE_DIRECTORY) {
|
|
status = ENOTDIR;
|
|
goto out_node;
|
|
}
|
|
if (target.entry == target.mount->root) {
|
|
status = EBUSY;
|
|
goto out_node;
|
|
}
|
|
|
|
status = check_dir_empty(node, target.entry, identity);
|
|
if (status)
|
|
goto out_node;
|
|
|
|
struct vfs_node* parent;
|
|
status = get_writable_parent(target, identity, &parent);
|
|
if (status)
|
|
goto out_node;
|
|
|
|
status = check_sticky(parent, node, identity);
|
|
if (!status) {
|
|
if (parent->ops->dir.rmdir)
|
|
status = parent->ops->dir.rmdir(parent, &target, identity);
|
|
else if (parent->ops->dir.unlink)
|
|
status = parent->ops->dir.unlink(parent, &target, identity);
|
|
else
|
|
status = EPERM;
|
|
}
|
|
vfs_node_unref(parent);
|
|
|
|
out_node:
|
|
vfs_node_unref(node);
|
|
out:
|
|
vfs_path_drop(target);
|
|
return status;
|
|
}
|
|
|
|
errno_t vfs_mknod(
|
|
struct vfs_path root,
|
|
struct vfs_path cwd,
|
|
const char* path,
|
|
mode_t mode,
|
|
struct vfs_mknod_target* target,
|
|
const struct ident* identity,
|
|
struct vfs_node** out
|
|
) {
|
|
struct vfs_path new_path;
|
|
errno_t status = vfs_path_lookup(root, cwd, path, identity, VFS_LOOKUP_MUST_NOT_EXIST, &new_path);
|
|
if (status)
|
|
return status;
|
|
|
|
struct vfs_node* parent;
|
|
status = get_writable_parent(new_path, identity, &parent);
|
|
if (status)
|
|
goto out;
|
|
|
|
status = parent->ops->dir.mknod ? parent->ops->dir.mknod(parent, &new_path, mode, target, identity) : ENODEV;
|
|
vfs_node_unref(parent);
|
|
if (!status && out) {
|
|
*out = vfs_entry_get_inode(new_path.entry);
|
|
if (!*out)
|
|
status = ENOENT;
|
|
}
|
|
|
|
out:
|
|
vfs_path_drop(new_path);
|
|
return status;
|
|
}
|
|
|
|
static bool entry_is_ancestor(struct vfs_entry* ancestor, struct vfs_entry* entry) {
|
|
for (struct vfs_entry* it = entry; it; it = it->parent) {
|
|
if (it == ancestor)
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
static bool entry_points_to(struct vfs_entry* entry, struct vfs_node* node) {
|
|
struct vfs_node* current = vfs_entry_get_inode(entry);
|
|
if (current)
|
|
vfs_node_unref(current);
|
|
return current == node;
|
|
}
|
|
|
|
static errno_t check_rename_target(struct vfs_node* moved, struct vfs_path target, const struct ident* identity) {
|
|
struct vfs_node* existing = vfs_entry_get_inode(target.entry);
|
|
if (!existing)
|
|
return 0;
|
|
|
|
errno_t status = 0;
|
|
if (moved->type == VNODE_DIRECTORY && existing->type != VNODE_DIRECTORY)
|
|
status = ENOTDIR;
|
|
else if (moved->type != VNODE_DIRECTORY && existing->type == VNODE_DIRECTORY)
|
|
status = EISDIR;
|
|
else if (existing->type == VNODE_DIRECTORY)
|
|
status = check_dir_empty(existing, target.entry, identity);
|
|
|
|
vfs_node_unref(existing);
|
|
return status;
|
|
}
|
|
|
|
static errno_t check_rename_perms(
|
|
struct vfs_node* moved,
|
|
struct vfs_path new,
|
|
struct vfs_node* old_parent,
|
|
struct vfs_node* new_parent,
|
|
const struct ident* identity
|
|
) {
|
|
errno_t status = check_sticky(old_parent, moved, identity);
|
|
if (status)
|
|
return status;
|
|
|
|
struct vfs_node* existing = vfs_entry_get_inode(new.entry);
|
|
if (existing) {
|
|
status = check_sticky(new_parent, existing, identity);
|
|
vfs_node_unref(existing);
|
|
if (status)
|
|
return status;
|
|
}
|
|
|
|
if (moved->type == VNODE_DIRECTORY && old_parent != new_parent)
|
|
return vfs_node_try_access(moved, identity, W_OK, false);
|
|
return 0;
|
|
}
|
|
|
|
static errno_t do_rename(
|
|
struct vfs_path old,
|
|
struct vfs_path new,
|
|
struct vfs_node* moved,
|
|
const struct ident* identity
|
|
) {
|
|
struct vfs_node* old_parent;
|
|
errno_t status = get_writable_parent(old, identity, &old_parent);
|
|
if (status)
|
|
return status;
|
|
|
|
struct vfs_node* new_parent;
|
|
status = get_writable_parent(new, identity, &new_parent);
|
|
if (status) {
|
|
vfs_node_unref(old_parent);
|
|
return status;
|
|
}
|
|
|
|
status = check_rename_perms(moved, new, old_parent, new_parent, identity);
|
|
if (!status) {
|
|
if (old_parent->ops->dir.rename)
|
|
status = old_parent->ops->dir.rename(old_parent, &old, new_parent, &new, identity);
|
|
else
|
|
status = EPERM;
|
|
}
|
|
vfs_node_unref(new_parent);
|
|
vfs_node_unref(old_parent);
|
|
return status;
|
|
}
|
|
|
|
errno_t vfs_rename(
|
|
struct vfs_path root,
|
|
struct vfs_path old_cwd,
|
|
const char* old_path,
|
|
struct vfs_path new_cwd,
|
|
const char* new_path,
|
|
const struct ident* identity
|
|
) {
|
|
if (last_component_is(old_path, ".") || last_component_is(old_path, "..") || last_component_is(new_path, ".") ||
|
|
last_component_is(new_path, ".."))
|
|
return EINVAL;
|
|
|
|
struct vfs_path old;
|
|
errno_t status = vfs_path_lookup(root, old_cwd, old_path, identity, VFS_LOOKUP_MUST_EXIST, &old);
|
|
if (status)
|
|
return status;
|
|
|
|
struct vfs_path new;
|
|
status = vfs_path_lookup(root, new_cwd, new_path, identity, 0, &new);
|
|
if (status)
|
|
goto out_old;
|
|
|
|
if (old.mount != new.mount) {
|
|
status = EXDEV;
|
|
goto out_new;
|
|
}
|
|
if (old.entry == old.mount->root || new.entry == new.mount->root) {
|
|
status = EBUSY;
|
|
goto out_new;
|
|
}
|
|
if (old.entry == new.entry)
|
|
goto out_new;
|
|
|
|
struct vfs_node* moved = vfs_entry_get_inode(old.entry);
|
|
if (!moved) {
|
|
status = ENOENT;
|
|
goto out_new;
|
|
}
|
|
if (moved->type == VNODE_DIRECTORY && entry_is_ancestor(old.entry, new.entry))
|
|
status = EINVAL;
|
|
if (!status && !entry_points_to(new.entry, moved)) {
|
|
status = check_rename_target(moved, new, identity);
|
|
if (!status)
|
|
status = do_rename(old, new, moved, identity);
|
|
}
|
|
|
|
vfs_node_unref(moved);
|
|
out_new:
|
|
vfs_path_drop(new);
|
|
out_old:
|
|
vfs_path_drop(old);
|
|
return status;
|
|
}
|
|
|
|
errno_t vfs_link(
|
|
struct vfs_path root,
|
|
struct vfs_path old_cwd,
|
|
const char* old_path,
|
|
struct vfs_path new_cwd,
|
|
const char* new_path,
|
|
bool follow_symlinks,
|
|
const struct ident* identity
|
|
) {
|
|
enum vfs_lookup_flags flags = VFS_LOOKUP_MUST_EXIST;
|
|
if (follow_symlinks)
|
|
flags |= VFS_LOOKUP_FOLLOW_SYMLINKS;
|
|
|
|
struct vfs_path old;
|
|
errno_t status = vfs_path_lookup(root, old_cwd, old_path, identity, flags, &old);
|
|
if (status)
|
|
return status;
|
|
|
|
struct vfs_node* target = vfs_entry_get_inode(old.entry);
|
|
if (!target) {
|
|
status = ENOENT;
|
|
goto out_old;
|
|
}
|
|
if (target->type == VNODE_DIRECTORY) {
|
|
status = EPERM;
|
|
goto out_target;
|
|
}
|
|
|
|
struct vfs_path new;
|
|
status = vfs_path_lookup(root, new_cwd, new_path, identity, VFS_LOOKUP_MUST_NOT_EXIST, &new);
|
|
if (status)
|
|
goto out_target;
|
|
|
|
if (new.mount != old.mount) {
|
|
status = EXDEV;
|
|
goto out_new;
|
|
}
|
|
|
|
struct vfs_node* parent;
|
|
status = get_writable_parent(new, identity, &parent);
|
|
if (status)
|
|
goto out_new;
|
|
|
|
status = parent->ops->dir.link ? parent->ops->dir.link(parent, &new, target, identity) : EPERM;
|
|
vfs_node_unref(parent);
|
|
|
|
out_new:
|
|
vfs_path_drop(new);
|
|
out_target:
|
|
vfs_node_unref(target);
|
|
out_old:
|
|
vfs_path_drop(old);
|
|
return status;
|
|
}
|
|
|
|
static mode_t node_type_mode(enum vfs_node_type type) {
|
|
switch (type) {
|
|
case VNODE_DIRECTORY:
|
|
return __S_IFDIR;
|
|
case VNODE_SYMLINK:
|
|
return __S_IFLNK;
|
|
case VNODE_FIFO:
|
|
return __S_IFIFO;
|
|
case VNODE_BLOCKDEV:
|
|
return __S_IFBLK;
|
|
case VNODE_CHARDEV:
|
|
return __S_IFCHR;
|
|
case VNODE_SOCKET:
|
|
return __S_IFSOCK;
|
|
default:
|
|
return __S_IFREG;
|
|
}
|
|
}
|
|
|
|
void vfs_stat(struct vfs_node* node, struct __stat* out) {
|
|
mutex_lock(&node->lock);
|
|
*out = (struct __stat){
|
|
.st_ino = node->id,
|
|
.st_mode = node->mode | node_type_mode(node->type),
|
|
.st_nlink = node->nlink,
|
|
.st_uid = node->uid,
|
|
.st_gid = node->gid,
|
|
.st_rdev = node->rdev,
|
|
.st_size = node->size,
|
|
.st_atim = node->atime,
|
|
.st_mtim = node->mtime,
|
|
.st_ctim = node->ctime,
|
|
.st_blksize = 4096, // TODO: Get from file system
|
|
.st_blocks = node->blocks,
|
|
};
|
|
mutex_unlock(&node->lock);
|
|
}
|
|
|
|
static bool is_owner(struct vfs_node* node, const struct ident* identity) {
|
|
return identity->euid == 0 || identity->euid == node->uid;
|
|
}
|
|
|
|
errno_t vfs_chmod(struct vfs_node* node, mode_t mode, const struct ident* identity) {
|
|
if (is_read_only(node))
|
|
return EROFS;
|
|
|
|
mutex_lock(&node->lock);
|
|
if (!is_owner(node, identity)) {
|
|
mutex_unlock(&node->lock);
|
|
return EPERM;
|
|
}
|
|
mode &= 07777;
|
|
if (identity->euid != 0 && node->type == VNODE_REGULAR && !ident_in_group(identity, node->gid, false))
|
|
mode &= ~__S_ISGID;
|
|
node->mode = mode;
|
|
node->ctime = now();
|
|
mutex_unlock(&node->lock);
|
|
|
|
return sync_node(node);
|
|
}
|
|
|
|
errno_t vfs_chown(struct vfs_node* node, uid_t uid, gid_t gid, const struct ident* identity) {
|
|
if (is_read_only(node))
|
|
return EROFS;
|
|
|
|
mutex_lock(&node->lock);
|
|
if (identity->euid != 0) {
|
|
const bool uid_ok = uid == (uid_t)-1 || uid == node->uid;
|
|
const bool gid_ok = gid == (gid_t)-1 || gid == node->gid || ident_in_group(identity, gid, false);
|
|
if (identity->euid != node->uid || !uid_ok || !gid_ok) {
|
|
mutex_unlock(&node->lock);
|
|
return EPERM;
|
|
}
|
|
}
|
|
|
|
if (uid == (uid_t)-1 && gid == (gid_t)-1) {
|
|
mutex_unlock(&node->lock);
|
|
return 0;
|
|
}
|
|
|
|
if (uid != (uid_t)-1)
|
|
node->uid = uid;
|
|
if (gid != (gid_t)-1)
|
|
node->gid = gid;
|
|
if (identity->euid != 0 && node->type == VNODE_REGULAR && (node->mode & (__S_IXUSR | __S_IXGRP | __S_IXOTH)))
|
|
node->mode &= ~(__S_ISUID | __S_ISGID);
|
|
node->ctime = now();
|
|
mutex_unlock(&node->lock);
|
|
|
|
return sync_node(node);
|
|
}
|
|
|
|
static bool valid_utime(const timespec_t* ts) {
|
|
return ts->tv_nsec == __UTIME_NOW || ts->tv_nsec == __UTIME_OMIT ||
|
|
(ts->tv_nsec >= 0 && ts->tv_nsec < 1'000'000'000);
|
|
}
|
|
|
|
errno_t vfs_utimens(struct vfs_node* node, const timespec_t* times, const struct ident* identity) {
|
|
const timespec_t current = now();
|
|
timespec_t ts[2] = {
|
|
{.tv_nsec = __UTIME_NOW},
|
|
{.tv_nsec = __UTIME_NOW},
|
|
};
|
|
if (times) {
|
|
ts[0] = times[0];
|
|
ts[1] = times[1];
|
|
if (!valid_utime(&ts[0]) || !valid_utime(&ts[1]))
|
|
return EINVAL;
|
|
}
|
|
|
|
if (ts[0].tv_nsec == __UTIME_OMIT && ts[1].tv_nsec == __UTIME_OMIT)
|
|
return 0;
|
|
if (is_read_only(node))
|
|
return EROFS;
|
|
|
|
const bool only_now = (ts[0].tv_nsec == __UTIME_NOW || ts[0].tv_nsec == __UTIME_OMIT) &&
|
|
(ts[1].tv_nsec == __UTIME_NOW || ts[1].tv_nsec == __UTIME_OMIT);
|
|
|
|
mutex_lock(&node->lock);
|
|
const bool owner = is_owner(node, identity);
|
|
mutex_unlock(&node->lock);
|
|
|
|
if (!owner) {
|
|
if (!only_now)
|
|
return EPERM;
|
|
errno_t status = vfs_node_try_access(node, identity, W_OK, false);
|
|
if (status)
|
|
return status;
|
|
}
|
|
|
|
mutex_lock(&node->lock);
|
|
for (size_t i = 0; i < 2; i++) {
|
|
if (ts[i].tv_nsec == __UTIME_OMIT)
|
|
continue;
|
|
timespec_t* target = i == 0 ? &node->atime : &node->mtime;
|
|
*target = ts[i].tv_nsec == __UTIME_NOW ? current : ts[i];
|
|
}
|
|
node->ctime = current;
|
|
mutex_unlock(&node->lock);
|
|
|
|
return sync_node(node);
|
|
}
|
|
|
|
errno_t vfs_node_check_exec(struct vfs_node* node, const struct ident* identity) {
|
|
if (node->type != VNODE_REGULAR)
|
|
return EACCES;
|
|
return vfs_node_try_access(node, identity, X_OK, false);
|
|
}
|
|
|
|
errno_t vfs_truncate(struct vfs_node* node, uint64_t length) {
|
|
if (node->type == VNODE_DIRECTORY)
|
|
return EISDIR;
|
|
if (node->type != VNODE_REGULAR)
|
|
return EINVAL;
|
|
if (!node->ops->regular.truncate)
|
|
return EPERM;
|
|
return node->ops->regular.truncate(node, length);
|
|
}
|
|
|
|
errno_t vfs_read_link(struct vfs_node* node, iovec_iter_t* iter, size_t* out_read) {
|
|
if (node->type != VNODE_SYMLINK)
|
|
return EINVAL;
|
|
if (!node->ops->symlink.read_link)
|
|
return EINVAL;
|
|
return node->ops->symlink.read_link(node, iter, out_read);
|
|
}
|
|
|
|
errno_t vfs_statvfs(struct vfs_node* node, struct __statvfs* out) {
|
|
if (!node->sb || !node->sb->ops || !node->sb->ops->statvfs)
|
|
return ENOSYS;
|
|
return node->sb->ops->statvfs(node->sb, out);
|
|
}
|