371 lines
12 KiB
C
371 lines
12 KiB
C
// 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;
|
|
}
|