joeylib2/tests/host/fileRootHost.c
2026-10-13 14:01:46 -05:00

375 lines
12 KiB
C

// Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License.
// See LICENSE.md for license information.
// SPDX-License-Identifier: MIT
//
// fileRootHost.c - Host-cc gate for the run-time file roots
// (src/core/fileRoot.h, JL_HAS_FILE_ROOTS): where jlDataOpen reads DATA/,
// and where the save calls and the log write SAVES/ and joeylog.txt.
//
// Built by scripts/check-fileroots.sh as a whole BLANK-port program (core +
// generics + src/blank/blank.c + this file) twice, each with the POSIX save
// HAL below (-DJL_HAS_SAVE_DIR_ENSURE -DJL_HAS_SAVE_DELETE, as check-save.sh
// does):
// roots -DJL_HAS_FILE_ROOTS: this file is the port, its jlpReadRoot and
// jlpWriteRoot the folders argv[1] and argv[2] (each ending in '/'),
// the write root made the first time it is asked for, as a port
// makes it. DATA/ must be read under the read root, SAVES/ and
// joeylog.txt written under the write root and never in the
// current directory (which holds a decoy DATA/); jlLogClose must
// close the log and the next line append to it; reading data must
// not ask for the write root; an empty root must mean the current
// directory, the layout without the flag.
// plain without the flag, as every other port builds the core: DATA/,
// SAVES/ and joeylog.txt relative to the current directory.
// Both check that a path too long for the core's buffers fails cleanly and
// leaves no file behind. Each build runs in an empty current directory of
// its own, and writes its fixtures itself. Prints one line per check and a
// final tally; exits nonzero on any failure.
#include <dirent.h>
#include <errno.h>
#include <stdio.h>
#include <string.h>
#include <sys/stat.h>
#include "joey/joey.h"
#include "joey/file.h"
#include "port.h"
#include "fileRoot.h"
// ----- Constants -----
#define DATA_DIR "DATA"
#define SAVES_DIR "SAVES"
#define LOG_NAME "joeylog.txt"
#define PATH_SEPARATOR "/"
#define DIR_MODE 0777
#define PROBE_NAME "probe.dat"
#define MISSING_NAME "missing.dat"
#define SAVE_NAME "slot.sav"
#define STREAM_NAME "stream.sav"
// The two copies of PROBE_NAME: the current directory's is a decoy while
// the roots are set.
#define CWD_TEXT "the current directory's DATA"
#define ROOT_TEXT "the read root's DATA"
#define SAVE_TEXT "a save file"
#define STREAM_TEXT "a streamed save file"
#define LOG_FIRST "the first log line"
#define LOG_SECOND "the log line after jlLogClose"
#define LOG_THIRD "the log line after the log moved away"
#define LOG_MOVED_SUFFIX ".moved"
#define LOG_EMPTY_ROOT "the log line with an empty root"
// Longer than every path buffer in the core (256 bytes).
#define LONG_NAME_CHARS 300
#define LONG_NAME_CHAR 'x'
#define PATH_BYTES 1024
#define FILE_BYTES 256
#define ARG_READ_ROOT 1
#define ARG_WRITE_ROOT 2
#define ROOTS_ARGC 3
// ----- Prototypes -----
static void check(const char *name, bool ok);
static bool dataReads(const char *text);
static int dirEntries(const char *dir);
static bool dirMake(const char *dir);
static bool fileHolds(const char *path, const char *text);
static bool fileWrite(const char *path, const char *text);
static bool fixtureWrite(const char *root, const char *text);
static bool pathExists(const char *path);
static void pathJoin(char *out, const char *root, const char *rel, const char *name);
static void testLog(const char *root, bool reopen);
static void testLongName(const char *root);
static void testSaves(const char *root);
// ----- Module state -----
static int gPass = 0;
static int gFail = 0;
#if defined(JL_HAS_FILE_ROOTS)
static const char *gReadRoot = "";
static const char *gWriteRoot = "";
static bool gWriteRootMade = false;
static int gWriteRootAsks = 0;
#endif
// ----- The port (POSIX save HAL; the roots under JL_HAS_FILE_ROOTS) -----
#if defined(JL_HAS_FILE_ROOTS)
const char *jlpReadRoot(void) {
return gReadRoot;
}
#endif
bool jlpSaveDelete(const char *path) {
return remove(path) == 0;
}
bool jlpSaveDirEnsure(const char *dir) {
return dirMake(dir);
}
#if defined(JL_HAS_FILE_ROOTS)
// Made on its first use, as a port makes its settings folder.
const char *jlpWriteRoot(void) {
gWriteRootAsks++;
if (!gWriteRootMade) {
gWriteRootMade = gWriteRoot[0] == '\0' || dirMake(gWriteRoot);
}
return gWriteRoot;
}
#endif
// ----- Helpers (alphabetical) -----
static void check(const char *name, bool ok) {
if (ok) {
gPass++;
printf(" ok %s\n", name);
} else {
gFail++;
printf(" FAIL %s\n", name);
}
}
// jlDataOpen finds PROBE_NAME, and it holds text.
static bool dataReads(const char *text) {
char buf[FILE_BYTES];
size_t got;
FILE *fp;
fp = jlDataOpen(PROBE_NAME, "rb");
if (fp == NULL) {
return false;
}
got = fread(buf, 1, sizeof(buf) - 1, fp);
fclose(fp);
buf[got] = '\0';
return strcmp(buf, text) == 0;
}
// The entries in dir, . and .. aside; -1 when it cannot be read.
static int dirEntries(const char *dir) {
DIR *d;
struct dirent *e;
int count;
d = opendir(dir);
if (d == NULL) {
return -1;
}
count = 0;
while ((e = readdir(d)) != NULL) {
if (strcmp(e->d_name, ".") != 0 && strcmp(e->d_name, "..") != 0) {
count++;
}
}
closedir(d);
return count;
}
static bool dirMake(const char *dir) {
struct stat st;
if (mkdir(dir, DIR_MODE) == 0) {
return true;
}
return errno == EEXIST && stat(dir, &st) == 0 && S_ISDIR(st.st_mode);
}
// The file at path holds exactly text.
static bool fileHolds(const char *path, const char *text) {
char buf[FILE_BYTES];
size_t got;
FILE *fp;
fp = fopen(path, "rb");
if (fp == NULL) {
return false;
}
got = fread(buf, 1, sizeof(buf) - 1, fp);
fclose(fp);
buf[got] = '\0';
return strcmp(buf, text) == 0;
}
static bool fileWrite(const char *path, const char *text) {
FILE *fp;
bool ok;
fp = fopen(path, "wb");
if (fp == NULL) {
return false;
}
ok = fputs(text, fp) >= 0;
return fclose(fp) == 0 && ok;
}
// root + DATA/PROBE_NAME holding text, the folders made first.
static bool fixtureWrite(const char *root, const char *text) {
char path[PATH_BYTES];
if (root[0] != '\0' && !dirMake(root)) {
return false;
}
pathJoin(path, root, DATA_DIR, "");
if (!dirMake(path)) {
return false;
}
pathJoin(path, root, DATA_DIR PATH_SEPARATOR, PROBE_NAME);
return fileWrite(path, text);
}
static bool pathExists(const char *path) {
struct stat st;
return stat(path, &st) == 0;
}
static void pathJoin(char *out, const char *root, const char *rel, const char *name) {
(void)snprintf(out, PATH_BYTES, "%s%s%s", root, rel, name);
}
// jlLog writes root + joeylog.txt. With reopen: after jlLogClose the next
// line is appended to it; and jlLogClose really closes it: with the file
// moved away, the next line starts a new one.
static void testLog(const char *root, bool reopen) {
char path[PATH_BYTES];
#if defined(JL_HAS_FILE_ROOTS)
char moved[PATH_BYTES];
#endif
pathJoin(path, root, LOG_NAME, "");
jlLog(LOG_FIRST);
check("the log line lands in joeylog.txt there", fileHolds(path, LOG_FIRST "\n"));
#if defined(JL_HAS_FILE_ROOTS)
if (reopen) {
jlLogClose();
jlLogF("%s", LOG_SECOND);
check("after jlLogClose the next line is appended", fileHolds(path, LOG_FIRST "\n" LOG_SECOND "\n"));
jlLogClose();
pathJoin(moved, root, LOG_NAME, LOG_MOVED_SUFFIX);
check("the log moves away", rename(path, moved) == 0);
jlLog(LOG_THIRD);
check("jlLogClose closed the file: the next line starts a new one", fileHolds(path, LOG_THIRD "\n") && fileHolds(moved, LOG_FIRST "\n" LOG_SECOND "\n"));
}
#else
(void)reopen;
#endif
}
// A name too long for the core's path buffers: every call fails, and no
// file appears (a cut-short path would make one).
static void testLongName(const char *root) {
char name[LONG_NAME_CHARS + 1];
char saves[PATH_BYTES];
int before;
memset(name, LONG_NAME_CHAR, LONG_NAME_CHARS);
name[LONG_NAME_CHARS] = '\0';
pathJoin(saves, root, SAVES_DIR, "");
before = dirEntries(saves);
check("too long: jlDataOpen gives NULL", jlDataOpen(name, "rb") == NULL);
check("too long: jlSaveWrite fails", !jlSaveWrite(name, SAVE_TEXT, (uint32_t)strlen(SAVE_TEXT)));
check("too long: jlSaveOpen gives NULL", jlSaveOpen(name, "wb") == NULL);
check("too long: jlSaveExists is false", !jlSaveExists(name));
check("too long: no file appeared in SAVES/", dirEntries(saves) == before);
}
// The save calls write, read, stream and delete in root + SAVES/.
static void testSaves(const char *root) {
char path[PATH_BYTES];
char buf[FILE_BYTES];
uint32_t got;
FILE *fp;
pathJoin(path, root, SAVES_DIR PATH_SEPARATOR, SAVE_NAME);
check("jlSaveWrite", jlSaveWrite(SAVE_NAME, SAVE_TEXT, (uint32_t)strlen(SAVE_TEXT)));
check("the save file is in SAVES/ there", fileHolds(path, SAVE_TEXT));
check("jlSaveExists finds it", jlSaveExists(SAVE_NAME));
memset(buf, 0, sizeof(buf));
got = jlSaveRead(SAVE_NAME, buf, sizeof(buf) - 1);
check("jlSaveRead reads it back", got == strlen(SAVE_TEXT) && strcmp(buf, SAVE_TEXT) == 0);
fp = jlSaveOpen(STREAM_NAME, "wb");
check("jlSaveOpen", fp != NULL);
if (fp != NULL) {
fputs(STREAM_TEXT, fp);
fclose(fp);
}
pathJoin(path, root, SAVES_DIR PATH_SEPARATOR, STREAM_NAME);
check("the streamed file is in SAVES/ there", fileHolds(path, STREAM_TEXT));
pathJoin(path, root, SAVES_DIR PATH_SEPARATOR, SAVE_NAME);
check("jlSaveDelete", jlSaveDelete(SAVE_NAME));
check("the save file is gone from SAVES/ there", !pathExists(path));
}
int main(int argc, char **argv) {
#if defined(JL_HAS_FILE_ROOTS)
if (argc != ROOTS_ARGC) {
fprintf(stderr, "usage: %s READROOT/ WRITEROOT/ (run in an empty directory)\n", argv[0]);
return 2;
}
gReadRoot = argv[ARG_READ_ROOT];
gWriteRoot = argv[ARG_WRITE_ROOT];
printf("fileroot-host: roots (JL_HAS_FILE_ROOTS): read %s, write %s\n", gReadRoot, gWriteRoot);
check("fixtures: DATA/ in the current directory and the read root", fixtureWrite("", CWD_TEXT) && fixtureWrite(gReadRoot, ROOT_TEXT));
check("the write root does not exist yet", !pathExists(gWriteRoot));
check("jlDataOpen reads the read root's DATA/, not the current directory's", dataReads(ROOT_TEXT));
check("jlDataOpen of a missing file gives NULL", jlDataOpen(MISSING_NAME, "rb") == NULL);
check("reading data never asked for the write root", gWriteRootAsks == 0 && !pathExists(gWriteRoot));
testSaves(gWriteRoot);
testLog(gWriteRoot, true);
testLongName(gWriteRoot);
check("nothing written in the current directory", !pathExists(SAVES_DIR) && !pathExists(LOG_NAME));
// An empty root is the current directory: the layout without the flag.
printf("fileroot-host: roots, both empty\n");
jlLogClose();
gReadRoot = "";
gWriteRoot = "";
check("jlDataOpen reads the current directory's DATA/", dataReads(CWD_TEXT));
testSaves("");
jlLog(LOG_EMPTY_ROOT);
check("the log line lands in the current directory's joeylog.txt", fileHolds(LOG_NAME, LOG_EMPTY_ROOT "\n"));
jlLogClose();
#else
(void)argc;
(void)argv;
printf("fileroot-host: plain (no JL_HAS_FILE_ROOTS)\n");
check("fixture: DATA/ in the current directory", fixtureWrite("", CWD_TEXT));
check("jlDataOpen reads the current directory's DATA/", dataReads(CWD_TEXT));
check("jlDataOpen of a missing file gives NULL", jlDataOpen(MISSING_NAME, "rb") == NULL);
testSaves("");
testLog("", false);
testLongName("");
#endif
printf("fileroot-host: %d passed, %d failed\n", gPass, gFail);
return gFail == 0 ? 0 : 1;
}