calog/libs/calogFs.c

405 lines
14 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 "calogInternal.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>
#ifdef _WIN32
#include <io.h> // _chmod (clear the read-only bit before deleting)
#endif
// Windows (mingw/MSVCRT) opens files in TEXT mode by default -- read() stops at Ctrl-Z and CRLF<->LF
// translation corrupts data. O_BINARY forces byte-for-byte I/O there; it is 0 (no-op) on POSIX, so
// the file natives are binary-safe on all three ports.
#ifndef O_BINARY
#define O_BINARY 0
#endif
// mingw's `struct stat` carries a 32-bit st_size (LLP64), so a file >= 2 GiB reports a truncated
// size; the _stat64 variant has a 64-bit st_size. POSIX stat is already 64-bit on the supported
// targets, so this only redirects the Windows build.
#ifdef _WIN32
typedef struct _stat64 CalogStatT;
#define calogStat _stat64
#else
typedef struct stat CalogStatT;
#define calogStat stat
#endif
// fsRead's starting buffer capacity when fstat's st_size reports 0 (procfs/sysfs/FIFO-style
// files, whose content is read regardless by growing this buffer to EOF).
#define FS_READ_INITIAL_CAP 4096
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;
#ifdef _WIN32
// The Win32 CRT mkdir takes no mode argument (the mode is meaningless on Windows ACLs).
if (mkdir(path) != 0) {
#else
if (mkdir(path, 0777) != 0) {
#endif
// 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)) {
return calogOkE;
}
return fsFail(result, errno);
}
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 | O_BINARY | (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;
void *buffer;
ssize_t got;
int64_t cap;
int64_t initialCap;
size_t offset;
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 | O_BINARY);
if (fd < 0) {
return fsFail(result, errno);
}
if (fstat(fd, &st) != 0) {
saved = errno;
close(fd);
return fsFail(result, saved);
}
// st_size is only a hint: procfs/sysfs/FIFO-style files can report 0 (or an
// understated size) yet still have real content, so keep reading past it until
// read() hits EOF instead of trusting st_size as the exact length.
initialCap = (st.st_size > 0) ? (int64_t)st.st_size + 1 : FS_READ_INITIAL_CAP;
buffer = NULL;
cap = 0;
status = calogGrow(&buffer, &cap, initialCap, sizeof(char));
if (status != calogOkE) {
close(fd);
return calogFail(result, status, "fsRead: out of memory");
}
data = (char *)buffer;
offset = 0;
for (;;) {
if (offset == (size_t)cap) {
buffer = data;
status = calogGrow(&buffer, &cap, cap + 1, sizeof(char));
if (status != calogOkE) {
free(data);
close(fd);
return calogFail(result, status, "fsRead: out of memory");
}
data = (char *)buffer;
}
got = read(fd, data + offset, (size_t)cap - 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 on some POSIX, EACCES on Windows); 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;
}
#ifdef _WIN32
// Windows _unlink refuses a read-only file with EACCES; clear the read-only bit and retry (an
// EACCES from another open handle is a genuine OS limitation and still fails below).
if (errno == EACCES && _chmod(args[0].as.s.bytes, _S_IWRITE) == 0 && unlink(args[0].as.s.bytes) == 0) {
return calogOkE;
}
#endif
if ((errno == EISDIR || errno == EPERM || errno == EACCES) && 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) {
CalogStatT 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 (calogStat(path, &st) != 0) {
// A missing path is not an error: report nil so callers can probe with fsStat.
if (errno == ENOENT) {
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);
}