352 lines
12 KiB
C
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);
|
|
}
|