calog/libs/calogFs.c
2026-07-03 02:13:23 -05:00

352 lines
12 KiB
C

// calogFs.c -- calog filesystem library (see calogFs.h). Thin, binary-safe wrappers over the
// POSIX file API (open/read/write, stat, opendir/readdir, mkdir, unlink), bridging paths and
// file bytes to calog's value/aggregate model. No shared state: every native is a pure function
// of its arguments over the OS filesystem, so the natives are INLINE and there is nothing to
// shut down. A system-call failure becomes a calogFail carrying strerror(errno).
#define _POSIX_C_SOURCE 200809L
#include "calogFs.h"
#include <dirent.h>
#include <errno.h>
#include <fcntl.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>
static int32_t fsAppendNative(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);
static int32_t fsExistsNative(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);
static int32_t fsFail(CalogValueT *result, int32_t err);
static int32_t fsListNative(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);
static int32_t fsMapSet(CalogAggT *agg, const char *keyName, CalogValueT *value);
static int32_t fsMkdirNative(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);
static int32_t fsPutFile(const char *path, const char *bytes, int64_t length, bool append, CalogValueT *result);
static int32_t fsReadNative(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);
static int32_t fsRemoveNative(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);
static int32_t fsStatNative(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);
static int32_t fsWriteNative(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);
int32_t calogFsRegister(CalogT *calog) {
calogRegisterInline(calog, "fsRead", fsReadNative, NULL);
calogRegisterInline(calog, "fsWrite", fsWriteNative, NULL);
calogRegisterInline(calog, "fsAppend", fsAppendNative, NULL);
calogRegisterInline(calog, "fsExists", fsExistsNative, NULL);
calogRegisterInline(calog, "fsRemove", fsRemoveNative, NULL);
calogRegisterInline(calog, "fsMkdir", fsMkdirNative, NULL);
calogRegisterInline(calog, "fsList", fsListNative, NULL);
calogRegisterInline(calog, "fsStat", fsStatNative, NULL);
return calogOkE;
}
static int32_t fsAppendNative(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) {
(void)userData;
calogValueNil(result);
if (argCount != 2 || args[0].type != calogStringE || args[1].type != calogStringE) {
return calogFail(result, calogErrArgE, "fsAppend expects (path, data)");
}
return fsPutFile(args[0].as.s.bytes, args[1].as.s.bytes, args[1].as.s.length, true, result);
}
static int32_t fsExistsNative(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) {
(void)userData;
calogValueNil(result);
if (argCount != 1 || args[0].type != calogStringE) {
return calogFail(result, calogErrArgE, "fsExists expects (path)");
}
calogValueBool(result, access(args[0].as.s.bytes, F_OK) == 0);
return calogOkE;
}
// Map a failed syscall's errno to a calog status (ENOENT and the general case both read as
// "not found", ENOMEM as OOM) and carry the human-readable strerror text as the message.
static int32_t fsFail(CalogValueT *result, int32_t err) {
if (err == ENOMEM) {
return calogFail(result, calogErrOomE, strerror(err));
}
return calogFail(result, calogErrNotFoundE, strerror(err));
}
static int32_t fsListNative(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) {
struct dirent *entry;
CalogValueT name;
CalogAggT *agg;
DIR *dir;
int32_t status;
int saved;
(void)userData;
calogValueNil(result);
if (argCount != 1 || args[0].type != calogStringE) {
return calogFail(result, calogErrArgE, "fsList expects (path)");
}
dir = opendir(args[0].as.s.bytes);
if (dir == NULL) {
return fsFail(result, errno);
}
status = calogAggCreate(&agg, calogListE);
if (status != calogOkE) {
closedir(dir);
return calogFail(result, status, "fsList: out of memory");
}
for (;;) {
errno = 0;
entry = readdir(dir);
if (entry == NULL) {
if (errno != 0) {
saved = errno;
calogAggFree(agg);
closedir(dir);
return fsFail(result, saved);
}
break;
}
if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0) {
continue;
}
status = calogValueString(&name, entry->d_name, (int64_t)strlen(entry->d_name));
if (status != calogOkE) {
calogAggFree(agg);
closedir(dir);
return calogFail(result, status, "fsList: out of memory");
}
status = calogAggPush(agg, &name);
if (status != calogOkE) {
calogValueFree(&name);
calogAggFree(agg);
closedir(dir);
return calogFail(result, status, "fsList: out of memory");
}
}
closedir(dir);
calogValueAgg(result, agg);
return calogOkE;
}
// Add keyName -> value to a map, taking ownership of value: on any failure both the freshly
// built key and the caller's value are freed, so the caller never double-frees.
static int32_t fsMapSet(CalogAggT *agg, const char *keyName, CalogValueT *value) {
CalogValueT key;
int32_t status;
status = calogValueString(&key, keyName, (int64_t)strlen(keyName));
if (status != calogOkE) {
calogValueFree(value);
return status;
}
status = calogAggSet(agg, &key, value);
if (status != calogOkE) {
calogValueFree(&key);
calogValueFree(value);
return status;
}
return calogOkE;
}
static int32_t fsMkdirNative(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) {
struct stat st;
const char *path;
(void)userData;
calogValueNil(result);
if (argCount != 1 || args[0].type != calogStringE) {
return calogFail(result, calogErrArgE, "fsMkdir expects (path)");
}
path = args[0].as.s.bytes;
if (mkdir(path, 0777) != 0) {
// An already-existing directory is success; anything else (including a non-directory
// squatting on the name) is the failure the caller sees.
if (errno == EEXIST && stat(path, &st) == 0 && S_ISDIR(st.st_mode)) {
calogValueNil(result);
return calogOkE;
}
return fsFail(result, errno);
}
calogValueNil(result);
return calogOkE;
}
// Shared body of fsWrite (truncate) and fsAppend: create the file if needed and write exactly
// length bytes, honoring embedded NULs. On success result is nil.
static int32_t fsPutFile(const char *path, const char *bytes, int64_t length, bool append, CalogValueT *result) {
size_t offset;
size_t total;
int fd;
int flags;
int saved;
flags = O_WRONLY | O_CREAT | (append ? O_APPEND : O_TRUNC);
fd = open(path, flags, 0666);
if (fd < 0) {
return fsFail(result, errno);
}
total = (length > 0) ? (size_t)length : (size_t)0;
offset = 0;
while (offset < total) {
ssize_t put;
put = write(fd, bytes + offset, total - offset);
if (put < 0) {
if (errno == EINTR) {
continue;
}
saved = errno;
close(fd);
return fsFail(result, saved);
}
offset += (size_t)put;
}
if (close(fd) != 0) {
return fsFail(result, errno);
}
calogValueNil(result);
return calogOkE;
}
static int32_t fsReadNative(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) {
struct stat st;
const char *path;
char *data;
ssize_t got;
size_t offset;
size_t total;
int fd;
int saved;
int32_t status;
(void)userData;
calogValueNil(result);
if (argCount != 1 || args[0].type != calogStringE) {
return calogFail(result, calogErrArgE, "fsRead expects (path)");
}
path = args[0].as.s.bytes;
fd = open(path, O_RDONLY);
if (fd < 0) {
return fsFail(result, errno);
}
if (fstat(fd, &st) != 0) {
saved = errno;
close(fd);
return fsFail(result, saved);
}
total = (st.st_size > 0) ? (size_t)st.st_size : (size_t)0;
data = (char *)malloc(total + 1);
if (data == NULL) {
close(fd);
return calogFail(result, calogErrOomE, "fsRead: out of memory");
}
offset = 0;
while (offset < total) {
got = read(fd, data + offset, total - offset);
if (got < 0) {
if (errno == EINTR) {
continue;
}
saved = errno;
free(data);
close(fd);
return fsFail(result, saved);
}
if (got == 0) {
break;
}
offset += (size_t)got;
}
close(fd);
status = calogValueString(result, data, (int64_t)offset);
free(data);
return status;
}
static int32_t fsRemoveNative(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) {
(void)userData;
calogValueNil(result);
if (argCount != 1 || args[0].type != calogStringE) {
return calogFail(result, calogErrArgE, "fsRemove expects (path)");
}
// unlink refuses a directory (EISDIR on Linux, EPERM elsewhere); remove an empty
// directory with rmdir instead, so fsRemove deletes either a file or an empty directory.
if (unlink(args[0].as.s.bytes) == 0) {
return calogOkE;
}
if ((errno == EISDIR || errno == EPERM) && rmdir(args[0].as.s.bytes) == 0) {
return calogOkE;
}
return fsFail(result, errno);
}
static int32_t fsStatNative(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) {
struct stat st;
CalogValueT value;
CalogAggT *agg;
const char *path;
int32_t status;
(void)userData;
calogValueNil(result);
if (argCount != 1 || args[0].type != calogStringE) {
return calogFail(result, calogErrArgE, "fsStat expects (path)");
}
path = args[0].as.s.bytes;
if (stat(path, &st) != 0) {
// A missing path is not an error: report nil so callers can probe with fsStat.
if (errno == ENOENT) {
calogValueNil(result);
return calogOkE;
}
return fsFail(result, errno);
}
status = calogAggCreate(&agg, calogMapE);
if (status != calogOkE) {
return calogFail(result, status, "fsStat: out of memory");
}
calogValueInt(&value, (int64_t)st.st_size);
status = fsMapSet(agg, "size", &value);
if (status != calogOkE) {
calogAggFree(agg);
return calogFail(result, status, "fsStat: out of memory");
}
calogValueBool(&value, S_ISDIR(st.st_mode) != 0);
status = fsMapSet(agg, "isDir", &value);
if (status != calogOkE) {
calogAggFree(agg);
return calogFail(result, status, "fsStat: out of memory");
}
calogValueBool(&value, S_ISREG(st.st_mode) != 0);
status = fsMapSet(agg, "isFile", &value);
if (status != calogOkE) {
calogAggFree(agg);
return calogFail(result, status, "fsStat: out of memory");
}
calogValueInt(&value, (int64_t)st.st_mtime);
status = fsMapSet(agg, "mtime", &value);
if (status != calogOkE) {
calogAggFree(agg);
return calogFail(result, status, "fsStat: out of memory");
}
calogValueAgg(result, agg);
return calogOkE;
}
static int32_t fsWriteNative(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) {
(void)userData;
calogValueNil(result);
if (argCount != 2 || args[0].type != calogStringE || args[1].type != calogStringE) {
return calogFail(result, calogErrArgE, "fsWrite expects (path, data)");
}
return fsPutFile(args[0].as.s.bytes, args[1].as.s.bytes, args[1].as.s.length, false, result);
}