// 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 #include #include #include #include #include #include #include #include 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); }