neonucleus/src/components/volatileFilesystem.c
2025-07-30 21:49:34 +02:00

638 lines
19 KiB
C

#include "../neonucleus.h"
// Data structures
typedef struct nn_vfnode {
struct nn_vfilesystem *fs;
char name[NN_MAX_PATH];
struct nn_vfnode *parent;
nn_bool_t isDirectory;
union {
// if directory
struct nn_vfnode **entries;
// if file
char *data;
};
nn_size_t len;
nn_size_t cap;
nn_timestamp_t lastModified;
// this is used to block deleting
nn_refc handleCount;
} nn_vfnode;
typedef enum nn_vfmode {
NN_VFMODE_READ,
NN_VFMODE_WRITE,
NN_VFMODE_APPEND,
} nn_vfmode;
typedef struct nn_vfhandle {
nn_vfnode *node;
nn_intptr_t position;
nn_vfmode mode;
} nn_vfhandle;
typedef struct nn_vfilesystem {
nn_Context ctx;
nn_vfilesystemOptions opts;
double birthday;
nn_vfnode *root;
} nn_vfilesystem;
// virtual node helpers
nn_timestamp_t nn_vf_now(nn_vfilesystem *fs) {
nn_Clock c = fs->ctx.clock;
double time = c.proc(c.userdata);
double elapsed = time - fs->birthday;
nn_timestamp_t elapsedMS = elapsed * 1000;
return fs->opts.creationTime + elapsedMS;
}
nn_vfnode *nn_vf_allocFile(nn_vfilesystem *fs, const char *name) {
nn_Alloc *alloc = &fs->ctx.allocator;
nn_vfnode *node = nn_alloc(alloc, sizeof(nn_vfnode));
if(node == NULL) return NULL;
*node = (nn_vfnode) {
.fs = fs,
.lastModified = nn_vf_now(fs),
.parent = NULL,
.isDirectory = false,
.data = NULL,
.len = 0,
.cap = 0,
.handleCount = 0,
};
// we pray
nn_strcpy(node->name, name);
return node;
}
nn_vfnode *nn_vf_allocDirectory(nn_vfilesystem *fs, const char *name) {
nn_Alloc *alloc = &fs->ctx.allocator;
nn_vfnode *node = nn_alloc(alloc, sizeof(nn_vfnode));
if(node == NULL) return NULL;
nn_vfnode **buffer = nn_alloc(alloc, sizeof(nn_vfnode *));
if(buffer == NULL) {
nn_dealloc(alloc, node, sizeof(nn_vfnode));
return NULL;
}
*node = (nn_vfnode) {
.fs = fs,
.lastModified = nn_vf_now(fs),
.parent = NULL,
.isDirectory = true,
.entries = buffer,
.len = 0,
.cap = fs->opts.maxDirEntries,
.handleCount = 0,
};
// we pray
nn_strcpy(node->name, name);
return node;
}
void nn_vf_freeNode(nn_vfnode *node) {
nn_Alloc *alloc = &node->fs->ctx.allocator;
if(node->isDirectory) {
for(nn_size_t i = 0; i < node->len; i++) {
nn_vf_freeNode(node->entries[i]);
}
nn_dealloc(alloc, node->entries, sizeof(nn_vfnode *) * node->cap);
} else {
nn_dealloc(alloc, node->data, node->cap);
}
nn_dealloc(alloc, node, sizeof(nn_vfnode));
}
nn_size_t nn_vf_spaceUsedByNode(nn_vfnode *node) {
if(node->isDirectory) {
nn_size_t sum = 0;
for(nn_size_t i = 0; i < node->len; i++) {
sum += nn_vf_spaceUsedByNode(node->entries[i]);
}
return sum;
} else {
return node->len;
}
}
nn_vfnode *nn_vf_find(nn_vfnode *parent, const char *name) {
if(!parent->isDirectory) return NULL;
for(nn_size_t i = 0; i < parent->len; i++) {
nn_vfnode *entry = parent->entries[i];
if(nn_strcmp(entry->name, name) == 0) {
return entry;
}
}
return NULL;
}
nn_bool_t nn_vf_ensureFileCapacity(nn_vfnode *file, nn_size_t capacity) {
if(file->isDirectory) return false;
nn_Alloc *alloc = &file->fs->ctx.allocator;
if(file->cap >= capacity) return true; // already at that point
char *newData = nn_resize(alloc, file->data, file->cap, capacity);
if(newData == NULL) {
return false; // OOM
}
file->data = newData;
file->cap = capacity;
return true;
}
// this is used to compute exponential backoff
// TODO: add an option to select either a slower growth rate or
// linear backoff to reduce memory usage at the cost of speed
nn_size_t nn_vf_getIdealCapacity(nn_vfnode *file, nn_size_t spaceNeeded) {
nn_size_t cap = file->cap;
if(cap == 0) cap = 1;
while(cap < spaceNeeded) cap *= 2; // this would mean a file with 1,048,577 bytes takes up 2,097,152 bytes, potentially wasting 1,048,575 bytes
return cap;
}
void nn_vf_clampHandlePosition(nn_vfhandle *handle) {
nn_size_t len = handle->node->len;
if(handle->mode == NN_VFMODE_APPEND) {
handle->position = len;
return;
}
if(handle->position < 0) handle->position = 0;
if(handle->position > len) handle->position = len;
}
nn_vfnode *nn_vf_resolvePathFromNode(nn_vfnode *node, const char *path) {
if(path[0] == 0) {
return node;
}
char firstDirectory[NN_MAX_PATH];
char subpath[NN_MAX_PATH];
if(nn_path_firstName(path, firstDirectory, subpath)) {
return nn_vf_find(node, firstDirectory);
}
nn_vfnode *dir = nn_vf_find(node, firstDirectory);
if(dir == NULL) return NULL;
if(!dir->isDirectory) return NULL;
return nn_vf_resolvePathFromNode(dir, subpath);
}
nn_vfnode *nn_vf_resolvePath(nn_vfilesystem *fs, const char *path) {
return nn_vf_resolvePathFromNode(fs->root, path);
}
nn_size_t nn_vf_countTree(nn_vfnode *node) {
if(!node->isDirectory) return 1;
nn_size_t n = 1;
for(nn_size_t i = 0; i < node->len; i++) {
n += nn_vf_countTree(node->entries[i]);
}
return n;
}
nn_bool_t nn_vf_treeHasHandles(nn_vfnode *node) {
if(!node->isDirectory) return node->handleCount > 0;
for(nn_size_t i = 0; i < node->len; i++) {
if(nn_vf_treeHasHandles(node->entries[i])) return true;
}
return false;
}
void nn_vf_appendNode(nn_vfnode *parent, nn_vfnode *node) {
if(!parent->isDirectory) return;
if(parent->len == parent->cap) return;
parent->entries[parent->len] = node;
parent->len++;
node->parent = parent; // just to be sure
}
void nn_vf_removeNode(nn_vfnode *parent, nn_vfnode *node) {
if(!parent->isDirectory) return;
nn_size_t j = 0;
for(nn_size_t i = 0; i < parent->len; i++) {
if(parent->entries[i] != node) {
parent->entries[j] = parent->entries[i];
j++;
}
}
parent->len = j;
}
// methods
void nn_vfs_deinit(nn_vfilesystem *fs) {
nn_Context ctx = fs->ctx;
nn_vf_freeNode(fs->root);
nn_dealloc(&ctx.allocator, fs, sizeof(nn_vfilesystem));
}
void nn_vfs_getLabel(nn_vfilesystem *fs, char *buf, nn_size_t *buflen) {
*buflen = fs->opts.labelLen;
nn_memcpy(buf, fs->opts.label, fs->opts.labelLen);
}
void nn_vfs_setLabel(nn_vfilesystem *fs, const char *buf, nn_size_t buflen) {
nn_memcpy(fs->opts.label, buf, buflen);
fs->opts.labelLen = buflen;
}
nn_size_t nn_vfs_spaceUsed(nn_vfilesystem *fs) {
return nn_vf_spaceUsedByNode(fs->root);
}
nn_bool_t nn_vfs_isReadOnly(nn_vfilesystem *fs) {
return fs->opts.isReadOnly;
}
nn_size_t nn_vfs_size(nn_vfilesystem *fs, const char *path, nn_errorbuf_t err) {
nn_vfnode *node = nn_vf_resolvePath(fs, path);
if(node == NULL) {
nn_error_write(err, "No such file");
return 0;
}
if(node->isDirectory) return 0;
return node->len;
}
nn_size_t nn_vfs_remove(nn_vfilesystem *fs, const char *path, nn_errorbuf_t err) {
nn_vfnode *node = nn_vf_resolvePath(fs, path);
if(node == NULL) {
nn_error_write(err, "No such file");
return 0;
}
nn_vfnode *parent = node->parent;
if(parent == NULL) {
// root, can't delete
nn_error_write(err, "Unable to delete root");
return 0;
}
if(nn_vf_treeHasHandles(node)) {
nn_error_write(err, "Files are pinned by handles");
return 0;
}
nn_size_t removed = nn_vf_countTree(node);
// it is super easy to delete a tree
nn_vf_removeNode(parent, node);
parent->lastModified = nn_vf_now(fs);
nn_vf_freeNode(node);
return removed;
}
nn_timestamp_t nn_vfs_lastModified(nn_vfilesystem *fs, const char *path, nn_errorbuf_t err) {
nn_vfnode *node = nn_vf_resolvePath(fs, path);
if(node == NULL) {
nn_error_write(err, "No such file");
return 0;
}
return node->lastModified;
}
nn_size_t nn_vfs_rename(nn_vfilesystem *fs, const char *from, const char *to, nn_errorbuf_t err) {
// TODO: implement rename
nn_error_write(err, "Unsupported operation");
nn_vfnode *srcNode = nn_vf_resolvePath(fs, from);
if(srcNode == NULL) {
nn_error_write(err, "No such file");
return 0;
}
nn_vfnode *srcParent = srcNode->parent;
if(srcParent == NULL) {
// root, can't move
nn_error_write(err, "Unable to move root");
return 0;
}
if(nn_vf_treeHasHandles(srcNode)) {
nn_error_write(err, "Files are pinned by handles");
return 0;
}
char name[NN_MAX_PATH];
char parentPath[NN_MAX_PATH];
nn_bool_t rootOut = nn_path_lastName(to, name, parentPath);
nn_vfnode *destParent = rootOut ? fs->root : nn_vf_resolvePath(fs, parentPath);
if(destParent == NULL) {
nn_error_write(err, "No such directory");
return 0;
}
if(!destParent->isDirectory) {
nn_error_write(err, "Is a file");
return 0;
}
if(nn_vf_find(destParent, name) != NULL) {
nn_error_write(err, "Already exists");
return 0;
}
if(destParent->len == destParent->cap) {
nn_error_write(err, "Too many entries");
return 0;
}
nn_size_t moved = nn_vf_countTree(srcNode);
// super efficient moving
nn_vf_removeNode(srcParent, srcNode);
nn_vf_appendNode(destParent, srcNode);
return moved;
}
nn_bool_t nn_vfs_exists(nn_vfilesystem *fs, const char *path, nn_errorbuf_t err) {
nn_vfnode *node = nn_vf_resolvePath(fs, path);
return node != NULL;
}
nn_bool_t nn_vfs_isDirectory(nn_vfilesystem *fs, const char *path, nn_errorbuf_t err) {
nn_vfnode *node = nn_vf_resolvePath(fs, path);
if(node == NULL) {
nn_error_write(err, "No such file");
return 0;
}
return node->isDirectory;
}
static nn_bool_t nn_vfs_recursiveMkdir(nn_vfilesystem *fs, const char *path, nn_errorbuf_t err) {
// this code is horribly unoptimized, with a time complexity of O(F^U),
// where F is 8.1 * 10^53 (monsterous) and U is 7 * 10^27 (very human)
// TODO: burn it with fire and make something good
nn_vfnode *node = nn_vf_resolvePath(fs, path);
if(node != NULL) {
if(node->isDirectory) {
return true;
}
nn_error_write(err, "Is a file");
return false;
}
char name[NN_MAX_PATH];
char parentPath[NN_MAX_PATH];
nn_bool_t isRootDir = nn_path_lastName(path, name, parentPath);
if(!isRootDir) {
if(!nn_vfs_recursiveMkdir(fs, parentPath, err)) return false;
}
nn_vfnode *parent = isRootDir ? fs->root : nn_vf_resolvePath(fs, parentPath);
if(parent == NULL) {
nn_error_write(err, "Bad state"); // just a sanity check
return false;
}
if(parent->len == parent->cap) {
nn_error_write(err, "Too many entries");
return false;
}
nn_vfnode *dir = nn_vf_allocDirectory(fs, name);
if(dir == NULL) {
nn_error_write(err, "Out of memory");
return false;
}
dir->parent = parent;
parent->entries[parent->len] = dir;
parent->len++;
return true;
}
nn_bool_t nn_vfs_makeDirectory(nn_vfilesystem *fs, const char *path, nn_errorbuf_t err) {
return nn_vfs_recursiveMkdir(fs, path, err);
}
char **nn_vfs_list(nn_Alloc *alloc, nn_vfilesystem *fs, const char *path, nn_size_t *len, nn_errorbuf_t err) {
nn_vfnode *node = nn_vf_resolvePath(fs, path);
if(node == NULL) {
nn_error_write(err, "No such file");
return NULL;
}
if(!node->isDirectory) {
nn_error_write(err, "Not a directory");
return NULL;
}
nn_size_t count = node->len;
*len = count;
char **buf = nn_alloc(alloc, sizeof(char *) * count);
if(buf == NULL) {
nn_error_write(err, "Out of memory");
return NULL;
}
for(nn_size_t i = 0; i < count; i++) {
nn_vfnode *entry = node->entries[i];
char *s = NULL;
if(entry->isDirectory) {
nn_size_t l = nn_strlen(entry->name);
s = nn_alloc(alloc, l + 2);
if(s != NULL) {
nn_memcpy(s, entry->name, l);
s[l] = '/';
s[l+1] = 0;
}
} else {
s = nn_strdup(alloc, entry->name);
}
if(s == NULL) {
for(nn_size_t j = 0; j < i; j++) {
nn_deallocStr(alloc, buf[j]);
}
nn_error_write(err, "Out of memory");
return NULL;
}
buf[i] = s;
}
return buf;
}
nn_vfhandle *nn_vfs_open(nn_vfilesystem *fs, const char *path, const char *mode, nn_errorbuf_t err) {
// TODO: complete
char m = mode[0];
nn_vfmode fmode = NN_VFMODE_READ;
if(m == 'w') fmode = NN_VFMODE_WRITE;
if(m == 'a') fmode = NN_VFMODE_APPEND;
nn_vfnode *node = nn_vf_resolvePath(fs, path);
if(node == NULL && fmode != NN_VFMODE_READ && !fs->opts.isReadOnly) {
char parentPath[NN_MAX_PATH];
char name[NN_MAX_PATH];
nn_bool_t isRootFile = nn_path_lastName(path, name, parentPath);
nn_vfnode *parent = isRootFile ? fs->root : nn_vf_resolvePath(fs, parentPath);
if(parent == NULL) {
nn_error_write(err, "Missing parent directory");
return NULL;
}
if(!parent->isDirectory) {
nn_error_write(err, "Parent is not a directory");
return NULL;
}
if(parent->len == parent->cap) {
nn_error_write(err, "Too many entries");
return NULL;
}
node = nn_vf_allocFile(fs, name);
if(node == NULL) {
nn_error_write(err, "Out of memory");
return NULL;
}
node->parent = parent;
parent->entries[parent->len] = node;
parent->len++;
}
if(node == NULL) {
nn_error_write(err, "No such file");
return NULL;
}
if(fs->opts.isReadOnly && fmode != NN_VFMODE_READ) {
nn_error_write(err, "readonly");
return NULL;
}
if(node->isDirectory) {
nn_error_write(err, "Is a directory");
return NULL;
}
if(fmode == NN_VFMODE_WRITE) {
node->len = 0;
node->lastModified = nn_vf_now(fs);
}
nn_vfhandle *handle = nn_alloc(&fs->ctx.allocator, sizeof(nn_vfhandle));
if(handle == NULL) {
nn_error_write(err, "Out of memory");
return NULL;
}
handle->mode = fmode;
handle->node = node;
handle->position = 0;
node->handleCount++;
return handle;
}
nn_bool_t nn_vfs_close(nn_vfilesystem *fs, nn_vfhandle *handle, nn_errorbuf_t err) {
handle->node->handleCount--;
nn_dealloc(&fs->ctx.allocator, handle, sizeof(nn_vfhandle));
return true;
}
nn_bool_t nn_vfs_write(nn_vfilesystem *fs, nn_vfhandle *handle, const char *buf, nn_size_t len, nn_errorbuf_t err) {
nn_vf_clampHandlePosition(handle);
if(!nn_vf_ensureFileCapacity(handle->node, handle->position + len)) {
nn_error_write(err, "Out of memory");
return false;
}
nn_memcpy(handle->node->data + handle->position, buf, len);
handle->position += len;
if(handle->node->len < handle->position) handle->node->len = handle->position;
return true;
}
nn_size_t nn_vfs_read(nn_vfilesystem *fs, nn_vfhandle *handle, char *buf, nn_size_t required, nn_errorbuf_t err) {
nn_size_t remaining = handle->node->len - handle->position;
if(required > remaining) required = remaining;
if(required == 0) return 0;
nn_memcpy(buf, handle->node->data + handle->position, required);
handle->position += required;
return required;
}
nn_size_t nn_vfs_seek(nn_vfilesystem *fs, nn_vfhandle *handle, const char *whence, int off, nn_errorbuf_t err) {
if(handle->mode == NN_VFMODE_APPEND) {
nn_error_write(err, "Bad file descriptor");
return handle->node->len;
}
nn_intptr_t ptr = handle->position;
if(nn_strcmp(whence, "set") == 0) {
ptr = off;
}
if(nn_strcmp(whence, "cur") == 0) {
ptr += off;
}
if(nn_strcmp(whence, "end") == 0) {
ptr = handle->node->len - off;
}
handle->position = ptr;
nn_vf_clampHandlePosition(handle);
return handle->position;
}
typedef struct nn_vfilesystemImage {
nn_vfilesystemImageNode *nodes;
nn_size_t ptr;
} nn_vfilesystemImage;
static nn_vfilesystemImageNode nni_vfsimg_nextNode(nn_vfilesystemImage *stream) {
nn_vfilesystemImageNode node = stream->nodes[stream->ptr];
stream->ptr++;
return node;
}
static nn_vfnode *nni_vfsimg_parseNode(nn_vfilesystem *fs, nn_vfilesystemImage *stream) {
// TODO: make this handle OOMs
nn_vfilesystemImageNode node = nni_vfsimg_nextNode(stream);
if(node.data == NULL) {
// directory!!!!!
nn_vfnode *dir = nn_vf_allocDirectory(fs, node.name);
dir->len = node.len;
for(int i = 0; i < node.len; i++) {
nn_vfnode *entry = nni_vfsimg_parseNode(fs, stream);
dir->entries[i] = entry;
}
return dir;
}
// file!!!!!
nn_vfnode *file = nn_vf_allocFile(fs, node.name);
nn_vf_ensureFileCapacity(file, node.len);
file->len = node.len;
nn_memcpy(file->data, node.data, node.len);
return file;
}
// constructor
nn_filesystem *nn_volatileFilesystem(nn_Context *context, nn_vfilesystemOptions opts, nn_filesystemControl control) {
// TODO: handle OOM
nn_vfilesystem *fs = nn_alloc(&context->allocator, sizeof(nn_vfilesystem));
fs->ctx = *context;
nn_Clock c = fs->ctx.clock;
double time = c.proc(c.userdata);
fs->birthday = time;
fs->opts = opts;
fs->root = nn_vf_allocDirectory(fs, "/");
if(opts.image != NULL) {
nn_vfilesystemImage stream = {
.nodes = opts.image,
.ptr = 0,
};
// we got supplied an image, shit
fs->root->len = opts.rootEntriesInImage;
for(int i = 0; i < opts.rootEntriesInImage; i++) {
nn_vfnode *entry = nni_vfsimg_parseNode(fs, &stream);
fs->root->entries[i] = entry;
}
}
nn_filesystemTable table = {
.userdata = fs,
.deinit = (void *)nn_vfs_deinit,
.getLabel = (void *)nn_vfs_getLabel,
.setLabel = (void *)nn_vfs_setLabel,
.spaceUsed = (void *)nn_vfs_spaceUsed,
.spaceTotal = opts.capacity,
.isReadOnly = (void *)nn_vfs_isReadOnly,
.size = (void *)nn_vfs_size,
.remove = (void *)nn_vfs_remove,
.lastModified = (void *)nn_vfs_lastModified,
.rename = (void *)nn_vfs_rename,
.exists = (void *)nn_vfs_exists,
.isDirectory = (void *)nn_vfs_isDirectory,
.makeDirectory = (void *)nn_vfs_makeDirectory,
.list = (void *)nn_vfs_list,
.open = (void *)nn_vfs_open,
.close = (void *)nn_vfs_close,
.write = (void *)nn_vfs_write,
.read = (void *)nn_vfs_read,
.seek = (void *)nn_vfs_seek,
};
return nn_newFilesystem(context, table, control);
}