412 lines
11 KiB
C
412 lines
11 KiB
C
// SPDX-License-Identifier: ISC
|
|
|
|
#include <zinnia/alloc.h>
|
|
#include <zinnia/vfs.h>
|
|
#include <string.h>
|
|
#include "internal.h"
|
|
|
|
constexpr size_t MAX_SYMLINK_DEPTH = 40;
|
|
|
|
// If `path` is a mount point, crosses into the root of the topmost mounted file system.
|
|
// Returns a new reference.
|
|
static struct vfs_path cross_mounts(struct vfs_path path) {
|
|
struct vfs_mount* mount = path.mount;
|
|
struct vfs_entry* entry = path.entry;
|
|
|
|
again:
|
|
mutex_lock(&entry->lock);
|
|
vec_foreach(&entry->mounts, i) {
|
|
struct vfs_mount* child = vec_elem_get(&entry->mounts, i);
|
|
if (child != mount) {
|
|
mutex_unlock(&entry->lock);
|
|
mount = child;
|
|
entry = child->root;
|
|
goto again;
|
|
}
|
|
}
|
|
mutex_unlock(&entry->lock);
|
|
|
|
return vfs_path_clone((struct vfs_path){.mount = mount, .entry = entry});
|
|
}
|
|
|
|
errno_t vfs_path_lookup_parent(struct vfs_path path, struct vfs_path* out) {
|
|
struct vfs_mount* mount = path.mount;
|
|
struct vfs_entry* entry = path.entry;
|
|
|
|
// Get the top mount point.
|
|
while (mount->has_mount_point && entry == mount->root) {
|
|
entry = mount->mount_point.entry;
|
|
mount = mount->mount_point.mount;
|
|
}
|
|
|
|
if (!entry->parent)
|
|
return ENOENT;
|
|
|
|
*out = vfs_path_clone((struct vfs_path){.mount = mount, .entry = entry->parent});
|
|
return 0;
|
|
}
|
|
|
|
errno_t vfs_path_lookup_child(
|
|
struct vfs_path path,
|
|
struct vfs_path* root,
|
|
const char* name,
|
|
size_t name_len,
|
|
struct vfs_path* out
|
|
) {
|
|
errno_t status = 0;
|
|
|
|
// Traverse the mounts on the current entry.
|
|
struct vfs_path crossed = cross_mounts(path);
|
|
|
|
// A lookup only makes sense on directories.
|
|
struct vfs_node* parent = vfs_entry_get_inode(crossed.entry);
|
|
if (!parent) {
|
|
status = ENOENT;
|
|
goto fail;
|
|
}
|
|
if (parent->type != VNODE_DIRECTORY) {
|
|
status = ENOTDIR;
|
|
goto fail;
|
|
}
|
|
|
|
if (name_len == 1 && name[0] == '.') {
|
|
vfs_node_unref(parent);
|
|
*out = crossed;
|
|
return 0;
|
|
}
|
|
|
|
if (name_len == 2 && name[0] == '.' && name[1] == '.') {
|
|
vfs_node_unref(parent);
|
|
|
|
struct vfs_path crossed_root = cross_mounts(*root);
|
|
bool at_root = vfs_path_equal(crossed, crossed_root);
|
|
vfs_path_drop(crossed_root);
|
|
if (at_root) {
|
|
*out = crossed;
|
|
return 0;
|
|
}
|
|
|
|
status = vfs_path_lookup_parent(crossed, out);
|
|
if (status == ENOENT) {
|
|
*out = crossed;
|
|
return 0;
|
|
}
|
|
vfs_path_drop(crossed);
|
|
return status;
|
|
}
|
|
|
|
mutex_lock(&crossed.entry->lock);
|
|
|
|
// If this entry has already been looked up before, return that.
|
|
struct vfs_entry* child = nullptr;
|
|
for (struct vfs_entry* it = tailq_first(&crossed.entry->children); it; it = tailq_next(it, child_link)) {
|
|
if (it->name_len == name_len && !memcmp(it->name, name, name_len)) {
|
|
child = it;
|
|
vfs_entry_ref(child);
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!child) {
|
|
status = vfs_entry_new(name, name_len, crossed.entry, &child);
|
|
if (status) {
|
|
mutex_unlock(&crossed.entry->lock);
|
|
goto fail;
|
|
}
|
|
|
|
struct vfs_path child_path = {.mount = crossed.mount, .entry = child};
|
|
status = parent->ops->dir.lookup ? parent->ops->dir.lookup(parent, &child_path) : ENOENT;
|
|
if (status && status != ENOENT) {
|
|
mutex_unlock(&crossed.entry->lock);
|
|
vfs_entry_unref(child);
|
|
goto fail;
|
|
}
|
|
|
|
// Allow lookup failures so we can cache it as a negative entry.
|
|
if (status == ENOENT)
|
|
vfs_entry_set_negative(child);
|
|
status = 0;
|
|
|
|
// The children list takes over the initial reference.
|
|
tailq_insert_tail(&crossed.entry->children, child, child_link);
|
|
vfs_entry_ref(child);
|
|
}
|
|
|
|
mutex_unlock(&crossed.entry->lock);
|
|
|
|
*out = cross_mounts((struct vfs_path){.mount = crossed.mount, .entry = child});
|
|
vfs_entry_unref(child);
|
|
vfs_node_unref(parent);
|
|
vfs_path_drop(crossed);
|
|
return 0;
|
|
|
|
fail:
|
|
if (parent)
|
|
vfs_node_unref(parent);
|
|
vfs_path_drop(crossed);
|
|
return status;
|
|
}
|
|
|
|
static errno_t lookup_inner(
|
|
struct vfs_path root,
|
|
struct vfs_path start,
|
|
const char* path,
|
|
const struct ident* identity,
|
|
enum vfs_lookup_flags flags,
|
|
size_t* symlink_depth,
|
|
struct vfs_path* out
|
|
);
|
|
|
|
// Resolves `current` in place until it no longer points to a symbolic link.
|
|
static errno_t resolve_symlink(
|
|
struct vfs_path root,
|
|
struct vfs_path* current,
|
|
const struct ident* identity,
|
|
enum vfs_lookup_flags flags,
|
|
size_t* symlink_depth
|
|
) {
|
|
errno_t status = 0;
|
|
char* link_buf = nullptr;
|
|
|
|
while (true) {
|
|
struct vfs_node* node = vfs_entry_get_inode(current->entry);
|
|
if (!node)
|
|
break;
|
|
if (node->type != VNODE_SYMLINK) {
|
|
vfs_node_unref(node);
|
|
break;
|
|
}
|
|
|
|
if (*symlink_depth >= MAX_SYMLINK_DEPTH || !node->ops->symlink.read_link) {
|
|
vfs_node_unref(node);
|
|
status = ELOOP;
|
|
break;
|
|
}
|
|
(*symlink_depth)++;
|
|
|
|
if (!link_buf) {
|
|
link_buf = mem_alloc(__PATH_MAX + 1, ALLOC_NOZERO);
|
|
if (!link_buf) {
|
|
vfs_node_unref(node);
|
|
status = ENOMEM;
|
|
break;
|
|
}
|
|
}
|
|
|
|
iovec_t vec = {.base = (uintptr_t)link_buf, .len = __PATH_MAX};
|
|
iovec_iter_t iter = iovec_iter_kernel(&vec, 1);
|
|
size_t link_len = 0;
|
|
status = node->ops->symlink.read_link(node, &iter, &link_len);
|
|
vfs_node_unref(node);
|
|
if (status)
|
|
break;
|
|
link_buf[link_len] = '\0';
|
|
|
|
struct vfs_path parent;
|
|
status = vfs_path_lookup_parent(*current, &parent);
|
|
if (status)
|
|
break;
|
|
|
|
struct vfs_path result;
|
|
status = lookup_inner(
|
|
root,
|
|
parent,
|
|
link_buf,
|
|
identity,
|
|
flags & ~(VFS_LOOKUP_MUST_EXIST | VFS_LOOKUP_MUST_NOT_EXIST),
|
|
symlink_depth,
|
|
&result
|
|
);
|
|
vfs_path_drop(parent);
|
|
if (status)
|
|
break;
|
|
|
|
if (vfs_path_equal(result, *current)) {
|
|
vfs_path_drop(result);
|
|
status = ELOOP;
|
|
break;
|
|
}
|
|
|
|
vfs_path_drop(*current);
|
|
*current = result;
|
|
}
|
|
|
|
mem_free(link_buf);
|
|
return status;
|
|
}
|
|
|
|
static errno_t lookup_inner(
|
|
struct vfs_path root,
|
|
struct vfs_path start,
|
|
const char* path,
|
|
const struct ident* identity,
|
|
enum vfs_lookup_flags flags,
|
|
size_t* symlink_depth,
|
|
struct vfs_path* out
|
|
) {
|
|
errno_t status = 0;
|
|
|
|
if (path[0] == '\0')
|
|
return ENOENT;
|
|
|
|
// If a path starts with '/', it's an absolute path.
|
|
// In that case, skip the first character and use the current root as a starting point.
|
|
struct vfs_path current;
|
|
if (path[0] == '/') {
|
|
current = vfs_path_clone(root);
|
|
path++;
|
|
} else {
|
|
current = vfs_path_clone(start);
|
|
}
|
|
|
|
// Parse each component.
|
|
while (true) {
|
|
while (*path == '/')
|
|
path++;
|
|
if (*path == '\0')
|
|
break;
|
|
|
|
const char* component = path;
|
|
size_t component_len = 0;
|
|
while (path[component_len] != '/' && path[component_len] != '\0')
|
|
component_len++;
|
|
path += component_len;
|
|
|
|
if (component_len > __NAME_MAX) {
|
|
status = ENAMETOOLONG;
|
|
goto fail;
|
|
}
|
|
|
|
status = resolve_symlink(root, ¤t, identity, flags, symlink_depth);
|
|
if (status)
|
|
goto fail;
|
|
|
|
struct vfs_node* node = vfs_entry_get_inode(current.entry);
|
|
if (!node) {
|
|
status = ENOENT;
|
|
goto fail;
|
|
}
|
|
if (node->type != VNODE_DIRECTORY) {
|
|
vfs_node_unref(node);
|
|
status = ENOTDIR;
|
|
goto fail;
|
|
}
|
|
|
|
status = vfs_node_try_access(node, identity, X_OK, flags & VFS_LOOKUP_REAL_ID);
|
|
vfs_node_unref(node);
|
|
if (status)
|
|
goto fail;
|
|
|
|
struct vfs_path next;
|
|
status = vfs_path_lookup_child(current, &root, component, component_len, &next);
|
|
if (status)
|
|
goto fail;
|
|
vfs_path_drop(current);
|
|
current = next;
|
|
}
|
|
|
|
if (flags & VFS_LOOKUP_FOLLOW_SYMLINKS) {
|
|
status = resolve_symlink(root, ¤t, identity, flags, symlink_depth);
|
|
if (status)
|
|
goto fail;
|
|
}
|
|
|
|
struct vfs_node* node = vfs_entry_get_inode(current.entry);
|
|
if (node)
|
|
vfs_node_unref(node);
|
|
|
|
if ((flags & VFS_LOOKUP_MUST_EXIST) && !node) {
|
|
status = ENOENT;
|
|
goto fail;
|
|
}
|
|
if ((flags & VFS_LOOKUP_MUST_NOT_EXIST) && node) {
|
|
status = EEXIST;
|
|
goto fail;
|
|
}
|
|
|
|
*out = current;
|
|
return 0;
|
|
|
|
fail:
|
|
vfs_path_drop(current);
|
|
return status;
|
|
}
|
|
|
|
errno_t vfs_path_lookup(
|
|
struct vfs_path root,
|
|
struct vfs_path cwd,
|
|
const char* path,
|
|
const struct ident* identity,
|
|
enum vfs_lookup_flags flags,
|
|
struct vfs_path* out
|
|
) {
|
|
size_t symlink_depth = 0;
|
|
return lookup_inner(root, cwd, path, identity, flags, &symlink_depth, out);
|
|
}
|
|
|
|
errno_t vfs_path_absolute(struct vfs_path path, struct vfs_path* root, char* buf, size_t len, size_t* out_len) {
|
|
if (len == 0)
|
|
return ENAMETOOLONG;
|
|
|
|
struct vfs_path global_root;
|
|
if (!root) {
|
|
global_root = vfs_get_root();
|
|
root = &global_root;
|
|
}
|
|
|
|
errno_t status = 0;
|
|
struct vfs_mount* mount = path.mount;
|
|
struct vfs_entry* entry = path.entry;
|
|
|
|
// Build the path back to front, leaving room for the NUL terminator.
|
|
size_t cursor = len - 1;
|
|
buf[cursor] = '\0';
|
|
|
|
while (entry != root->entry || mount != root->mount) {
|
|
// At the root of a mounted file system we must step into the parent file system.
|
|
if (entry == mount->root) {
|
|
if (!mount->has_mount_point) {
|
|
// Global root with no parent.
|
|
status = ENOENT;
|
|
goto out;
|
|
}
|
|
entry = mount->mount_point.entry;
|
|
mount = mount->mount_point.mount;
|
|
continue;
|
|
}
|
|
|
|
if (entry->name_len + 1 > cursor) {
|
|
status = ENAMETOOLONG;
|
|
goto out;
|
|
}
|
|
cursor -= entry->name_len;
|
|
memcpy(buf + cursor, entry->name, entry->name_len);
|
|
buf[--cursor] = '/';
|
|
|
|
if (!entry->parent) {
|
|
status = ENOENT;
|
|
goto out;
|
|
}
|
|
entry = entry->parent;
|
|
}
|
|
|
|
// The root itself resolves to "/".
|
|
if (cursor == len - 1) {
|
|
if (cursor == 0) {
|
|
status = ENAMETOOLONG;
|
|
goto out;
|
|
}
|
|
buf[--cursor] = '/';
|
|
}
|
|
|
|
size_t result_len = len - 1 - cursor;
|
|
memmove(buf, buf + cursor, result_len + 1);
|
|
if (out_len)
|
|
*out_len = result_len;
|
|
|
|
out:
|
|
if (root == &global_root)
|
|
vfs_path_drop(global_root);
|
|
return status;
|
|
}
|