2016-11-19 09:28:01 +00:00
|
|
|
#include "filesystem/filesystem.h"
|
2019-11-25 01:27:17 +00:00
|
|
|
#include "core/arr.h"
|
|
|
|
#include "core/fs.h"
|
|
|
|
#include "core/map.h"
|
2020-06-25 19:28:40 +00:00
|
|
|
#include "core/os.h"
|
2020-05-19 19:49:40 +00:00
|
|
|
#include "core/util.h"
|
2019-11-25 01:27:17 +00:00
|
|
|
#include "core/zip.h"
|
|
|
|
#include "lib/stb/stb_image.h"
|
2018-07-04 20:51:35 +00:00
|
|
|
#include <string.h>
|
2019-11-25 01:27:17 +00:00
|
|
|
#include <stdlib.h>
|
2019-12-08 09:46:54 +00:00
|
|
|
#include <time.h>
|
2019-11-25 01:27:17 +00:00
|
|
|
|
|
|
|
#define FOREACH_ARCHIVE(a) for (Archive* a = state.archives.data; a != state.archives.data + state.archives.length; a++)
|
|
|
|
|
|
|
|
typedef arr_t(char) strpool;
|
|
|
|
|
|
|
|
static size_t strpool_append(strpool* pool, const char* string, size_t length) {
|
|
|
|
size_t tip = pool->length;
|
|
|
|
arr_reserve(pool, pool->length + length + 1);
|
|
|
|
memcpy(pool->data + tip, string, length);
|
|
|
|
pool->data[tip + length] = '\0';
|
|
|
|
pool->length += length + 1;
|
|
|
|
return tip;
|
|
|
|
}
|
|
|
|
|
|
|
|
static const char* strpool_resolve(strpool* pool, size_t offset) {
|
|
|
|
return pool->data + offset;
|
|
|
|
}
|
|
|
|
|
|
|
|
typedef struct {
|
|
|
|
uint32_t firstChild;
|
|
|
|
uint32_t nextSibling;
|
|
|
|
size_t filename;
|
|
|
|
uint64_t offset;
|
2020-02-21 23:44:48 +00:00
|
|
|
uint64_t csize;
|
2019-12-08 09:46:54 +00:00
|
|
|
uint16_t mdate;
|
|
|
|
uint16_t mtime;
|
2019-11-25 01:27:17 +00:00
|
|
|
FileInfo info;
|
|
|
|
} zip_node;
|
|
|
|
|
|
|
|
typedef struct Archive {
|
|
|
|
bool (*stat)(struct Archive* archive, const char* path, FileInfo* info);
|
2019-12-08 09:46:54 +00:00
|
|
|
void (*list)(struct Archive* archive, const char* path, fs_list_cb callback, void* context);
|
2019-11-25 01:27:17 +00:00
|
|
|
bool (*read)(struct Archive* archive, const char* path, size_t bytes, size_t* bytesRead, void** data);
|
2020-01-20 23:53:43 +00:00
|
|
|
void (*close)(struct Archive* archive);
|
2019-11-25 01:27:17 +00:00
|
|
|
zip_state zip;
|
|
|
|
strpool strings;
|
|
|
|
arr_t(zip_node) nodes;
|
|
|
|
map_t lookup;
|
|
|
|
size_t path;
|
|
|
|
size_t pathLength;
|
|
|
|
size_t mountpoint;
|
|
|
|
size_t mountpointLength;
|
|
|
|
} Archive;
|
2016-11-07 22:30:32 +00:00
|
|
|
|
2019-05-20 10:51:22 +00:00
|
|
|
static struct {
|
|
|
|
bool initialized;
|
2019-11-25 01:27:17 +00:00
|
|
|
arr_t(Archive) archives;
|
|
|
|
size_t savePathLength;
|
|
|
|
char savePath[1024];
|
|
|
|
char source[1024];
|
2019-06-08 10:45:03 +00:00
|
|
|
char requirePath[2][1024];
|
2020-06-26 00:11:56 +00:00
|
|
|
char identity[64];
|
2019-11-25 01:27:17 +00:00
|
|
|
bool fused;
|
2019-05-20 10:51:22 +00:00
|
|
|
} state;
|
2018-01-26 02:32:16 +00:00
|
|
|
|
2020-04-22 07:47:16 +00:00
|
|
|
// Rejects any path component that would escape the virtual filesystem (./, ../, :, and \)
|
2019-11-25 01:27:17 +00:00
|
|
|
static bool valid(const char* path) {
|
2019-12-08 09:46:54 +00:00
|
|
|
if (path[0] == '.' && (path[1] == '\0' || path[1] == '.')) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2019-11-25 01:27:17 +00:00
|
|
|
do {
|
2019-12-08 09:46:54 +00:00
|
|
|
if (
|
|
|
|
*path == ':' ||
|
|
|
|
*path == '\\' ||
|
|
|
|
(*path == '/' && path[1] == '.' &&
|
|
|
|
(path[2] == '.' ? (path[3] == '/' || path[3] == '\0') : (path[2] == '/' || path[2] == '\0')))
|
|
|
|
) {
|
2019-11-25 01:27:17 +00:00
|
|
|
return false;
|
|
|
|
}
|
2019-12-08 09:46:54 +00:00
|
|
|
} while (*path++ != '\0');
|
|
|
|
|
2019-11-25 01:27:17 +00:00
|
|
|
return true;
|
|
|
|
}
|
2019-09-27 03:19:50 +00:00
|
|
|
|
2019-12-08 09:46:54 +00:00
|
|
|
// Does not work with empty strings
|
2019-11-25 01:27:17 +00:00
|
|
|
static bool concat(char* buffer, const char* p1, size_t length1, const char* p2, size_t length2) {
|
|
|
|
if (length1 + 1 + length2 >= LOVR_PATH_MAX) return false;
|
|
|
|
memcpy(buffer + length1 + 1, p2, length2);
|
|
|
|
buffer[length1 + 1 + length2] = '\0';
|
|
|
|
memcpy(buffer, p1, length1);
|
|
|
|
buffer[length1] = '/';
|
|
|
|
return true;
|
|
|
|
}
|
2019-09-27 03:19:50 +00:00
|
|
|
|
2019-11-25 01:27:17 +00:00
|
|
|
static size_t normalize(char* buffer, const char* path, size_t length) {
|
|
|
|
size_t i = 0;
|
|
|
|
size_t n = 0;
|
|
|
|
while (path[i] == '/') i++;
|
|
|
|
while (i < length) {
|
|
|
|
buffer[n++] = path[i++];
|
|
|
|
while (path[i] == '/' && (path[i + 1] == '/' || path[i + 1] == '\0')) {
|
|
|
|
i++;
|
|
|
|
}
|
2019-09-27 03:19:50 +00:00
|
|
|
}
|
2019-11-25 01:27:17 +00:00
|
|
|
buffer[n] = '\0';
|
|
|
|
return n;
|
2019-09-27 03:19:50 +00:00
|
|
|
}
|
|
|
|
|
2018-11-19 16:08:56 +00:00
|
|
|
bool lovrFilesystemInit(const char* argExe, const char* argGame, const char* argRoot) {
|
|
|
|
if (state.initialized) return false;
|
2018-02-23 03:18:36 +00:00
|
|
|
state.initialized = true;
|
2018-01-26 02:32:16 +00:00
|
|
|
|
2019-11-25 01:27:17 +00:00
|
|
|
arr_init(&state.archives);
|
|
|
|
arr_reserve(&state.archives, 2);
|
2016-11-07 22:30:32 +00:00
|
|
|
|
2018-10-05 07:40:50 +00:00
|
|
|
lovrFilesystemSetRequirePath("?.lua;?/init.lua;lua_modules/?.lua;lua_modules/?/init.lua;deps/?.lua;deps/?/init.lua");
|
2018-10-05 07:41:22 +00:00
|
|
|
lovrFilesystemSetCRequirePath("??;lua_modules/??;deps/??");
|
2017-03-11 09:37:00 +00:00
|
|
|
|
2019-11-25 01:27:17 +00:00
|
|
|
// First, try to mount a bundled archive
|
2020-06-25 19:28:40 +00:00
|
|
|
if (lovrPlatformGetBundlePath(state.source, LOVR_PATH_MAX) && lovrFilesystemMount(state.source, NULL, true, "/assets")) {
|
2019-11-25 01:27:17 +00:00
|
|
|
state.fused = true;
|
|
|
|
return true;
|
|
|
|
}
|
2017-04-02 12:55:21 +00:00
|
|
|
|
2019-11-25 01:27:17 +00:00
|
|
|
// If that didn't work, try mounting an archive passed in from the command line
|
|
|
|
if (argGame) {
|
|
|
|
state.source[LOVR_PATH_MAX - 1] = '\0';
|
|
|
|
strncpy(state.source, argGame, LOVR_PATH_MAX - 1);
|
|
|
|
if (lovrFilesystemMount(state.source, NULL, true, argRoot)) {
|
|
|
|
return true;
|
2017-03-11 09:37:00 +00:00
|
|
|
}
|
|
|
|
}
|
2018-11-19 16:08:56 +00:00
|
|
|
|
2019-11-25 01:27:17 +00:00
|
|
|
// Otherwise, there is no source
|
|
|
|
state.source[0] = '\0';
|
2018-11-19 16:08:56 +00:00
|
|
|
return true;
|
2016-11-01 00:14:31 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
void lovrFilesystemDestroy() {
|
2018-02-23 03:18:36 +00:00
|
|
|
if (!state.initialized) return;
|
2019-12-20 12:13:38 +00:00
|
|
|
for (size_t i = 0; i < state.archives.length; i++) {
|
|
|
|
Archive* archive = &state.archives.data[i];
|
|
|
|
archive->close(archive);
|
|
|
|
}
|
|
|
|
arr_free(&state.archives);
|
2019-05-20 10:51:22 +00:00
|
|
|
memset(&state, 0, sizeof(state));
|
2016-11-01 00:14:31 +00:00
|
|
|
}
|
|
|
|
|
2019-11-25 01:27:17 +00:00
|
|
|
const char* lovrFilesystemGetSource() {
|
|
|
|
return state.source;
|
|
|
|
}
|
|
|
|
|
|
|
|
bool lovrFilesystemIsFused() {
|
|
|
|
return state.fused;
|
2017-03-11 09:37:00 +00:00
|
|
|
}
|
2016-11-07 22:31:11 +00:00
|
|
|
|
2019-11-25 01:27:17 +00:00
|
|
|
// Archives
|
|
|
|
|
|
|
|
static bool dir_init(Archive* archive, const char* path, const char* mountpoint, const char* root);
|
|
|
|
static bool zip_init(Archive* archive, const char* path, const char* mountpoint, const char* root);
|
|
|
|
|
|
|
|
bool lovrFilesystemMount(const char* path, const char* mountpoint, bool append, const char* root) {
|
2019-12-08 09:46:54 +00:00
|
|
|
FOREACH_ARCHIVE(archive) {
|
|
|
|
if (!strcmp(strpool_resolve(&archive->strings, archive->path), path)) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-11-25 01:27:17 +00:00
|
|
|
Archive archive;
|
|
|
|
arr_init(&archive.strings);
|
|
|
|
|
|
|
|
if (!dir_init(&archive, path, mountpoint, root) && !zip_init(&archive, path, mountpoint, root)) {
|
|
|
|
arr_free(&archive.strings);
|
|
|
|
return false;
|
2016-11-07 22:31:11 +00:00
|
|
|
}
|
|
|
|
|
2019-11-25 01:27:17 +00:00
|
|
|
archive.pathLength = strlen(path);
|
|
|
|
archive.path = strpool_append(&archive.strings, path, archive.pathLength);
|
|
|
|
|
|
|
|
if (mountpoint) {
|
2020-01-20 22:12:37 +00:00
|
|
|
char buffer[LOVR_PATH_MAX];
|
|
|
|
size_t length = strlen(mountpoint);
|
|
|
|
if (length >= sizeof(buffer)) return false;
|
|
|
|
length = normalize(buffer, mountpoint, length);
|
|
|
|
archive.mountpointLength = length;
|
|
|
|
archive.mountpoint = strpool_append(&archive.strings, buffer, archive.mountpointLength);
|
2019-11-25 01:27:17 +00:00
|
|
|
} else {
|
|
|
|
archive.mountpointLength = 0;
|
|
|
|
archive.mountpoint = 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (append) {
|
|
|
|
arr_push(&state.archives, archive);
|
|
|
|
} else {
|
|
|
|
arr_expand(&state.archives, 1);
|
|
|
|
memmove(state.archives.data + 1, state.archives.data, sizeof(Archive) * state.archives.length);
|
|
|
|
state.archives.data[0] = archive;
|
|
|
|
state.archives.length++;
|
2017-08-01 19:23:33 +00:00
|
|
|
}
|
|
|
|
|
2019-05-20 10:51:22 +00:00
|
|
|
return true;
|
2019-11-25 01:27:17 +00:00
|
|
|
}
|
2017-03-11 09:37:00 +00:00
|
|
|
|
2019-11-25 01:27:17 +00:00
|
|
|
bool lovrFilesystemUnmount(const char* path) {
|
|
|
|
FOREACH_ARCHIVE(archive) {
|
|
|
|
if (!strcmp(strpool_resolve(&archive->strings, archive->path), path)) {
|
|
|
|
archive->close(archive);
|
|
|
|
arr_splice(&state.archives, archive - state.archives.data, 1);
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
}
|
2019-05-20 10:51:22 +00:00
|
|
|
return false;
|
2016-11-07 22:31:11 +00:00
|
|
|
}
|
|
|
|
|
2019-11-25 01:27:17 +00:00
|
|
|
static Archive* archiveStat(const char* path, FileInfo* info) {
|
|
|
|
if (valid(path)) {
|
|
|
|
FOREACH_ARCHIVE(archive) {
|
|
|
|
if (archive->stat(archive, path, info)) {
|
|
|
|
return archive;
|
|
|
|
}
|
|
|
|
}
|
2019-08-21 23:30:20 +00:00
|
|
|
}
|
2019-11-25 01:27:17 +00:00
|
|
|
return NULL;
|
|
|
|
}
|
|
|
|
|
|
|
|
const char* lovrFilesystemGetRealDirectory(const char* path) {
|
|
|
|
FileInfo info;
|
|
|
|
Archive* archive = archiveStat(path, &info);
|
|
|
|
return archive ? (archive->strings.data + archive->path) : NULL;
|
|
|
|
}
|
|
|
|
|
|
|
|
bool lovrFilesystemIsFile(const char* path) {
|
|
|
|
FileInfo info;
|
|
|
|
return archiveStat(path, &info) ? info.type == FILE_REGULAR : false;
|
|
|
|
}
|
|
|
|
|
|
|
|
bool lovrFilesystemIsDirectory(const char* path) {
|
|
|
|
FileInfo info;
|
|
|
|
return archiveStat(path, &info) ? info.type == FILE_DIRECTORY : false;
|
2019-08-21 23:30:20 +00:00
|
|
|
}
|
|
|
|
|
2019-11-25 01:27:17 +00:00
|
|
|
uint64_t lovrFilesystemGetSize(const char* path) {
|
|
|
|
FileInfo info;
|
|
|
|
return archiveStat(path, &info) ? info.size : ~0ull;
|
2016-11-01 00:14:31 +00:00
|
|
|
}
|
|
|
|
|
2019-11-25 01:27:17 +00:00
|
|
|
uint64_t lovrFilesystemGetLastModified(const char* path) {
|
|
|
|
FileInfo info;
|
|
|
|
return archiveStat(path, &info) ? info.lastModified : ~0ull;
|
2016-11-01 00:14:31 +00:00
|
|
|
}
|
|
|
|
|
2019-11-25 01:27:17 +00:00
|
|
|
void* lovrFilesystemRead(const char* path, size_t bytes, size_t* bytesRead) {
|
|
|
|
if (valid(path)) {
|
|
|
|
void* data;
|
|
|
|
FOREACH_ARCHIVE(archive) {
|
|
|
|
if (archive->read(archive, path, bytes, bytesRead, &data)) {
|
|
|
|
return data;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return NULL;
|
|
|
|
}
|
|
|
|
|
|
|
|
void lovrFilesystemGetDirectoryItems(const char* path, void (*callback)(void* context, const char* path), void* context) {
|
|
|
|
if (valid(path)) {
|
|
|
|
FOREACH_ARCHIVE(archive) {
|
|
|
|
archive->list(archive, path, callback, context);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Writing
|
|
|
|
|
2016-11-07 22:30:32 +00:00
|
|
|
const char* lovrFilesystemGetIdentity() {
|
2020-06-26 00:11:56 +00:00
|
|
|
return state.identity[0] == '\0' ? NULL : state.identity;
|
2016-11-07 22:30:32 +00:00
|
|
|
}
|
|
|
|
|
2019-11-25 01:27:17 +00:00
|
|
|
bool lovrFilesystemSetIdentity(const char* identity) {
|
|
|
|
size_t length = strlen(identity);
|
|
|
|
|
2020-06-26 00:11:56 +00:00
|
|
|
// Identity can only be set once, and can't be empty
|
|
|
|
if (state.identity[0] != '\0' || length == 0) {
|
2019-11-25 01:27:17 +00:00
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Initialize the save path to the data path
|
2020-06-25 19:28:40 +00:00
|
|
|
size_t cursor = lovrPlatformGetDataDirectory(state.savePath, sizeof(state.savePath));
|
2019-11-25 01:27:17 +00:00
|
|
|
|
|
|
|
// If the data path was too long or unavailable, fail
|
|
|
|
if (cursor == 0) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2020-06-26 00:11:56 +00:00
|
|
|
#ifdef __ANDROID__
|
|
|
|
// On Android the data path is the save path, and the identity is always the package id.
|
|
|
|
// The data path ends in /package.id/files, so to extract the identity the '/files' is temporarily
|
|
|
|
// chopped off and everything from the last slash is copied to the identity buffer
|
|
|
|
// FIXME brittle? could read package id from /proc/self/cmdline instead
|
|
|
|
state.savePath[cursor - 6] = '\0';
|
|
|
|
char* id = strrchr(state.savePath, '/') + 1;
|
|
|
|
length = strlen(id);
|
|
|
|
memcpy(state.identity, id, length);
|
|
|
|
state.identity[length] = '\0';
|
|
|
|
state.savePath[cursor - 6] = '/';
|
|
|
|
state.savePathLength = cursor;
|
|
|
|
#else
|
|
|
|
|
2019-11-25 01:27:17 +00:00
|
|
|
// Make sure there is enough room to tack on /LOVR/<identity>
|
|
|
|
if (cursor + strlen("/LOVR") + 1 + length >= sizeof(state.savePath)) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Append /LOVR, mkdir
|
|
|
|
memcpy(state.savePath + cursor, "/LOVR", strlen("/LOVR"));
|
|
|
|
cursor += strlen("/LOVR");
|
|
|
|
state.savePath[cursor] = '\0';
|
|
|
|
fs_mkdir(state.savePath);
|
|
|
|
|
|
|
|
// Append /<identity>, mkdir
|
|
|
|
state.savePath[cursor++] = '/';
|
|
|
|
memcpy(state.savePath + cursor, identity, length);
|
|
|
|
cursor += length;
|
|
|
|
state.savePath[cursor] = '\0';
|
|
|
|
state.savePathLength = cursor;
|
|
|
|
fs_mkdir(state.savePath);
|
|
|
|
|
2020-06-26 00:11:56 +00:00
|
|
|
// Set the identity string
|
|
|
|
memcpy(state.identity, identity, length + 1);
|
|
|
|
#endif
|
|
|
|
|
2019-11-25 01:27:17 +00:00
|
|
|
// Mount the fully resolved save path
|
|
|
|
if (!lovrFilesystemMount(state.savePath, NULL, false, NULL)) {
|
2020-06-26 00:11:56 +00:00
|
|
|
state.identity[0] = '\0';
|
2019-11-25 01:27:17 +00:00
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
return true;
|
2017-03-11 09:37:00 +00:00
|
|
|
}
|
|
|
|
|
2019-11-25 01:27:17 +00:00
|
|
|
const char* lovrFilesystemGetSaveDirectory() {
|
|
|
|
return state.savePath;
|
|
|
|
}
|
|
|
|
|
|
|
|
bool lovrFilesystemCreateDirectory(const char* path) {
|
|
|
|
char resolved[LOVR_PATH_MAX];
|
|
|
|
|
|
|
|
if (!valid(path) || !concat(resolved, state.savePath, state.savePathLength, path, strlen(path))) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
char* cursor = resolved + state.savePathLength;
|
|
|
|
while (*cursor == '/') cursor++;
|
|
|
|
|
|
|
|
while (*cursor) {
|
|
|
|
if (*cursor == '/') {
|
|
|
|
*cursor = '\0';
|
|
|
|
fs_mkdir(resolved);
|
|
|
|
*cursor = '/';
|
|
|
|
}
|
|
|
|
cursor++;
|
|
|
|
}
|
|
|
|
|
|
|
|
return fs_mkdir(resolved);
|
|
|
|
}
|
|
|
|
|
|
|
|
bool lovrFilesystemRemove(const char* path) {
|
|
|
|
char resolved[LOVR_PATH_MAX];
|
|
|
|
return valid(path) && concat(resolved, state.savePath, state.savePathLength, path, strlen(path)) && fs_remove(resolved);
|
|
|
|
}
|
|
|
|
|
|
|
|
size_t lovrFilesystemWrite(const char* path, const char* content, size_t size, bool append) {
|
|
|
|
char resolved[LOVR_PATH_MAX];
|
|
|
|
if (!valid(path) || !concat(resolved, state.savePath, state.savePathLength, path, strlen(path))) {
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
fs_handle file;
|
|
|
|
if (!fs_open(resolved, append ? OPEN_APPEND : OPEN_WRITE, &file)) {
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
fs_write(file, content, &size);
|
|
|
|
fs_close(file);
|
|
|
|
return size;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Paths
|
|
|
|
|
|
|
|
size_t lovrFilesystemGetAppdataDirectory(char* buffer, size_t size) {
|
2020-06-25 19:28:40 +00:00
|
|
|
return lovrPlatformGetDataDirectory(buffer, size);
|
2019-11-25 01:27:17 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
size_t lovrFilesystemGetExecutablePath(char* buffer, size_t size) {
|
2020-06-25 19:28:40 +00:00
|
|
|
return lovrPlatformGetExecutablePath(buffer, size);
|
2019-11-25 01:27:17 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
size_t lovrFilesystemGetUserDirectory(char* buffer, size_t size) {
|
2020-06-25 19:28:40 +00:00
|
|
|
return lovrPlatformGetHomeDirectory(buffer, size);
|
2019-11-25 01:27:17 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
size_t lovrFilesystemGetWorkingDirectory(char* buffer, size_t size) {
|
2020-06-25 19:28:40 +00:00
|
|
|
return lovrPlatformGetWorkingDirectory(buffer, size);
|
2017-03-11 09:37:00 +00:00
|
|
|
}
|
|
|
|
|
2019-06-08 10:45:03 +00:00
|
|
|
const char* lovrFilesystemGetRequirePath() {
|
|
|
|
return state.requirePath[0];
|
2018-07-07 04:21:07 +00:00
|
|
|
}
|
|
|
|
|
2019-06-08 10:45:03 +00:00
|
|
|
const char* lovrFilesystemGetCRequirePath() {
|
|
|
|
return state.requirePath[1];
|
2018-03-11 23:25:21 +00:00
|
|
|
}
|
|
|
|
|
2019-11-25 01:27:17 +00:00
|
|
|
void lovrFilesystemSetRequirePath(const char* requirePath) {
|
|
|
|
strncpy(state.requirePath[0], requirePath, sizeof(state.requirePath[0]) - 1);
|
2017-03-11 09:37:00 +00:00
|
|
|
}
|
|
|
|
|
2019-11-25 01:27:17 +00:00
|
|
|
void lovrFilesystemSetCRequirePath(const char* requirePath) {
|
|
|
|
strncpy(state.requirePath[1], requirePath, sizeof(state.requirePath[1]) - 1);
|
2016-11-07 22:30:32 +00:00
|
|
|
}
|
|
|
|
|
2019-11-25 01:27:17 +00:00
|
|
|
// Archive: dir
|
|
|
|
|
|
|
|
static bool dir_resolve(char* buffer, Archive* archive, const char* path) {
|
2020-01-20 22:12:37 +00:00
|
|
|
char innerBuffer[LOVR_PATH_MAX];
|
2019-11-25 01:27:17 +00:00
|
|
|
size_t length = strlen(path);
|
2020-02-17 22:42:13 +00:00
|
|
|
if (length >= sizeof(innerBuffer)) return false;
|
2020-01-20 22:12:37 +00:00
|
|
|
length = normalize(innerBuffer, path, length);
|
|
|
|
path = innerBuffer;
|
2019-11-25 01:27:17 +00:00
|
|
|
|
|
|
|
if (archive->mountpoint) {
|
|
|
|
if (strncmp(path, strpool_resolve(&archive->strings, archive->mountpoint), archive->mountpointLength)) {
|
|
|
|
return false;
|
|
|
|
} else {
|
|
|
|
path += archive->mountpointLength;
|
|
|
|
length -= archive->mountpointLength;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return concat(buffer, strpool_resolve(&archive->strings, archive->path), archive->pathLength, path, length);
|
2016-11-07 22:30:32 +00:00
|
|
|
}
|
|
|
|
|
2019-11-25 01:27:17 +00:00
|
|
|
static bool dir_stat(Archive* archive, const char* path, FileInfo* info) {
|
|
|
|
char resolved[LOVR_PATH_MAX];
|
|
|
|
return dir_resolve(resolved, archive, path) && fs_stat(resolved, info);
|
|
|
|
}
|
|
|
|
|
|
|
|
static void dir_list(Archive* archive, const char* path, fs_list_cb callback, void* context) {
|
|
|
|
char resolved[LOVR_PATH_MAX];
|
|
|
|
if (dir_resolve(resolved, archive, path)) {
|
|
|
|
fs_list(resolved, callback, context);
|
2017-10-22 08:37:06 +00:00
|
|
|
}
|
2016-11-01 00:14:31 +00:00
|
|
|
}
|
|
|
|
|
2019-11-25 01:27:17 +00:00
|
|
|
static bool dir_read(Archive* archive, const char* path, size_t bytes, size_t* bytesRead, void** data) {
|
|
|
|
char resolved[LOVR_PATH_MAX];
|
|
|
|
fs_handle file;
|
|
|
|
|
|
|
|
if (!dir_resolve(resolved, archive, path) || !fs_open(resolved, OPEN_READ, &file)) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
FileInfo info;
|
|
|
|
if (bytes == (size_t) -1) {
|
|
|
|
if (fs_stat(resolved, &info)) {
|
|
|
|
bytes = info.size;
|
|
|
|
} else {
|
|
|
|
fs_close(file);
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if ((*data = malloc(bytes)) == NULL) {
|
|
|
|
fs_close(file);
|
2019-05-20 10:51:22 +00:00
|
|
|
return true;
|
2018-03-19 23:52:17 +00:00
|
|
|
}
|
2019-11-25 01:27:17 +00:00
|
|
|
|
|
|
|
if (!fs_read(file, *data, &bytes)) {
|
|
|
|
fs_close(file);
|
|
|
|
free(*data);
|
|
|
|
*data = NULL;
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
fs_close(file);
|
|
|
|
*bytesRead = bytes;
|
|
|
|
return true;
|
2018-03-19 23:52:17 +00:00
|
|
|
}
|
|
|
|
|
2020-01-20 23:53:43 +00:00
|
|
|
static void dir_close(Archive* archive) {
|
2019-12-20 12:13:38 +00:00
|
|
|
arr_free(&archive->strings);
|
2016-11-01 01:35:00 +00:00
|
|
|
}
|
|
|
|
|
2019-11-25 01:27:17 +00:00
|
|
|
static bool dir_init(Archive* archive, const char* path, const char* mountpoint, const char* root) {
|
|
|
|
FileInfo info;
|
|
|
|
if (!fs_stat(path, &info) || info.type != FILE_DIRECTORY) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
archive->stat = dir_stat;
|
|
|
|
archive->list = dir_list;
|
|
|
|
archive->read = dir_read;
|
|
|
|
archive->close = dir_close;
|
|
|
|
return true;
|
2016-11-01 01:35:00 +00:00
|
|
|
}
|
|
|
|
|
2019-11-25 01:27:17 +00:00
|
|
|
// Archive: zip
|
|
|
|
|
|
|
|
static zip_node* zip_lookup(Archive* archive, const char* path) {
|
|
|
|
char buffer[LOVR_PATH_MAX];
|
|
|
|
size_t length = strlen(path);
|
2020-01-20 22:12:37 +00:00
|
|
|
if (length >= sizeof(buffer)) return NULL;
|
2019-11-25 01:27:17 +00:00
|
|
|
length = normalize(buffer, path, length);
|
|
|
|
uint64_t hash = hash64(buffer, length);
|
|
|
|
uint64_t index = map_get(&archive->lookup, hash);
|
|
|
|
return index == MAP_NIL ? NULL : &archive->nodes.data[index];
|
2017-03-11 09:37:00 +00:00
|
|
|
}
|
|
|
|
|
2019-11-25 01:27:17 +00:00
|
|
|
static bool zip_stat(Archive* archive, const char* path, FileInfo* info) {
|
2019-12-08 09:46:54 +00:00
|
|
|
zip_node* node = zip_lookup(archive, path);
|
2019-11-25 01:27:17 +00:00
|
|
|
if (!node) return false;
|
2019-12-08 09:46:54 +00:00
|
|
|
|
|
|
|
// zip stores timestamps in dos time, conversion is slow so we do it only on request
|
|
|
|
if (node->info.lastModified == ~0ull) {
|
|
|
|
uint16_t mdate = node->mdate;
|
|
|
|
uint16_t mtime = node->mtime;
|
|
|
|
struct tm t;
|
|
|
|
memset(&t, 0, sizeof(t));
|
|
|
|
t.tm_isdst = -1;
|
|
|
|
t.tm_year = ((mdate >> 9) & 127) + 80;
|
|
|
|
t.tm_mon = ((mdate >> 5) & 15) - 1;
|
|
|
|
t.tm_mday = mdate & 31;
|
|
|
|
t.tm_hour = (mtime >> 11) & 31;
|
|
|
|
t.tm_min = (mtime >> 5) & 63;
|
|
|
|
t.tm_sec = (mtime << 1) & 62;
|
|
|
|
node->info.lastModified = mktime(&t);
|
|
|
|
}
|
|
|
|
|
2019-11-25 01:27:17 +00:00
|
|
|
*info = node->info;
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
static void zip_list(Archive* archive, const char* path, fs_list_cb callback, void* context) {
|
|
|
|
const zip_node* node = zip_lookup(archive, path);
|
|
|
|
if (!node || node->info.type != FILE_DIRECTORY) return;
|
|
|
|
uint32_t i = node->firstChild;
|
|
|
|
while (i != ~0u) {
|
|
|
|
zip_node* child = &archive->nodes.data[i];
|
|
|
|
callback(context, strpool_resolve(&archive->strings, child->filename));
|
|
|
|
i = child->nextSibling;
|
2018-11-13 20:20:04 +00:00
|
|
|
}
|
2017-03-11 09:37:00 +00:00
|
|
|
}
|
|
|
|
|
2019-11-25 01:27:17 +00:00
|
|
|
static bool zip_read(Archive* archive, const char* path, size_t bytes, size_t* bytesRead, void** dst) {
|
|
|
|
const zip_node* node = zip_lookup(archive, path);
|
|
|
|
if (!node) return false;
|
|
|
|
|
|
|
|
// Directories can't be read (but still return true because the file was present in the archive)
|
|
|
|
if (node->info.type == FILE_DIRECTORY) {
|
|
|
|
*dst = NULL;
|
|
|
|
return true;
|
|
|
|
}
|
2016-11-02 03:27:15 +00:00
|
|
|
|
2019-11-25 01:27:17 +00:00
|
|
|
size_t dstSize = node->info.size;
|
2020-02-21 23:44:48 +00:00
|
|
|
size_t srcSize = node->csize;
|
2019-11-25 01:27:17 +00:00
|
|
|
bool compressed;
|
|
|
|
const void* src;
|
|
|
|
|
2020-02-21 23:44:48 +00:00
|
|
|
if ((src = zip_load(&archive->zip, node->offset, &compressed)) == NULL) {
|
2019-11-25 01:27:17 +00:00
|
|
|
*dst = NULL;
|
|
|
|
return true;
|
2017-10-22 03:23:29 +00:00
|
|
|
}
|
2017-10-22 00:35:50 +00:00
|
|
|
|
2019-11-25 01:27:17 +00:00
|
|
|
if ((*dst = malloc(dstSize)) == NULL) {
|
|
|
|
return true;
|
2016-11-05 22:55:01 +00:00
|
|
|
}
|
|
|
|
|
2019-12-08 09:46:54 +00:00
|
|
|
*bytesRead = (bytes == (size_t) -1 || bytes > dstSize) ? (uint32_t) dstSize : bytes;
|
2019-11-25 01:27:17 +00:00
|
|
|
|
|
|
|
if (compressed) {
|
2019-12-08 09:46:54 +00:00
|
|
|
if (stbi_zlib_decode_noheader_buffer(*dst, (int) dstSize, src, (int) srcSize) < 0) {
|
2019-11-25 01:27:17 +00:00
|
|
|
free(*dst);
|
|
|
|
*dst = NULL;
|
|
|
|
}
|
|
|
|
} else {
|
2019-12-08 09:46:54 +00:00
|
|
|
memcpy(*dst, src, *bytesRead);
|
2016-11-05 22:55:01 +00:00
|
|
|
}
|
|
|
|
|
2019-11-25 01:27:17 +00:00
|
|
|
return true;
|
2016-11-02 03:27:15 +00:00
|
|
|
}
|
|
|
|
|
2020-01-20 23:53:43 +00:00
|
|
|
static void zip_close(Archive* archive) {
|
2019-11-25 01:27:17 +00:00
|
|
|
arr_free(&archive->nodes);
|
|
|
|
map_free(&archive->lookup);
|
2019-12-20 12:13:38 +00:00
|
|
|
arr_free(&archive->strings);
|
2020-01-20 23:53:43 +00:00
|
|
|
if (archive->zip.data) fs_unmap(archive->zip.data, archive->zip.size);
|
2017-03-11 09:37:00 +00:00
|
|
|
}
|
|
|
|
|
2019-11-25 01:27:17 +00:00
|
|
|
static bool zip_init(Archive* archive, const char* filename, const char* mountpoint, const char* root) {
|
|
|
|
char path[LOVR_PATH_MAX];
|
|
|
|
memset(&archive->lookup, 0, sizeof(archive->lookup));
|
2019-12-08 09:46:54 +00:00
|
|
|
arr_init(&archive->nodes);
|
2016-11-07 22:30:32 +00:00
|
|
|
|
2019-11-25 01:27:17 +00:00
|
|
|
// mmap the zip file, try to parse it, and figure out how many files there are
|
|
|
|
archive->zip.data = fs_map(filename, &archive->zip.size);
|
|
|
|
if (!archive->zip.data || !zip_open(&archive->zip) || archive->zip.count > UINT32_MAX) {
|
|
|
|
zip_close(archive);
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Paste mountpoint into path, normalize, and add trailing slash. Paths are "pre hashed" with the
|
|
|
|
// mountpoint prepended (and the root stripped) to avoid doing those operations on every lookup.
|
|
|
|
size_t mountpointLength = 0;
|
|
|
|
if (mountpoint) {
|
|
|
|
mountpointLength = strlen(mountpoint);
|
|
|
|
if (mountpointLength + 1 >= sizeof(path)) {
|
|
|
|
zip_close(archive);
|
2019-05-20 10:51:22 +00:00
|
|
|
return false;
|
2017-03-11 09:37:00 +00:00
|
|
|
}
|
2016-11-07 22:30:32 +00:00
|
|
|
|
2019-11-25 01:27:17 +00:00
|
|
|
mountpointLength = normalize(path, mountpoint, mountpointLength);
|
|
|
|
path[mountpointLength++] = '/';
|
2017-11-03 02:01:31 +00:00
|
|
|
}
|
|
|
|
|
2019-11-25 01:27:17 +00:00
|
|
|
// Simple root normalization (only strips leading/trailing slashes, sorry)
|
|
|
|
while (root && root[0] == '/') root++;
|
|
|
|
size_t rootLength = root ? strlen(root) : 0;
|
|
|
|
while (root && root[rootLength - 1] == '/') rootLength--;
|
2017-03-11 09:58:11 +00:00
|
|
|
|
2019-11-25 01:27:17 +00:00
|
|
|
// Allocate
|
|
|
|
map_init(&archive->lookup, archive->zip.count);
|
|
|
|
arr_reserve(&archive->nodes, archive->zip.count);
|
2016-11-07 22:30:32 +00:00
|
|
|
|
2019-11-25 01:27:17 +00:00
|
|
|
zip_file info;
|
|
|
|
for (uint32_t i = 0; i < archive->zip.count; i++) {
|
|
|
|
if (!zip_next(&archive->zip, &info)) {
|
|
|
|
zip_close(archive);
|
|
|
|
return false;
|
|
|
|
}
|
2018-07-07 04:21:07 +00:00
|
|
|
|
2019-11-25 01:27:17 +00:00
|
|
|
// Node
|
|
|
|
zip_node node = {
|
|
|
|
.firstChild = ~0u,
|
|
|
|
.nextSibling = ~0u,
|
|
|
|
.filename = (size_t) -1,
|
|
|
|
.offset = info.offset,
|
2020-02-21 23:44:48 +00:00
|
|
|
.csize = info.csize,
|
2019-12-08 09:46:54 +00:00
|
|
|
.mdate = info.mdate,
|
|
|
|
.mtime = info.mtime,
|
2019-11-25 01:27:17 +00:00
|
|
|
.info.size = info.size,
|
2019-12-08 09:46:54 +00:00
|
|
|
.info.lastModified = ~0ull,
|
2019-11-25 01:27:17 +00:00
|
|
|
.info.type = FILE_REGULAR
|
|
|
|
};
|
|
|
|
|
|
|
|
// Filenames that end in slashes are directories
|
|
|
|
if (info.name[info.length - 1] == '/') {
|
|
|
|
node.info.type = FILE_DIRECTORY;
|
|
|
|
info.length--;
|
|
|
|
}
|
2018-07-07 04:21:07 +00:00
|
|
|
|
2019-11-25 01:27:17 +00:00
|
|
|
// Skip files if their names are too long
|
|
|
|
if (mountpointLength + info.length - rootLength >= sizeof(path)) {
|
|
|
|
continue;
|
|
|
|
}
|
2016-11-07 22:31:02 +00:00
|
|
|
|
2019-11-25 01:27:17 +00:00
|
|
|
// Skip files if they aren't under the root
|
|
|
|
if (root && (info.length < rootLength || memcmp(info.name, root, rootLength))) {
|
|
|
|
continue;
|
|
|
|
}
|
2016-11-07 22:31:02 +00:00
|
|
|
|
2019-11-25 01:27:17 +00:00
|
|
|
// Strip off the root from the filename and paste it after the mountpoint to get the "canonical" path
|
|
|
|
size_t length = normalize(path + mountpointLength, info.name + rootLength, info.length - rootLength) + mountpointLength;
|
|
|
|
size_t slash = length;
|
|
|
|
|
|
|
|
// Keep chopping off path segments, building up a tree of paths
|
|
|
|
// We can stop early if we reach a path that has already been indexed
|
|
|
|
// Also add individual path segments to the string pool, for zip_list
|
|
|
|
while (length != SIZE_MAX) {
|
|
|
|
uint64_t hash = hash64(path, length);
|
|
|
|
uint64_t index = map_get(&archive->lookup, hash);
|
|
|
|
|
|
|
|
if (index == MAP_NIL) {
|
|
|
|
index = archive->nodes.length;
|
|
|
|
map_set(&archive->lookup, hash, index);
|
|
|
|
arr_push(&archive->nodes, node);
|
|
|
|
node.firstChild = index;
|
|
|
|
node.info.type = FILE_DIRECTORY;
|
|
|
|
} else {
|
|
|
|
uint32_t childIndex = node.firstChild;
|
|
|
|
zip_node* parent = &archive->nodes.data[index];
|
|
|
|
zip_node* child = &archive->nodes.data[childIndex];
|
|
|
|
child->nextSibling = parent->firstChild;
|
|
|
|
parent->firstChild = childIndex;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
|
|
|
while (length && path[length - 1] != '/') {
|
|
|
|
length--;
|
|
|
|
}
|
|
|
|
|
|
|
|
archive->nodes.data[index].filename = strpool_append(&archive->strings, path + length, slash - length);
|
|
|
|
slash = --length;
|
|
|
|
}
|
2018-10-29 02:46:31 +00:00
|
|
|
}
|
|
|
|
|
2019-11-25 01:27:17 +00:00
|
|
|
archive->stat = zip_stat;
|
|
|
|
archive->list = zip_list;
|
|
|
|
archive->read = zip_read;
|
|
|
|
archive->close = zip_close;
|
|
|
|
return true;
|
2016-11-07 22:31:02 +00:00
|
|
|
}
|