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