Files
lovedos/src/filesystem.c

586 lines
13 KiB
C

/**
* Copyright (c) 2017 rxi
*
* This library is free software; you can redistribute it and/or modify it
* under the terms of the MIT license. See LICENSE for details.
*/
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <ctype.h>
#include <dirent.h>
#include <sys/stat.h>
#include "lib/microtar/microtar.h"
#include "lib/dmt/dmt.h"
#include "filesystem.h"
#define MAX_MOUNTS 8
#define MAX_PATH 256
enum {
FILESYSTEM_TNONE,
FILESYSTEM_TREG,
FILESYSTEM_TDIR,
};
typedef struct mount_t mount_t;
struct mount_t {
void (*unmount)(mount_t *mnt);
int (*exists)(mount_t *mnt, const char *filename);
int (*isFile)(mount_t *mnt, const char *filename);
int (*isDirectory)(mount_t *mnt, const char *filename);
void *(*read)(mount_t *mnt, const char *filename, int *size);
void *udata;
char path[MAX_PATH];
};
int filesystem_mountIdx;
mount_t filesystem_mounts[MAX_MOUNTS];
char filesystem_writeDir[MAX_PATH];
#define FOREACH_MOUNT(var)\
for (mount_t *var = &filesystem_mounts[filesystem_mountIdx - 1];\
var >= filesystem_mounts;\
var--)
static int get_file_type(const char *filename) {
/* The use of `stat` is intentionally avoided here, a stat call seems to
* block for a long time on DOS -- over 500ms in Dosbox at 26800 cycles */
DIR *dir = opendir(filename);
if (dir) {
closedir(dir);
return FILESYSTEM_TDIR;
}
FILE *fp = fopen(filename, "rb");
if (fp) {
fclose(fp);
return FILESYSTEM_TREG;
}
return FILESYSTEM_TNONE;
}
static int concat_path(char *dst, const char *dir, const char *filename) {
int dirlen = strlen(dir);
int filenamelen = strlen(filename);
/* Fail if the resultant path would overflow buffer */
if (dirlen + filenamelen + 2 > MAX_PATH) {
return FILESYSTEM_ETOOLONG;
}
/* Write full name to buffer and return ok */
if ( dir[dirlen - 1] == '/' ) {
sprintf(dst, "%s%s", dir, filename);
} else {
sprintf(dst, "%s/%s", dir, filename);
}
return FILESYSTEM_ESUCCESS;
}
static int concat_and_get_file_type(const char *dir, const char *filename) {
char buf[MAX_PATH];
/* Make fullpath */
int err = concat_path(buf, dir, filename);
if (err) {
return err;
}
/* Stat */
return get_file_type(buf);
}
static unsigned hash_string_ignore_case(const char *str) {
unsigned hash = 5381;
while (*str) {
hash = ((hash << 5) + hash) ^ tolower(*str++);
}
return hash;
}
static int strings_equal_ignore_case(const char *a, const char *b) {
while (*a) {
if (tolower(*a++) != tolower(*b++)) {
return 0;
}
}
return !*b;
}
static void strip_trailing_slash(char *str) {
int len = strlen(str);
if (len > 0 && str[len - 1] == '/') {
str[len - 1] = '\0';
}
}
static int is_separator(int chr) {
return (chr == '/' || chr == '\\');
}
static int make_dirs(const char *path) {
char str[MAX_PATH];
char *p = str;
int err = concat_path(str, path, "");
if (err) {
return err;
}
if (p[0] == '/') p++;
if (p[0] && p[1] == ':' && p[2] == '\\') p += 3;
while (*p) {
if (is_separator(*p)) {
*p = '\0';
if (get_file_type(str) != FILESYSTEM_TDIR) {
if (mkdir(str, S_IRWXU) == -1) {
return FILESYSTEM_EMKDIRFAIL;
}
}
*p = '/';
}
p++;
}
return FILESYSTEM_ESUCCESS;
}
/*==================*/
/* Directory mount */
/*==================*/
static void dir_unmount(mount_t *mnt) {
/* Intentionally empty */
}
static int dir_exists(mount_t *mnt, const char *filename) {
return concat_and_get_file_type(mnt->path, filename) != FILESYSTEM_TNONE;
}
static int dir_isFile(mount_t *mnt, const char *filename) {
return concat_and_get_file_type(mnt->path, filename) == FILESYSTEM_TREG;
}
static int dir_isDirectory(mount_t *mnt, const char *filename) {
return concat_and_get_file_type(mnt->path, filename) == FILESYSTEM_TDIR;
}
static void* dir_read(mount_t *mnt, const char *filename, int *size) {
char buf[MAX_PATH];
/* Make fullpath */
int err = concat_path(buf, mnt->path, filename);
if (err) {
return NULL;
}
/* Open file */
FILE *fp = fopen(buf, "rb");
if (!fp) {
return NULL;
}
/* Get size */
fseek(fp, 0, SEEK_END);
*size = ftell(fp);
fseek(fp, 0, SEEK_SET);
/* Load data */
void *p = dmt_malloc(*size);
if (!p) {
return NULL;
}
fread(p, 1, *size, fp);
/* Close file and return data */
fclose(fp);
return p;
}
static int dir_mount(mount_t *mnt, const char *path) {
/* Check the path is actually a directory */
if ( get_file_type(path) != FILESYSTEM_TDIR ) {
return FILESYSTEM_EFAILURE;
}
/* Init mount */
mnt->udata = NULL;
mnt->unmount = dir_unmount;
mnt->exists = dir_exists;
mnt->isFile = dir_isFile;
mnt->isDirectory = dir_isDirectory;
mnt->read = dir_read;
/* Return ok */
return FILESYSTEM_ESUCCESS;
}
/*==================*/
/* Tar mount */
/*==================*/
typedef struct { unsigned hash, pos; } tar_file_ref_t;
typedef struct {
mtar_t tar;
FILE *fp;
int offset;
tar_file_ref_t *map;
int nfiles;
} tar_mount_t;
static int tar_find(mount_t *mnt, const char *filename, mtar_header_t *h) {
/* Hash filename and linear search map for matching hash, read header and
* check against filename if the hashes match */
tar_mount_t *tm = mnt->udata;
unsigned hash = hash_string_ignore_case(filename);
int i;
for (i = 0; i < tm->nfiles; i++) {
if (tm->map[i].hash == hash) {
/* Seek to and load header */
mtar_seek(&tm->tar, tm->map[i].pos);
mtar_read_header(&tm->tar, h);
/* Compare names */
strip_trailing_slash(h->name);
if (strings_equal_ignore_case(h->name, filename)) {
return FILESYSTEM_ESUCCESS;
}
}
}
return FILESYSTEM_EFAILURE;
}
static void tar_unmount(mount_t *mnt) {
tar_mount_t *tm = mnt->udata;
mtar_close(&tm->tar);
dmt_free(tm->map);
dmt_free(tm);
}
static int tar_exists(mount_t *mnt, const char *filename) {
mtar_header_t h;
return tar_find(mnt, filename, &h) == FILESYSTEM_ESUCCESS;
}
static int tar_isFile(mount_t *mnt, const char *filename) {
mtar_header_t h;
int err = tar_find(mnt, filename, &h);
if (err) {
return 0;
}
return h.type == MTAR_TREG;
}
static int tar_isDirectory(mount_t *mnt, const char *filename) {
mtar_header_t h;
int err = tar_find(mnt, filename, &h);
if (err) {
return 0;
}
return h.type == MTAR_TDIR;
}
static void* tar_read(mount_t *mnt, const char *filename, int *size) {
mtar_t *tar = mnt->udata;
int err;
mtar_header_t h;
/* Find and load header for file */
err = tar_find(mnt, filename, &h);
if (err) {
return 0;
}
/* Allocate and read data, set size and return */
char *p = dmt_malloc(h.size);
err = mtar_read_data(tar, p, h.size);
if (err) {
dmt_free(p);
return NULL;
}
*size = h.size;
return p;
}
static int tar_stream_read(mtar_t *tar, void *data, unsigned size) {
tar_mount_t *tm = tar->stream;
unsigned res = fread(data, 1, size, tm->fp);
return (res == size) ? MTAR_ESUCCESS : MTAR_EREADFAIL;
}
static int tar_stream_seek(mtar_t *tar, unsigned offset) {
tar_mount_t *tm = tar->stream;
int res = fseek(tm->fp, tm->offset + offset, SEEK_SET);
return (res == 0) ? MTAR_ESUCCESS : MTAR_ESEEKFAIL;
}
static int tar_stream_close(mtar_t *tar) {
tar_mount_t *tm = tar->stream;
fclose(tm->fp);
return MTAR_ESUCCESS;
}
static int tar_mount(mount_t *mnt, const char *path) {
tar_mount_t *tm = NULL;
FILE *fp = NULL;
/* Try to open file */
fp = fopen(path, "rb");
if (!fp) {
goto fail;
}
/* Init tar_mount_t */
tm = dmt_calloc(1, sizeof(*tm));
tm->fp = fp;
/* Init tar */
mtar_t *tar = &tm->tar;
tar->read = tar_stream_read;
tar->seek = tar_stream_seek;
tar->close = tar_stream_close;
tar->stream = tm;
/* Check start of file for valid tar header */
mtar_header_t h;
int err = mtar_read_header(tar, &h);
/* If checking the start of the file failed then check the end of file for a
* "TAR\0" tag and offset, this would have been added when packaging (see
* `package.c`) to indicate the offset of the tar archive's beginning from the
* file's end */
if (err) {
int offset;
char buf[4] = "";
fseek(fp, -8, SEEK_END);
fread(buf, 1, 4, fp);
fread(&offset, 1, 4, fp);
if ( !memcmp(buf, "TAR\0", 4) ) {
fseek(fp, -offset, SEEK_END);
tm->offset = ftell(fp);
}
mtar_rewind(tar);
err = mtar_read_header(tar, &h);
if (err) {
goto fail;
}
}
/* Iterate all files and store [namehash:position] pairs; this is used by
* tar_find() */
mtar_rewind(tar);
int n = 0;
int cap = 0;
while ( (mtar_read_header(tar, &h)) == MTAR_ESUCCESS ) {
/* Realloc if map capacity was reached */
if (n >= cap) {
cap = cap ? (cap << 1) : 16;
tm->map = dmt_realloc(tm->map, cap * sizeof(*tm->map));
}
/* Store entry */
strip_trailing_slash(h.name);
tm->map[n].hash = hash_string_ignore_case(h.name);
tm->map[n].pos = tar->pos;
/* Next */
mtar_next(tar);
n++;
}
tm->nfiles = n;
/* Init mount */
mnt->udata = tar;
mnt->unmount = tar_unmount;
mnt->exists = tar_exists;
mnt->isFile = tar_isFile;
mnt->isDirectory = tar_isDirectory;
mnt->read = tar_read;
/* Return ok */
return FILESYSTEM_ESUCCESS;
fail:
if (fp) fclose(fp);
if (tm) {
dmt_free(tm->map);
dmt_free(tm);
}
return FILESYSTEM_EFAILURE;
}
/*==================*/
/* Filesystem */
/*==================*/
const char* filesystem_strerror(int err) {
switch (err) {
case FILESYSTEM_ESUCCESS : return "success";
case FILESYSTEM_EFAILURE : return "failure";
case FILESYSTEM_ETOOLONG : return "path too long";
case FILESYSTEM_EMOUNTED : return "path already mounted";
case FILESYSTEM_ENOMOUNT : return "path is not mounted";
case FILESYSTEM_EMOUNTFAIL : return "could not mount path";
case FILESYSTEM_ENOWRITEDIR : return "no write directory set";
case FILESYSTEM_EWRITEFAIL : return "could not write file";
case FILESYSTEM_EMKDIRFAIL : return "could not make directory";
}
return "unknown error";
}
void filesystem_deinit(void) {
FOREACH_MOUNT(mnt) {
mnt->unmount(mnt);
}
filesystem_mountIdx = 0;
}
int filesystem_mount(const char *path) {
/* Check path length is ok */
if ( strlen(path) >= MAX_PATH ) {
return FILESYSTEM_ETOOLONG;
}
/* Check path isn't already mounted */
FOREACH_MOUNT(m) {
if ( !strcmp(m->path, path) ) {
return FILESYSTEM_EMOUNTED;
}
}
/* Get mount slot */
if (filesystem_mountIdx >= MAX_MOUNTS) {
return FILESYSTEM_EFAILURE;
}
mount_t *mnt = &filesystem_mounts[filesystem_mountIdx++];
/* Copy path name */
strcpy(mnt->path, path);
/* Try to mount path */
if ( tar_mount(mnt, path) == FILESYSTEM_ESUCCESS ) goto success;
if ( dir_mount(mnt, path) == FILESYSTEM_ESUCCESS ) goto success;
/* Fail */
filesystem_mountIdx--;
return FILESYSTEM_EMOUNTFAIL;
success:
return FILESYSTEM_ESUCCESS;
}
int filesystem_unmount(const char *path) {
FOREACH_MOUNT(mnt) {
if ( !strcmp(mnt->path, path) ) {
/* Unmount */
mnt->unmount(mnt);
/* Shift remaining mounts to fill gap and decrement idx */
int idx = mnt - filesystem_mounts;
memmove(mnt, mnt + 1, (filesystem_mountIdx - idx - 1) * sizeof(mount_t));
filesystem_mountIdx--;
return FILESYSTEM_ESUCCESS;
}
}
return FILESYSTEM_ENOMOUNT;
}
int filesystem_exists(const char *filename) {
FOREACH_MOUNT(mnt) {
if ( mnt->exists(mnt, filename) ) {
return 1;
}
}
return 0;
}
int filesystem_isFile(const char *filename) {
FOREACH_MOUNT(mnt) {
if ( mnt->exists(mnt, filename) ) {
return mnt->isFile(mnt, filename);
}
}
return 0;
}
int filesystem_isDirectory(const char *filename) {
FOREACH_MOUNT(mnt) {
if ( mnt->exists(mnt, filename) ) {
return mnt->isDirectory(mnt, filename);
}
}
return 0;
}
void* filesystem_read(const char *filename, int *size) {
FOREACH_MOUNT(mnt) {
if ( mnt->exists(mnt, filename) && mnt->isFile(mnt, filename) ) {
return mnt->read(mnt, filename, size);
}
}
return NULL;
}
void filesystem_free(void *ptr) {
dmt_free(ptr);
}
int filesystem_setWriteDir(const char *path) {
if (strlen(path) >= MAX_PATH) {
return FILESYSTEM_ETOOLONG;
}
int err = make_dirs(path);
if (err) {
return err;
}
strcpy(filesystem_writeDir, path);
return FILESYSTEM_ESUCCESS;
}
int filesystem_write(const char *filename, const void *data, int size) {
int err, n;
char buf[MAX_PATH];
if (!*filesystem_writeDir) {
return FILESYSTEM_ENOWRITEDIR;
}
err = concat_path(buf, filesystem_writeDir, filename);
if (err) {
return err;
}
FILE *fp = fopen(buf, "wb");
if (!fp) {
return FILESYSTEM_EWRITEFAIL;
}
n = fwrite(data, 1, size, fp);
fclose(fp);
return n == size ? FILESYSTEM_ESUCCESS : FILESYSTEM_EWRITEFAIL;
}