calog/libs/calogFs.c

512 lines
18 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 <stdio.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
#define calogFstat _fstat64
#define calogSeek _lseeki64
#else
typedef struct stat CalogStatT;
#define calogStat stat
#define calogFstat fstat
#define calogSeek lseek
#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 fsReadRange(const char *path, int64_t start, int64_t length, CalogValueT *result);
static int32_t fsRemoveNative(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);
static int32_t fsRenameNative(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);
// Every inline native this library exposes. One table so registration is a single
// checked call (calogRegisterBatch) rather than a run of calls whose status was dropped.
static const CalogNativeEntryT gFsNatives[] = {
{ "fsRead", fsReadNative },
{ "fsWrite", fsWriteNative },
{ "fsAppend", fsAppendNative },
{ "fsExists", fsExistsNative },
{ "fsRemove", fsRemoveNative },
{ "fsRename", fsRenameNative },
{ "fsMkdir", fsMkdirNative },
{ "fsList", fsListNative },
{ "fsStat", fsStatNative },
};
int32_t calogFsRegister(CalogT *calog) {
return calogRegisterBatch(calog, gFsNatives, (int64_t)(sizeof(gFsNatives) / sizeof(gFsNatives[0])), NULL);
}
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 || argCount > 3 || args[0].type != calogStringE || (argCount >= 2 && args[1].type != calogIntE) || (argCount == 3 && args[2].type != calogIntE)) {
return calogFail(result, calogErrArgE, "fsRead expects (path [, offset [, length]])");
}
path = args[0].as.s.bytes;
// A piece of the file: from offset, length bytes or to the end. A file too large to hold is
// read this way a piece at a time.
if (argCount >= 2) {
if (args[1].as.i < 0 || (argCount == 3 && args[2].as.i < 0)) {
return calogFail(result, calogErrRangeE, "fsRead: offset and length must not be negative");
}
return fsReadRange(path, args[1].as.i, (argCount == 3) ? args[2].as.i : -1, result);
}
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;
}
// length bytes of path from start, or to the end when length is negative; fewer when the file ends
// first, and "" from at or past the end. The buffer is the size of what will be read, never of
// what was asked for, so a bogus length cannot ask for memory the file does not justify.
static int32_t fsReadRange(const char *path, int64_t start, int64_t length, CalogValueT *result) {
CalogStatT st;
char *data;
int64_t available;
int64_t have;
ssize_t got;
int fd;
int saved;
int32_t status;
fd = open(path, O_RDONLY | O_BINARY);
if (fd < 0) {
return fsFail(result, errno);
}
if (calogFstat(fd, &st) != 0) {
saved = errno;
close(fd);
return fsFail(result, saved);
}
available = ((int64_t)st.st_size > start) ? (int64_t)st.st_size - start : 0;
if (length < 0 || length > available) {
length = available;
}
if (length == 0) {
close(fd);
return calogValueString(result, "", 0);
}
if (calogSeek(fd, start, SEEK_SET) < 0) {
saved = errno;
close(fd);
return fsFail(result, saved);
}
data = (char *)malloc((size_t)length);
if (data == NULL) {
close(fd);
return calogFail(result, calogErrOomE, "fsRead: out of memory");
}
have = 0;
while (have < length) {
got = read(fd, data + have, (size_t)(length - have));
if (got < 0) {
if (errno == EINTR) {
continue;
}
saved = errno;
free(data);
close(fd);
return fsFail(result, saved);
}
if (got == 0) {
break;
}
have += (int64_t)got;
}
close(fd);
status = calogValueString(result, data, have);
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);
}
// Moves from to to, replacing to when it exists (Windows refuses to rename over a file, so there it
// is removed first). Within one filesystem this is how a finished file takes its place whole.
static int32_t fsRenameNative(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, "fsRename expects (from, to)");
}
#ifdef _WIN32
// Windows rename refuses an existing target, and _unlink refuses a read-only one.
if (_chmod(args[1].as.s.bytes, _S_IWRITE) == 0) {
unlink(args[1].as.s.bytes);
}
#endif
if (rename(args[0].as.s.bytes, args[1].as.s.bytes) != 0) {
return fsFail(result, errno);
}
return calogOkE;
}
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);
}