1545 lines
47 KiB
C
1545 lines
47 KiB
C
/*
|
|
*
|
|
* Singe 3
|
|
* Copyright (C) 2006-2026 Scott Duensing <scott@kangaroopunch.com>
|
|
*
|
|
* This program is free software; you can redistribute it and/or
|
|
* modify it under the terms of the GNU General Public License
|
|
* as published by the Free Software Foundation; either version 3
|
|
* of the License, or (at your option) any later version.
|
|
*
|
|
* This program is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License
|
|
* along with this program; if not, write to the Free Software
|
|
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
|
|
* 02110-1301, USA.
|
|
*
|
|
*/
|
|
|
|
// Singe virtual filesystem. See vfs.h for the lookup rules.
|
|
//
|
|
// Database layout (format version 1):
|
|
// meta(key TEXT PRIMARY KEY, value TEXT) version, gamedir, chunk, packer
|
|
// assets(path TEXT PRIMARY KEY, name TEXT, size INTEGER, data BLOB)
|
|
// chunks(path TEXT, chunk INTEGER, data BLOB, PRIMARY KEY(path, chunk))
|
|
// path is the normalised key (lower case, forward slashes); name keeps the
|
|
// author's spelling for unpacking. Files larger than meta.chunk live in
|
|
// chunks with assets.data NULL.
|
|
//
|
|
// Nothing read from a database is trusted: sizes are checked against the
|
|
// bytes actually stored, meta.chunk must be sane, and names may not escape.
|
|
|
|
#include <ctype.h>
|
|
#include <dirent.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <strings.h>
|
|
#include <sys/stat.h>
|
|
|
|
#include "sqlite3.h"
|
|
|
|
#include "util.h"
|
|
#include "vfs.h"
|
|
|
|
|
|
#ifdef _WIN32
|
|
#define fileSeek _fseeki64
|
|
#define fileTell _ftelli64
|
|
#else
|
|
#define fileSeek fseeko
|
|
#define fileTell ftello
|
|
#endif
|
|
|
|
#define OVERLAY_DIRECTORY "files"
|
|
#define LIST_INITIAL_CAPACITY 64
|
|
#define MAIN_SCHEMA "main"
|
|
|
|
|
|
typedef struct DatabaseS {
|
|
char *path; // Database file as named by the caller
|
|
char *loose; // Sibling directory holding loose overrides, trailing separator
|
|
char *overlay; // Copy-on-write directory under the data directory, trailing separator
|
|
char *gameDir; // Lower case own-directory prefix the packer recorded, or NULL
|
|
int64_t chunkBytes;
|
|
sqlite3 *db;
|
|
sqlite3_stmt *assetStmt; // size, data by path
|
|
sqlite3_stmt *chunkStmt; // data by path and chunk number
|
|
sqlite3_stmt *listStmt; // paths under a prefix, for directory listings
|
|
// Held around every use of the three statements. A video's demuxer reads on its decoder thread
|
|
// while its sound is read on the main thread, and one statement shared between them, reset and
|
|
// rebound under each other's feet, handed each thread pieces of the other's chunks -- audio that
|
|
// decoded as a stream of garbage. SQLite's own mutex covers each call, not a sequence of them.
|
|
SDL_Mutex *lock;
|
|
struct DatabaseS *next;
|
|
} DatabaseT;
|
|
|
|
// One name of a directory listing being gathered. The filesystem is listed before the database,
|
|
// so on a case-insensitive tie the lower insertion order is the loose spelling and it wins.
|
|
typedef struct ListEntryS {
|
|
char *name;
|
|
int32_t order;
|
|
} ListEntryT;
|
|
|
|
// A directory listing being gathered: names are appended freely and sorted and de-duplicated once.
|
|
typedef struct ListS {
|
|
ListEntryT *entries;
|
|
int32_t count;
|
|
int32_t capacity;
|
|
} ListT;
|
|
|
|
// Where a name lands. A NULL database means the plain filesystem path.
|
|
typedef struct TargetS {
|
|
DatabaseT *db;
|
|
char *path; // Filesystem path, or the loose override candidate for a packed name
|
|
char *key; // Normalised key inside the database
|
|
char *overlay; // Overlay path for the key
|
|
} TargetT;
|
|
|
|
struct VfsStreamS {
|
|
FILE *file; // Filesystem-backed stream, or NULL
|
|
DatabaseT *db;
|
|
char *key;
|
|
int64_t size;
|
|
int64_t position;
|
|
int64_t chunkIndex; // Chunk held in buffer, -1 for none
|
|
int64_t chunkLength;
|
|
uint8_t *buffer;
|
|
bool whole; // buffer holds the entire asset
|
|
};
|
|
|
|
|
|
static bool _assetDirectory(DatabaseT *db, const char *key);
|
|
static bool _assetExists(DatabaseT *db, const char *key);
|
|
static bool _assetSize(DatabaseT *db, const char *key, int64_t *size);
|
|
static char *_bindListRange(DatabaseT *db, const char *key);
|
|
static bool _climbsAbove(const char *path);
|
|
static void _databaseClose(DatabaseT *db);
|
|
static DatabaseT *_databaseOpen(const char *path);
|
|
static void _databasesClose(void);
|
|
static size_t _engineNameLength(const char *name);
|
|
static bool _fileModified(const char *path, int64_t *size, int64_t *modified);
|
|
static bool _hasDatabaseExtension(const char *path);
|
|
static bool _hasParentComponent(const char *norm);
|
|
static bool _isAbsolute(const char *name);
|
|
static bool _isEngineName(const char *name);
|
|
static void _listAdd(ListT *list, const char *name);
|
|
static int _listCompare(const void *a, const void *b); // qsort callback. Not changing int.
|
|
static void _listDirectory(const char *path, ListT *list);
|
|
static char **_listFinish(ListT *list, int32_t *count);
|
|
static char *_normalise(const char *name);
|
|
static char *_overlayFor(const char *dataDirBase, const char *dataDir, const char *databasePath, bool isContainer);
|
|
static uint8_t *_readAsset(DatabaseT *db, const char *key, size_t *bytes, bool sdlMemory);
|
|
static bool _readChunk(DatabaseT *db, const char *key, int64_t index, uint8_t *buffer, int64_t capacity, int64_t *length);
|
|
static bool _resolve(const char *name, TargetT *target);
|
|
static int _sqliteAccess(sqlite3_vfs *vfs, const char *name, int flags, int *result);
|
|
static int _sqliteCheckReservedLock(sqlite3_file *file, int *result);
|
|
static int _sqliteClose(sqlite3_file *file);
|
|
static int _sqliteCurrentTime(sqlite3_vfs *vfs, double *now);
|
|
static int _sqliteDeviceCharacteristics(sqlite3_file *file);
|
|
static void _sqliteDlClose(sqlite3_vfs *vfs, void *handle);
|
|
static void _sqliteDlError(sqlite3_vfs *vfs, int bytes, char *out);
|
|
static void *_sqliteDlOpen(sqlite3_vfs *vfs, const char *name);
|
|
static void (*_sqliteDlSym(sqlite3_vfs *vfs, void *handle, const char *symbol))(void);
|
|
static int _sqliteFileControl(sqlite3_file *file, int op, void *arg);
|
|
static int _sqliteFileSize(sqlite3_file *file, sqlite3_int64 *size);
|
|
static int _sqliteFullPathname(sqlite3_vfs *vfs, const char *name, int bytes, char *out);
|
|
static int _sqliteGetLastError(sqlite3_vfs *vfs, int bytes, char *out);
|
|
static int _sqliteLock(sqlite3_file *file, int level);
|
|
static int _sqliteOpen(sqlite3_vfs *vfs, sqlite3_filename name, sqlite3_file *file, int flags, int *outFlags);
|
|
static int _sqliteRandomness(sqlite3_vfs *vfs, int bytes, char *out);
|
|
static int _sqliteRead(sqlite3_file *file, void *buffer, int amount, sqlite3_int64 offset);
|
|
static int _sqliteSectorSize(sqlite3_file *file);
|
|
static int _sqliteSleep(sqlite3_vfs *vfs, int microseconds);
|
|
static int _sqliteSync(sqlite3_file *file, int flags);
|
|
static int _sqliteTruncate(sqlite3_file *file, sqlite3_int64 size);
|
|
static int _sqliteUnlock(sqlite3_file *file, int level);
|
|
static int _sqliteWrite(sqlite3_file *file, const void *buffer, int amount, sqlite3_int64 offset);
|
|
static void _targetFree(TargetT *target);
|
|
static char *_uriEncode(const char *text);
|
|
|
|
|
|
// A database packed inside the game, opened by a script through sqlite3.open, is read where it lies
|
|
// through this read-only SQLite VFS: each file is a VfsStreamT over the packed asset. Registered
|
|
// the first time a script asks (vfsSqliteUri); what it does not do itself goes to the default VFS.
|
|
#define VFS_SQLITE_NAME "singePacked"
|
|
#define VFS_SQLITE_SECTOR_BYTES 4096
|
|
#define VFS_EXTRACT_BYTES (1024 * 1024)
|
|
|
|
typedef struct PackedFileS {
|
|
sqlite3_file base; // SQLite's part, first, as a VFS file must begin
|
|
VfsStreamT *stream;
|
|
} PackedFileT;
|
|
|
|
static const sqlite3_io_methods _sqliteFileMethods = {
|
|
1,
|
|
_sqliteClose,
|
|
_sqliteRead,
|
|
_sqliteWrite,
|
|
_sqliteTruncate,
|
|
_sqliteSync,
|
|
_sqliteFileSize,
|
|
_sqliteLock,
|
|
_sqliteUnlock,
|
|
_sqliteCheckReservedLock,
|
|
_sqliteFileControl,
|
|
_sqliteSectorSize,
|
|
_sqliteDeviceCharacteristics,
|
|
NULL, NULL, NULL, NULL, NULL, NULL
|
|
};
|
|
|
|
static sqlite3_vfs _sqliteVfs;
|
|
static sqlite3_vfs *_sqliteDefault = NULL;
|
|
|
|
static DatabaseT *_databases = NULL;
|
|
static DatabaseT *_container = NULL;
|
|
static char *_dataDirBase = NULL;
|
|
static char *_dataDir = NULL;
|
|
static char *_dataDirKey = NULL; // _dataDirBase normalised without trailing slashes, for _isEngineName
|
|
|
|
|
|
// True when any asset lives below the key, which is what a directory is in a database.
|
|
static bool _assetDirectory(DatabaseT *db, const char *key) {
|
|
char *from = NULL;
|
|
bool found = false;
|
|
|
|
SDL_LockMutex(db->lock);
|
|
from = _bindListRange(db, key);
|
|
found = (sqlite3_step(db->listStmt) == SQLITE_ROW);
|
|
sqlite3_reset(db->listStmt);
|
|
SDL_UnlockMutex(db->lock);
|
|
free(from);
|
|
|
|
return found;
|
|
}
|
|
|
|
|
|
static bool _assetExists(DatabaseT *db, const char *key) {
|
|
int64_t size = 0;
|
|
|
|
return _assetSize(db, key, &size);
|
|
}
|
|
|
|
|
|
static bool _assetSize(DatabaseT *db, const char *key, int64_t *size) {
|
|
bool found = false;
|
|
|
|
SDL_LockMutex(db->lock);
|
|
sqlite3_reset(db->assetStmt);
|
|
sqlite3_bind_text(db->assetStmt, 1, key, -1, SQLITE_STATIC);
|
|
if (sqlite3_step(db->assetStmt) == SQLITE_ROW) {
|
|
*size = sqlite3_column_int64(db->assetStmt, 0);
|
|
found = true;
|
|
}
|
|
sqlite3_reset(db->assetStmt);
|
|
SDL_UnlockMutex(db->lock);
|
|
|
|
return found;
|
|
}
|
|
|
|
|
|
// Binds the listing statement to everything below a directory key: paths sort between "key/" and
|
|
// "key0" ('0' follows '/'), and the root (an empty key) has no bounds at all. Returns the lower
|
|
// bound, which is also the prefix every listed path carries; the caller frees it.
|
|
static char *_bindListRange(DatabaseT *db, const char *key) {
|
|
char *from = (key[0] == 0) ? strdup("") : utilCreateString("%s/", key);
|
|
char *to = (key[0] == 0) ? strdup("") : utilCreateString("%s0", key);
|
|
|
|
sqlite3_reset(db->listStmt);
|
|
sqlite3_bind_text(db->listStmt, 1, from, -1, SQLITE_TRANSIENT);
|
|
sqlite3_bind_text(db->listStmt, 2, to, -1, SQLITE_TRANSIENT);
|
|
free(to);
|
|
|
|
return from;
|
|
}
|
|
|
|
|
|
// True when the ".." components of a normalised path climb above where it starts: "a/../b" stays
|
|
// put, "a/../../b" and "../b" do not.
|
|
static bool _climbsAbove(const char *path) {
|
|
const char *p = path;
|
|
int32_t depth = 0;
|
|
|
|
while (*p != 0) {
|
|
if (*p == '/') {
|
|
p++;
|
|
continue;
|
|
}
|
|
if ((p[0] == '.') && (p[1] == '.') && ((p[2] == '/') || (p[2] == 0))) {
|
|
if (depth == 0) {
|
|
return true;
|
|
}
|
|
depth--;
|
|
} else {
|
|
depth++;
|
|
}
|
|
while ((*p != 0) && (*p != '/')) {
|
|
p++;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
|
|
// Releases a database that is not (or no longer) in the open list.
|
|
static void _databaseClose(DatabaseT *db) {
|
|
sqlite3_finalize(db->assetStmt);
|
|
sqlite3_finalize(db->chunkStmt);
|
|
sqlite3_finalize(db->listStmt);
|
|
sqlite3_close(db->db);
|
|
SDL_DestroyMutex(db->lock);
|
|
free(db->path);
|
|
free(db->loose);
|
|
free(db->overlay);
|
|
free(db->gameDir);
|
|
free(db);
|
|
}
|
|
|
|
|
|
// Opens a game database read only, or returns the cached handle. NULL when the file is not one of
|
|
// ours or cannot be trusted; a newer format than this Singe understands is fatal.
|
|
static DatabaseT *_databaseOpen(const char *path) {
|
|
DatabaseT *db = NULL;
|
|
char *uri = NULL;
|
|
char *value = NULL;
|
|
char *encoded = NULL;
|
|
char *stem = NULL;
|
|
int64_t version = 0;
|
|
|
|
for (db = _databases; db != NULL; db = db->next) {
|
|
if (strcmp(db->path, path) == 0) {
|
|
return db;
|
|
}
|
|
}
|
|
if (!_hasDatabaseExtension(path)) {
|
|
return NULL;
|
|
}
|
|
|
|
encoded = _uriEncode(path);
|
|
uri = utilCreateString("file:%s?immutable=1", encoded);
|
|
free(encoded);
|
|
|
|
db = (DatabaseT *)calloc(1, sizeof(DatabaseT));
|
|
if (db == NULL) {
|
|
utilDie("Out of memory opening %s.", path);
|
|
}
|
|
db->lock = SDL_CreateMutex();
|
|
if (db->lock == NULL) {
|
|
utilDie("%s", SDL_GetError());
|
|
}
|
|
db->path = strdup(path);
|
|
if (sqlite3_open_v2(uri, &db->db, SQLITE_OPEN_READONLY | SQLITE_OPEN_URI, NULL) != SQLITE_OK) {
|
|
free(uri);
|
|
_databaseClose(db);
|
|
return NULL;
|
|
}
|
|
free(uri);
|
|
value = vfsReadMeta(db->db, MAIN_SCHEMA, VFS_META_VERSION);
|
|
if (value == NULL) {
|
|
_databaseClose(db);
|
|
return NULL;
|
|
}
|
|
version = strtoll(value, NULL, 10);
|
|
free(value);
|
|
if (version > VFS_FORMAT_VERSION) {
|
|
utilDie("%s is a format %" PRId64 " game database; this Singe understands up to format %d.", path, version, VFS_FORMAT_VERSION);
|
|
}
|
|
if (sqlite3_prepare_v2(db->db, "SELECT size, data FROM assets WHERE path = ?", -1, &db->assetStmt, NULL) != SQLITE_OK) {
|
|
utilSay("Warning: %s has no assets table; ignoring it.", path);
|
|
_databaseClose(db);
|
|
return NULL;
|
|
}
|
|
if (sqlite3_prepare_v2(db->db, "SELECT data FROM chunks WHERE path = ? AND chunk = ?", -1, &db->chunkStmt, NULL) != SQLITE_OK) {
|
|
utilSay("Warning: %s has no chunks table; ignoring it.", path);
|
|
_databaseClose(db);
|
|
return NULL;
|
|
}
|
|
if (sqlite3_prepare_v2(db->db, "SELECT path FROM assets WHERE path >= ?1 AND (?2 = '' OR path < ?2) ORDER BY path", -1, &db->listStmt, NULL) != SQLITE_OK) {
|
|
utilSay("Warning: %s: %s; ignoring it.", path, sqlite3_errmsg(db->db));
|
|
_databaseClose(db);
|
|
return NULL;
|
|
}
|
|
db->chunkBytes = VFS_CHUNK_BYTES;
|
|
value = vfsReadMeta(db->db, MAIN_SCHEMA, VFS_META_CHUNK);
|
|
if (value != NULL) {
|
|
db->chunkBytes = strtoll(value, NULL, 10);
|
|
free(value);
|
|
}
|
|
if ((db->chunkBytes <= 0) || (db->chunkBytes > VFS_CHUNK_BYTES_MAX)) {
|
|
utilSay("Warning: %s has an unusable chunk size (%" PRId64 "); ignoring it.", path, db->chunkBytes);
|
|
_databaseClose(db);
|
|
return NULL;
|
|
}
|
|
value = vfsReadMeta(db->db, MAIN_SCHEMA, VFS_META_GAMEDIR);
|
|
if (value != NULL) {
|
|
db->gameDir = _normalise(value);
|
|
free(value);
|
|
}
|
|
|
|
// Loose overrides live in a directory named like the database without its extension.
|
|
stem = vfsDatabaseStem(path);
|
|
db->loose = utilCreateString("%s%c", stem, utilGetPathSeparator());
|
|
free(stem);
|
|
db->next = _databases;
|
|
_databases = db;
|
|
|
|
return db;
|
|
}
|
|
|
|
|
|
// Closes every open database. Streams and targets pointing into them must be gone by now.
|
|
static void _databasesClose(void) {
|
|
DatabaseT *db = NULL;
|
|
DatabaseT *next = NULL;
|
|
|
|
for (db = _databases; db != NULL; db = next) {
|
|
next = db->next;
|
|
_databaseClose(db);
|
|
}
|
|
_databases = NULL;
|
|
_container = NULL;
|
|
}
|
|
|
|
|
|
// The length of the engine-owned prefix of a name, or 0 when the engine does not own it. The
|
|
// engine owns Singe/ and the data directory, which resolve on the filesystem even inside a packed
|
|
// game. Only the leading component counts: "x/../Singe/y" is not an engine name.
|
|
static size_t _engineNameLength(const char *name) {
|
|
size_t length = strlen(VFS_ENGINE_DIRECTORY);
|
|
|
|
if ((strncasecmp(name, VFS_ENGINE_DIRECTORY, length) == 0) && ((name[length] == '/') || (name[length] == 0))) {
|
|
return length;
|
|
}
|
|
// The data directory base, when it is a real prefix: "data/" or an absolute path. With no
|
|
// --datadir the base is "./", which normalises to nothing and must not match everything.
|
|
if ((_dataDirKey != NULL) && (_dataDirKey[0] != 0)) {
|
|
length = strlen(_dataDirKey);
|
|
if ((strncmp(name, _dataDirKey, length) == 0) && ((name[length] == '/') || (name[length] == 0))) {
|
|
return length;
|
|
}
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
|
|
static bool _fileModified(const char *path, int64_t *size, int64_t *modified) {
|
|
struct stat info;
|
|
|
|
if (stat(path, &info) != 0) {
|
|
return false;
|
|
}
|
|
*size = (int64_t)info.st_size;
|
|
*modified = (int64_t)info.st_mtime;
|
|
|
|
return true;
|
|
}
|
|
|
|
|
|
// Ends in the database extension (any case) with something before it.
|
|
static bool _hasDatabaseExtension(const char *path) {
|
|
size_t length = strlen(path);
|
|
size_t extLen = strlen(VFS_DATABASE_EXTENSION);
|
|
|
|
return (length > extLen) && (utilStricmp(path + length - extLen, VFS_DATABASE_EXTENSION) == 0);
|
|
}
|
|
|
|
|
|
// True when any "/"-separated component of a normalised name is "..".
|
|
static bool _hasParentComponent(const char *norm) {
|
|
const char *p = norm;
|
|
|
|
while (*p != 0) {
|
|
if ((p[0] == '.') && (p[1] == '.') && ((p[2] == '/') || (p[2] == 0)) && ((p == norm) || (p[-1] == '/'))) {
|
|
return true;
|
|
}
|
|
p++;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
|
|
static bool _isAbsolute(const char *name) {
|
|
return (name[0] == '/') || (isalpha((unsigned char)name[0]) && (name[1] == ':'));
|
|
}
|
|
|
|
|
|
// Names the engine owns resolve on the filesystem even inside a packed game: Singe/ and the data directory.
|
|
static bool _isEngineName(const char *name) {
|
|
return _engineNameLength(name) > 0;
|
|
}
|
|
|
|
|
|
// Appends a name to a listing. Duplicates are dropped by _listFinish.
|
|
static void _listAdd(ListT *list, const char *name) {
|
|
ListEntryT *grown = NULL;
|
|
|
|
if (list->count == list->capacity) {
|
|
list->capacity = (list->capacity == 0) ? LIST_INITIAL_CAPACITY : list->capacity * 2;
|
|
grown = (ListEntryT *)realloc(list->entries, (size_t)list->capacity * sizeof(ListEntryT));
|
|
if (grown == NULL) {
|
|
utilDie("Out of memory listing a directory.");
|
|
}
|
|
list->entries = grown;
|
|
}
|
|
list->entries[list->count].name = strdup(name);
|
|
list->entries[list->count].order = list->count;
|
|
if (list->entries[list->count].name == NULL) {
|
|
utilDie("Out of memory listing a directory.");
|
|
}
|
|
list->count++;
|
|
}
|
|
|
|
|
|
// Case-insensitive by name, then by insertion order, so equal names sort deterministically.
|
|
static int _listCompare(const void *a, const void *b) {
|
|
const ListEntryT *x = (const ListEntryT *)a;
|
|
const ListEntryT *y = (const ListEntryT *)b;
|
|
int result = utilStricmp(x->name, y->name);
|
|
|
|
if (result == 0) {
|
|
result = (x->order > y->order) - (x->order < y->order);
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
|
|
// Adds the entries of a filesystem directory to a listing, if it exists.
|
|
static void _listDirectory(const char *path, ListT *list) {
|
|
DIR *dir = opendir(path);
|
|
struct dirent *de = NULL;
|
|
|
|
if (dir == NULL) {
|
|
return;
|
|
}
|
|
while ((de = readdir(dir)) != NULL) {
|
|
if ((strcmp(de->d_name, ".") != 0) && (strcmp(de->d_name, "..") != 0)) {
|
|
_listAdd(list, de->d_name);
|
|
}
|
|
}
|
|
closedir(dir);
|
|
}
|
|
|
|
|
|
// Sorts a listing and drops names that repeat (case-insensitively, like the keys), keeping the
|
|
// first added. NULL when empty.
|
|
static char **_listFinish(ListT *list, int32_t *count) {
|
|
char **names = NULL;
|
|
int32_t i = 0;
|
|
int32_t kept = 0;
|
|
|
|
*count = 0;
|
|
if (list->count == 0) {
|
|
return NULL;
|
|
}
|
|
qsort(list->entries, (size_t)list->count, sizeof(ListEntryT), _listCompare);
|
|
names = (char **)malloc((size_t)list->count * sizeof(char *));
|
|
if (names == NULL) {
|
|
utilDie("Out of memory listing a directory.");
|
|
}
|
|
for (i = 0; i < list->count; i++) {
|
|
if ((kept > 0) && (utilStricmp(names[kept - 1], list->entries[i].name) == 0)) {
|
|
free(list->entries[i].name);
|
|
} else {
|
|
names[kept++] = list->entries[i].name;
|
|
}
|
|
}
|
|
free(list->entries);
|
|
memset(list, 0, sizeof(*list));
|
|
*count = kept;
|
|
|
|
return names;
|
|
}
|
|
|
|
|
|
// Forward slashes, no leading "./", no doubled or trailing slashes. Case is untouched.
|
|
static char *_normalise(const char *name) {
|
|
char *out = strdup(name);
|
|
size_t i = 0;
|
|
size_t o = 0;
|
|
|
|
if (out == NULL) {
|
|
utilDie("Out of memory resolving %s.", name);
|
|
}
|
|
for (i = 0; out[i] != 0; i++) {
|
|
char c = (out[i] == '\\') ? '/' : out[i];
|
|
|
|
if ((c == '/') && (o > 0) && (out[o - 1] == '/')) {
|
|
continue;
|
|
}
|
|
if ((c == '.') && (out[i + 1] == '/' || out[i + 1] == '\\') && ((o == 0) || (out[o - 1] == '/'))) {
|
|
i++;
|
|
continue;
|
|
}
|
|
out[o++] = c;
|
|
}
|
|
// A trailing "." is the directory itself ("." and "Overlay/." are the root and "Overlay").
|
|
if ((o >= 1) && (out[o - 1] == '.') && ((o == 1) || (out[o - 2] == '/'))) {
|
|
o--;
|
|
}
|
|
// A trailing separator names the same thing ("Overlay/" is "Overlay"), except for the root.
|
|
while ((o > 1) && (out[o - 1] == '/')) {
|
|
o--;
|
|
}
|
|
if ((o == 1) && (out[0] == '/')) {
|
|
o = 0;
|
|
}
|
|
out[o] = 0;
|
|
|
|
return out;
|
|
}
|
|
|
|
|
|
// A game's own directory under the data directory, holding its overlay.
|
|
static char *_overlayFor(const char *dataDirBase, const char *dataDir, const char *databasePath, bool isContainer) {
|
|
char *stem = NULL;
|
|
char *result = NULL;
|
|
|
|
if (isContainer && (dataDir != NULL)) {
|
|
return utilCreateString("%s%s%c", dataDir, OVERLAY_DIRECTORY, utilGetPathSeparator());
|
|
}
|
|
stem = vfsDatabaseStem(utilGetLastPathComponent(databasePath));
|
|
result = utilCreateString("%s%s%c%s%c", dataDirBase ? dataDirBase : "", stem, utilGetPathSeparator(), OVERLAY_DIRECTORY, utilGetPathSeparator());
|
|
free(stem);
|
|
|
|
return result;
|
|
}
|
|
|
|
|
|
// Whole asset, from the row or reassembled from its chunks. The stored bytes must fit the size the
|
|
// row claims; a database that disagrees with itself is refused rather than overrun.
|
|
static uint8_t *_readAsset(DatabaseT *db, const char *key, size_t *bytes, bool sdlMemory) {
|
|
uint8_t *data = NULL;
|
|
int64_t size = 0;
|
|
int64_t offset = 0;
|
|
int64_t index = 0;
|
|
int64_t length = 0;
|
|
bool found = false;
|
|
|
|
// The row under the lock, and only the row: the chunks lock for themselves, and nothing may end
|
|
// the program while the lock is held, since dying waits for decoder threads that may want it.
|
|
*bytes = 0;
|
|
SDL_LockMutex(db->lock);
|
|
sqlite3_reset(db->assetStmt);
|
|
sqlite3_bind_text(db->assetStmt, 1, key, -1, SQLITE_STATIC);
|
|
found = sqlite3_step(db->assetStmt) == SQLITE_ROW;
|
|
if (found) {
|
|
size = sqlite3_column_int64(db->assetStmt, 0);
|
|
if ((size >= 0) && ((uint64_t)size < (uint64_t)SIZE_MAX)) {
|
|
data = sdlMemory ? (uint8_t *)SDL_malloc((size_t)size + 1) : (uint8_t *)malloc((size_t)size + 1);
|
|
}
|
|
if ((data != NULL) && (sqlite3_column_type(db->assetStmt, 1) != SQLITE_NULL)) {
|
|
offset = sqlite3_column_bytes(db->assetStmt, 1);
|
|
if (offset <= size) {
|
|
memcpy(data, sqlite3_column_blob(db->assetStmt, 1), (size_t)offset);
|
|
}
|
|
}
|
|
}
|
|
sqlite3_reset(db->assetStmt);
|
|
SDL_UnlockMutex(db->lock);
|
|
if (!found) {
|
|
return NULL;
|
|
}
|
|
if ((size < 0) || ((uint64_t)size >= (uint64_t)SIZE_MAX)) {
|
|
utilDie("%s in %s has an impossible size (%" PRId64 ").", key, db->path, size);
|
|
}
|
|
if (data == NULL) {
|
|
utilDie("Out of memory reading %s from %s.", key, db->path);
|
|
}
|
|
if (offset > size) {
|
|
utilDie("%s in %s holds more data than its size says.", key, db->path);
|
|
}
|
|
while (offset < size) {
|
|
if (!_readChunk(db, key, index, data + offset, size - offset, &length) || (length == 0)) {
|
|
utilDie("%s in %s is missing chunk %" PRId64 ".", key, db->path, index);
|
|
}
|
|
offset += length;
|
|
index++;
|
|
}
|
|
data[size] = 0;
|
|
*bytes = (size_t)size;
|
|
|
|
return data;
|
|
}
|
|
|
|
|
|
// One chunk into a buffer of the given capacity. False when there is no such chunk. Called from the
|
|
// decoder threads as well as this one, so the statement is only ever used under the lock.
|
|
static bool _readChunk(DatabaseT *db, const char *key, int64_t index, uint8_t *buffer, int64_t capacity, int64_t *length) {
|
|
bool found = false;
|
|
bool oversized = false;
|
|
|
|
*length = 0;
|
|
SDL_LockMutex(db->lock);
|
|
sqlite3_reset(db->chunkStmt);
|
|
sqlite3_bind_text(db->chunkStmt, 1, key, -1, SQLITE_STATIC);
|
|
sqlite3_bind_int64(db->chunkStmt, 2, index);
|
|
if (sqlite3_step(db->chunkStmt) == SQLITE_ROW) {
|
|
*length = sqlite3_column_bytes(db->chunkStmt, 0);
|
|
oversized = (*length > db->chunkBytes) || (*length > capacity);
|
|
if (!oversized) {
|
|
memcpy(buffer, sqlite3_column_blob(db->chunkStmt, 0), (size_t)*length);
|
|
found = true;
|
|
}
|
|
}
|
|
sqlite3_reset(db->chunkStmt);
|
|
SDL_UnlockMutex(db->lock);
|
|
if (oversized) {
|
|
utilDie("%s in %s has an oversized chunk %" PRId64 ".", key, db->path, index);
|
|
}
|
|
|
|
return found;
|
|
}
|
|
|
|
|
|
// Decides where a name lives. Returns false for a relative name inside a packed game that climbs
|
|
// out with "..": the packer refused it and so does the lookup.
|
|
static bool _resolve(const char *name, TargetT *target) {
|
|
char *norm = _normalise(name);
|
|
char *prefix = NULL;
|
|
const char *inner = NULL;
|
|
const char *relative = NULL;
|
|
char *p = NULL;
|
|
size_t i = 0;
|
|
size_t length = strlen(norm);
|
|
size_t extLen = strlen(VFS_DATABASE_EXTENSION);
|
|
size_t engineLength = 0;
|
|
|
|
memset(target, 0, sizeof(*target));
|
|
// "." (or "") is the current directory of a loose game and the root of a packed one.
|
|
if ((length == 0) && (_container == NULL)) {
|
|
target->path = strdup(".");
|
|
free(norm);
|
|
return true;
|
|
}
|
|
|
|
// A path that passes through a database file addresses its contents: "Games/MyGame.game/Overlay/x.png".
|
|
// Absolute paths included, so a tool can reach the packed games beside it from inside its own.
|
|
// The database file itself ("Games/MyGame.game", or with a trailing separator or ".") is its root.
|
|
for (i = extLen; i <= length; i++) {
|
|
if (((norm[i] == '/') || (norm[i] == 0)) && (strncasecmp(norm + i - extLen, VFS_DATABASE_EXTENSION, extLen) == 0)) {
|
|
prefix = utilStrndup(norm, i);
|
|
if (utilFileExists(prefix)) {
|
|
target->db = _databaseOpen(prefix);
|
|
if (target->db != NULL) {
|
|
inner = (norm[i] == 0) ? norm + i : norm + i + 1;
|
|
if (target->db->overlay == NULL) {
|
|
target->db->overlay = _overlayFor(_dataDirBase, _dataDir, prefix, false);
|
|
}
|
|
free(prefix);
|
|
break;
|
|
}
|
|
}
|
|
free(prefix);
|
|
prefix = NULL;
|
|
}
|
|
}
|
|
// The escape rule covers everything inside a database and every relative name of a packed game.
|
|
// A name the engine owns never reaches a database, so it may use ".." as long as it stays below
|
|
// the engine prefix ("data/game/script/../controls.cfg" is the documented override); climbing
|
|
// above the prefix is an escape. Ownership is decided by the leading component alone, so
|
|
// "x/../Singe/y" is not an engine name and is refused like any other ".." in a packed game.
|
|
if ((target->db != NULL) || ((_container != NULL) && !_isAbsolute(norm))) {
|
|
relative = (target->db != NULL) ? inner : norm;
|
|
engineLength = _engineNameLength(relative);
|
|
if ((engineLength > 0) ? _climbsAbove(relative + engineLength) : _hasParentComponent(relative)) {
|
|
memset(target, 0, sizeof(*target));
|
|
free(norm);
|
|
return false;
|
|
}
|
|
}
|
|
if ((target->db == NULL) && (_container != NULL) && !_isAbsolute(norm) && (engineLength == 0)) {
|
|
target->db = _container;
|
|
inner = norm;
|
|
}
|
|
|
|
// The engine's own files are never inside a game, however the name reached us.
|
|
if ((target->db != NULL) && _isEngineName(inner)) {
|
|
target->db = NULL;
|
|
target->path = strdup(inner);
|
|
free(norm);
|
|
return true;
|
|
}
|
|
if (target->db == NULL) {
|
|
target->path = strdup(name);
|
|
free(norm);
|
|
return true;
|
|
}
|
|
|
|
// Inside a database: drop the game's own directory when the name starts with it.
|
|
if ((target->db->gameDir != NULL) && (inner[0] != 0)) {
|
|
i = strlen(target->db->gameDir);
|
|
if ((utilStricmp(inner, target->db->gameDir) == 0) || ((strncasecmp(inner, target->db->gameDir, i) == 0) && (inner[i] == '/'))) {
|
|
inner += i;
|
|
while (*inner == '/') {
|
|
inner++;
|
|
}
|
|
}
|
|
}
|
|
target->key = strdup(inner);
|
|
for (p = target->key; *p != 0; p++) {
|
|
*p = (char)tolower((unsigned char)*p);
|
|
}
|
|
target->path = utilCreateString("%s%s", target->db->loose, inner);
|
|
target->overlay = utilCreateString("%s%s", target->db->overlay, target->key);
|
|
utilFixPathSeparators(&target->path, false);
|
|
utilFixPathSeparators(&target->overlay, false);
|
|
free(norm);
|
|
|
|
return true;
|
|
}
|
|
|
|
|
|
// The packed-database VFS (see _sqliteVfs). Nothing is ever written, so there is never a journal
|
|
// to look for.
|
|
static int _sqliteAccess(sqlite3_vfs *vfs, const char *name, int flags, int *result) {
|
|
(void)vfs;
|
|
(void)name;
|
|
(void)flags;
|
|
*result = 0;
|
|
return SQLITE_OK;
|
|
}
|
|
|
|
|
|
static int _sqliteCheckReservedLock(sqlite3_file *file, int *result) {
|
|
(void)file;
|
|
*result = 0;
|
|
return SQLITE_OK;
|
|
}
|
|
|
|
|
|
static int _sqliteClose(sqlite3_file *file) {
|
|
PackedFileT *packed = (PackedFileT *)file;
|
|
|
|
vfsStreamClose(packed->stream);
|
|
packed->stream = NULL;
|
|
return SQLITE_OK;
|
|
}
|
|
|
|
|
|
static int _sqliteCurrentTime(sqlite3_vfs *vfs, double *now) {
|
|
(void)vfs;
|
|
return _sqliteDefault->xCurrentTime(_sqliteDefault, now);
|
|
}
|
|
|
|
|
|
static int _sqliteDeviceCharacteristics(sqlite3_file *file) {
|
|
(void)file;
|
|
return SQLITE_IOCAP_IMMUTABLE;
|
|
}
|
|
|
|
|
|
// Loading an extension works as it would on any other database: the default VFS does it.
|
|
static void _sqliteDlClose(sqlite3_vfs *vfs, void *handle) {
|
|
(void)vfs;
|
|
_sqliteDefault->xDlClose(_sqliteDefault, handle);
|
|
}
|
|
|
|
|
|
static void _sqliteDlError(sqlite3_vfs *vfs, int bytes, char *out) {
|
|
(void)vfs;
|
|
_sqliteDefault->xDlError(_sqliteDefault, bytes, out);
|
|
}
|
|
|
|
|
|
static void *_sqliteDlOpen(sqlite3_vfs *vfs, const char *name) {
|
|
(void)vfs;
|
|
return _sqliteDefault->xDlOpen(_sqliteDefault, name);
|
|
}
|
|
|
|
|
|
static void (*_sqliteDlSym(sqlite3_vfs *vfs, void *handle, const char *symbol))(void) {
|
|
(void)vfs;
|
|
return _sqliteDefault->xDlSym(_sqliteDefault, handle, symbol);
|
|
}
|
|
|
|
|
|
static int _sqliteFileControl(sqlite3_file *file, int op, void *arg) {
|
|
(void)file;
|
|
(void)op;
|
|
(void)arg;
|
|
return SQLITE_NOTFOUND;
|
|
}
|
|
|
|
|
|
static int _sqliteFileSize(sqlite3_file *file, sqlite3_int64 *size) {
|
|
*size = vfsStreamSize(((PackedFileT *)file)->stream);
|
|
return SQLITE_OK;
|
|
}
|
|
|
|
|
|
// The name is the game's own name for the file, not a path on disk, and is kept as it was given.
|
|
static int _sqliteFullPathname(sqlite3_vfs *vfs, const char *name, int bytes, char *out) {
|
|
(void)vfs;
|
|
if ((int)strlen(name) >= bytes) {
|
|
return SQLITE_CANTOPEN;
|
|
}
|
|
strcpy(out, name);
|
|
return SQLITE_OK;
|
|
}
|
|
|
|
|
|
static int _sqliteGetLastError(sqlite3_vfs *vfs, int bytes, char *out) {
|
|
(void)vfs;
|
|
return _sqliteDefault->xGetLastError(_sqliteDefault, bytes, out);
|
|
}
|
|
|
|
|
|
static int _sqliteLock(sqlite3_file *file, int level) {
|
|
(void)file;
|
|
(void)level;
|
|
return SQLITE_OK;
|
|
}
|
|
|
|
|
|
// Only the main database is opened, and always read only: a packed file cannot be changed.
|
|
static int _sqliteOpen(sqlite3_vfs *vfs, sqlite3_filename name, sqlite3_file *file, int flags, int *outFlags) {
|
|
PackedFileT *packed = (PackedFileT *)file;
|
|
|
|
(void)vfs;
|
|
packed->base.pMethods = NULL;
|
|
if ((name == NULL) || ((flags & SQLITE_OPEN_MAIN_DB) == 0)) {
|
|
return SQLITE_CANTOPEN;
|
|
}
|
|
packed->stream = vfsStreamOpen(name);
|
|
if (packed->stream == NULL) {
|
|
return SQLITE_CANTOPEN;
|
|
}
|
|
packed->base.pMethods = &_sqliteFileMethods;
|
|
if (outFlags != NULL) {
|
|
*outFlags = SQLITE_OPEN_READONLY;
|
|
}
|
|
return SQLITE_OK;
|
|
}
|
|
|
|
|
|
static int _sqliteRandomness(sqlite3_vfs *vfs, int bytes, char *out) {
|
|
(void)vfs;
|
|
return _sqliteDefault->xRandomness(_sqliteDefault, bytes, out);
|
|
}
|
|
|
|
|
|
// A read that runs past the end comes back short and zero filled, which is how SQLite expects a
|
|
// file to answer it.
|
|
static int _sqliteRead(sqlite3_file *file, void *buffer, int amount, sqlite3_int64 offset) {
|
|
VfsStreamT *stream = ((PackedFileT *)file)->stream;
|
|
int64_t got = 0;
|
|
|
|
if (offset < vfsStreamSize(stream)) {
|
|
if (vfsStreamSeek(stream, offset, SEEK_SET) < 0) {
|
|
return SQLITE_IOERR_READ;
|
|
}
|
|
got = vfsStreamRead(stream, buffer, amount);
|
|
}
|
|
if (got < amount) {
|
|
memset((uint8_t *)buffer + got, 0, (size_t)(amount - got));
|
|
return SQLITE_IOERR_SHORT_READ;
|
|
}
|
|
return SQLITE_OK;
|
|
}
|
|
|
|
|
|
static int _sqliteSectorSize(sqlite3_file *file) {
|
|
(void)file;
|
|
return VFS_SQLITE_SECTOR_BYTES;
|
|
}
|
|
|
|
|
|
static int _sqliteSleep(sqlite3_vfs *vfs, int microseconds) {
|
|
(void)vfs;
|
|
return _sqliteDefault->xSleep(_sqliteDefault, microseconds);
|
|
}
|
|
|
|
|
|
static int _sqliteSync(sqlite3_file *file, int flags) {
|
|
(void)file;
|
|
(void)flags;
|
|
return SQLITE_OK;
|
|
}
|
|
|
|
|
|
static int _sqliteTruncate(sqlite3_file *file, sqlite3_int64 size) {
|
|
(void)file;
|
|
(void)size;
|
|
return SQLITE_READONLY;
|
|
}
|
|
|
|
|
|
static int _sqliteUnlock(sqlite3_file *file, int level) {
|
|
(void)file;
|
|
(void)level;
|
|
return SQLITE_OK;
|
|
}
|
|
|
|
|
|
static int _sqliteWrite(sqlite3_file *file, const void *buffer, int amount, sqlite3_int64 offset) {
|
|
(void)file;
|
|
(void)buffer;
|
|
(void)amount;
|
|
(void)offset;
|
|
return SQLITE_READONLY;
|
|
}
|
|
|
|
|
|
static void _targetFree(TargetT *target) {
|
|
free(target->path);
|
|
free(target->key);
|
|
free(target->overlay);
|
|
memset(target, 0, sizeof(*target));
|
|
}
|
|
|
|
|
|
// Percent-encodes the three characters a SQLite URI would misread. Free the result.
|
|
static char *_uriEncode(const char *text) {
|
|
char *encoded = NULL;
|
|
size_t i = 0;
|
|
size_t o = 0;
|
|
|
|
encoded = (char *)calloc(strlen(text) * 3 + 1, 1);
|
|
if (encoded == NULL) {
|
|
utilDie("Out of memory encoding %s.", text);
|
|
}
|
|
for (i = 0; text[i] != 0; i++) {
|
|
if ((text[i] == '?') || (text[i] == '#') || (text[i] == '%')) {
|
|
sprintf(encoded + o, "%%%02X", (unsigned char)text[i]);
|
|
o += 3;
|
|
} else {
|
|
encoded[o++] = text[i];
|
|
}
|
|
}
|
|
|
|
return encoded;
|
|
}
|
|
|
|
|
|
// The path without its database extension, as a new string; unchanged when it has none.
|
|
char *vfsDatabaseStem(const char *path) {
|
|
if (_hasDatabaseExtension(path)) {
|
|
return utilStrndup(path, strlen(path) - strlen(VFS_DATABASE_EXTENSION));
|
|
}
|
|
|
|
return strdup(path);
|
|
}
|
|
|
|
|
|
bool vfsExists(const char *name) {
|
|
TargetT target;
|
|
bool found = false;
|
|
|
|
if (!_resolve(name, &target)) {
|
|
return false;
|
|
}
|
|
found = utilFileExists(target.path);
|
|
if (!found && (target.db != NULL)) {
|
|
found = utilFileExists(target.overlay) || _assetExists(target.db, target.key);
|
|
}
|
|
_targetFree(&target);
|
|
|
|
return found;
|
|
}
|
|
|
|
|
|
// A filesystem path for the name, or NULL when the name resolves nowhere. Writes land in the loose
|
|
// directory or the overlay, never the database.
|
|
//
|
|
// There is no path to a packed asset: it is a row in a database and it stays there. Reading one
|
|
// used to unpack it beside the game and answer with the copy, which is what the engine did before
|
|
// anything could read a database row as a stream; everything can now -- vfsStreamOpen and
|
|
// vfsOpenIO, and the Lua file object built on the first of them -- so a packed name being read
|
|
// yields the loose candidate, which fails to open like any missing file. Ask for a stream instead.
|
|
// Copies a file the game names -- packed or not -- to path on disk, through a temporary name so an
|
|
// interrupted copy never leaves a partial file where path is. True when the whole file was written.
|
|
bool vfsExtract(const char *name, const char *path) {
|
|
VfsStreamT *stream = NULL;
|
|
FILE *out = NULL;
|
|
uint8_t *buffer = NULL;
|
|
char *temporary = NULL;
|
|
int64_t got = 0;
|
|
int64_t total = 0;
|
|
bool good = false;
|
|
|
|
stream = vfsStreamOpen(name);
|
|
if (stream == NULL) {
|
|
return false;
|
|
}
|
|
temporary = utilCreateString("%s.part", path);
|
|
buffer = (uint8_t *)malloc(VFS_EXTRACT_BYTES);
|
|
out = (buffer != NULL) ? fopen(temporary, "wb") : NULL;
|
|
if (out != NULL) {
|
|
good = true;
|
|
while (good && ((got = vfsStreamRead(stream, buffer, VFS_EXTRACT_BYTES)) > 0)) {
|
|
good = (fwrite(buffer, 1, (size_t)got, out) == (size_t)got);
|
|
total += got;
|
|
}
|
|
good = (fclose(out) == 0) && good && (total == vfsStreamSize(stream));
|
|
good = good && (remove(path) == 0 || !utilFileExists(path)) && (rename(temporary, path) == 0);
|
|
if (!good) {
|
|
remove(temporary);
|
|
}
|
|
}
|
|
free(buffer);
|
|
free(temporary);
|
|
vfsStreamClose(stream);
|
|
|
|
return good;
|
|
}
|
|
|
|
|
|
char *vfsFilePath(const char *name, bool forWriting) {
|
|
TargetT target;
|
|
char *path = NULL;
|
|
char *directory = NULL;
|
|
|
|
if (!_resolve(name, &target)) {
|
|
return NULL;
|
|
}
|
|
// The root of a packed game is a directory, never a file to open or a place to write.
|
|
if ((target.db != NULL) && (target.key[0] == 0)) {
|
|
_targetFree(&target);
|
|
return NULL;
|
|
}
|
|
if ((target.db == NULL) || utilFileExists(target.path)) {
|
|
path = target.path;
|
|
target.path = NULL;
|
|
} else if (forWriting || utilFileExists(target.overlay)) {
|
|
if (forWriting) {
|
|
directory = utilGetUpToLastPathComponent(target.overlay);
|
|
utilMkDirP(directory, 0755);
|
|
free(directory);
|
|
}
|
|
path = target.overlay;
|
|
target.overlay = NULL;
|
|
} else {
|
|
path = target.path;
|
|
target.path = NULL;
|
|
}
|
|
_targetFree(&target);
|
|
|
|
return path;
|
|
}
|
|
|
|
|
|
// container is the game's database, or NULL for a loose game. dataDir carries a trailing separator.
|
|
// May be called again with new settings; everything opened before is closed first.
|
|
void vfsInit(const char *container, const char *dataDirBase, const char *dataDir) {
|
|
size_t length = 0;
|
|
|
|
_databasesClose();
|
|
free(_dataDirBase);
|
|
free(_dataDir);
|
|
free(_dataDirKey);
|
|
_dataDirBase = dataDirBase ? strdup(dataDirBase) : NULL;
|
|
_dataDir = dataDir ? strdup(dataDir) : NULL;
|
|
_dataDirKey = NULL;
|
|
if (_dataDirBase != NULL) {
|
|
_dataDirKey = _normalise(_dataDirBase);
|
|
length = strlen(_dataDirKey);
|
|
while ((length > 0) && (_dataDirKey[length - 1] == '/')) {
|
|
length--;
|
|
}
|
|
_dataDirKey[length] = 0;
|
|
}
|
|
if (container != NULL) {
|
|
_container = _databaseOpen(container);
|
|
if (_container == NULL) {
|
|
utilDie("%s is not a Singe game database.", container);
|
|
}
|
|
free(_container->overlay);
|
|
_container->overlay = _overlayFor(_dataDirBase, _dataDir, container, true);
|
|
}
|
|
}
|
|
|
|
|
|
bool vfsIsDatabase(const char *path) {
|
|
if (!_hasDatabaseExtension(path) || !utilFileExists(path)) {
|
|
return false;
|
|
}
|
|
|
|
return _databaseOpen(path) != NULL;
|
|
}
|
|
|
|
|
|
// A directory, loose or packed: a packed one exists when any asset sits below the key.
|
|
bool vfsIsDirectory(const char *name) {
|
|
TargetT target;
|
|
struct stat info;
|
|
bool found = false;
|
|
|
|
if (!_resolve(name, &target)) {
|
|
return false;
|
|
}
|
|
found = (stat(target.path, &info) == 0) && S_ISDIR(info.st_mode);
|
|
if (!found && (target.db != NULL)) {
|
|
found = ((stat(target.overlay, &info) == 0) && S_ISDIR(info.st_mode)) || _assetDirectory(target.db, target.key);
|
|
}
|
|
_targetFree(&target);
|
|
|
|
return found;
|
|
}
|
|
|
|
|
|
// True when the name is a plain filesystem path, so a caller may use the filesystem directly.
|
|
// Inside a packed game a name that resolves nowhere is not one, so it never reaches the filesystem.
|
|
bool vfsIsFilesystem(const char *name) {
|
|
TargetT target;
|
|
bool plain = (_container == NULL);
|
|
|
|
if (_resolve(name, &target)) {
|
|
plain = (target.db == NULL);
|
|
_targetFree(&target);
|
|
}
|
|
|
|
return plain;
|
|
}
|
|
|
|
|
|
// Whether a name lives only inside a database: no loose file and no overlay copy shadows it. That
|
|
// is the case that has no filesystem path at all: vfsFilePath answers with a name that is not
|
|
// there, and a stream is the only way to read it.
|
|
bool vfsIsPacked(const char *name) {
|
|
TargetT target;
|
|
bool packed = false;
|
|
|
|
if (!_resolve(name, &target)) {
|
|
return false;
|
|
}
|
|
packed = (target.db != NULL) && (target.key[0] != 0) &&
|
|
!utilFileExists(target.path) && !utilFileExists(target.overlay) &&
|
|
_assetExists(target.db, target.key);
|
|
_targetFree(&target);
|
|
|
|
return packed;
|
|
}
|
|
|
|
|
|
// The entries directly under a directory name, from the loose directory, the overlay and the
|
|
// database together, each name once, sorted. Free with vfsListFree.
|
|
char **vfsList(const char *name, int32_t *count) {
|
|
TargetT target;
|
|
ListT list;
|
|
char *from = NULL;
|
|
char *entry = NULL;
|
|
const char *path = NULL;
|
|
const char *slash = NULL;
|
|
size_t prefix = 0;
|
|
|
|
*count = 0;
|
|
if (!_resolve(name, &target)) {
|
|
return NULL;
|
|
}
|
|
memset(&list, 0, sizeof(list));
|
|
_listDirectory(target.path, &list);
|
|
if (target.db != NULL) {
|
|
_listDirectory(target.overlay, &list);
|
|
SDL_LockMutex(target.db->lock);
|
|
from = _bindListRange(target.db, target.key);
|
|
prefix = strlen(from);
|
|
while (sqlite3_step(target.db->listStmt) == SQLITE_ROW) {
|
|
path = (const char *)sqlite3_column_text(target.db->listStmt, 0) + prefix;
|
|
slash = strchr(path, '/');
|
|
entry = slash ? utilStrndup(path, (size_t)(slash - path)) : strdup(path);
|
|
_listAdd(&list, entry);
|
|
free(entry);
|
|
}
|
|
sqlite3_reset(target.db->listStmt);
|
|
SDL_UnlockMutex(target.db->lock);
|
|
free(from);
|
|
}
|
|
_targetFree(&target);
|
|
|
|
return _listFinish(&list, count);
|
|
}
|
|
|
|
|
|
void vfsListFree(char **list, int32_t count) {
|
|
int32_t i = 0;
|
|
|
|
for (i = 0; i < count; i++) {
|
|
free(list[i]);
|
|
}
|
|
free(list);
|
|
}
|
|
|
|
|
|
// Read-only stream for the SDL loaders. Packed assets are served from memory.
|
|
SDL_IOStream *vfsOpenIO(const char *name) {
|
|
TargetT target;
|
|
SDL_IOStream *io = NULL;
|
|
uint8_t *data = NULL;
|
|
size_t bytes = 0;
|
|
|
|
if (!_resolve(name, &target)) {
|
|
SDL_SetError("%s resolves nowhere", name);
|
|
return NULL;
|
|
}
|
|
if ((target.db == NULL) || utilFileExists(target.path)) {
|
|
io = SDL_IOFromFile(target.path, "rb");
|
|
} else if (utilFileExists(target.overlay)) {
|
|
io = SDL_IOFromFile(target.overlay, "rb");
|
|
} else {
|
|
data = _readAsset(target.db, target.key, &bytes, true);
|
|
if (data != NULL) {
|
|
io = SDL_IOFromMem(data, bytes);
|
|
if (io == NULL) {
|
|
SDL_free(data);
|
|
} else {
|
|
SDL_SetPointerProperty(SDL_GetIOProperties(io), SDL_PROP_IOSTREAM_MEMORY_FREE_FUNC_POINTER, SDL_free);
|
|
}
|
|
} else {
|
|
SDL_SetError("%s not found", name);
|
|
}
|
|
}
|
|
_targetFree(&target);
|
|
|
|
return io;
|
|
}
|
|
|
|
|
|
void vfsQuit(void) {
|
|
_databasesClose();
|
|
free(_dataDirBase);
|
|
free(_dataDir);
|
|
free(_dataDirKey);
|
|
_dataDirBase = NULL;
|
|
_dataDir = NULL;
|
|
_dataDirKey = NULL;
|
|
}
|
|
|
|
|
|
// Whole file, NUL terminated, free() when done. NULL when the name resolves nowhere.
|
|
char *vfsRead(const char *name, size_t *bytes) {
|
|
TargetT target;
|
|
char *data = NULL;
|
|
|
|
*bytes = 0;
|
|
if (!_resolve(name, &target)) {
|
|
return NULL;
|
|
}
|
|
if ((target.db == NULL) || utilFileExists(target.path)) {
|
|
data = utilReadFile(target.path, bytes);
|
|
} else if (utilFileExists(target.overlay)) {
|
|
data = utilReadFile(target.overlay, bytes);
|
|
} else {
|
|
data = (char *)_readAsset(target.db, target.key, bytes, false);
|
|
}
|
|
_targetFree(&target);
|
|
|
|
return data;
|
|
}
|
|
|
|
|
|
// One value of a database's meta table as a new string, or NULL when the key is absent. schema is
|
|
// "main" or the name a database was attached as; it comes from code, never from a game.
|
|
char *vfsReadMeta(struct sqlite3 *db, const char *schema, const char *key) {
|
|
sqlite3_stmt *stmt = NULL;
|
|
char *sql = utilCreateString("SELECT value FROM %s.meta WHERE key = ?", schema);
|
|
char *value = NULL;
|
|
|
|
if (sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) == SQLITE_OK) {
|
|
sqlite3_bind_text(stmt, 1, key, -1, SQLITE_STATIC);
|
|
if ((sqlite3_step(stmt) == SQLITE_ROW) && (sqlite3_column_text(stmt, 0) != NULL)) {
|
|
value = strdup((const char *)sqlite3_column_text(stmt, 0));
|
|
}
|
|
}
|
|
sqlite3_finalize(stmt);
|
|
free(sql);
|
|
|
|
return value;
|
|
}
|
|
|
|
|
|
// Size and modification time, the latter being the database's own for a packed asset.
|
|
// The SQLite URI that opens a database packed inside the game where it lies, read only: give it to
|
|
// sqlite3_open_v2 with SQLITE_OPEN_READONLY | SQLITE_OPEN_URI. Registers the VFS that reads it the
|
|
// first time. Free the result.
|
|
char *vfsSqliteUri(const char *name) {
|
|
char *encoded = NULL;
|
|
char *uri = NULL;
|
|
|
|
if (_sqliteDefault == NULL) {
|
|
_sqliteDefault = sqlite3_vfs_find(NULL);
|
|
memset(&_sqliteVfs, 0, sizeof(_sqliteVfs));
|
|
_sqliteVfs.iVersion = 1;
|
|
_sqliteVfs.szOsFile = (int)sizeof(PackedFileT);
|
|
_sqliteVfs.mxPathname = _sqliteDefault->mxPathname;
|
|
_sqliteVfs.zName = VFS_SQLITE_NAME;
|
|
_sqliteVfs.xOpen = _sqliteOpen;
|
|
_sqliteVfs.xAccess = _sqliteAccess;
|
|
_sqliteVfs.xFullPathname = _sqliteFullPathname;
|
|
_sqliteVfs.xDlOpen = _sqliteDlOpen;
|
|
_sqliteVfs.xDlError = _sqliteDlError;
|
|
_sqliteVfs.xDlSym = _sqliteDlSym;
|
|
_sqliteVfs.xDlClose = _sqliteDlClose;
|
|
_sqliteVfs.xRandomness = _sqliteRandomness;
|
|
_sqliteVfs.xSleep = _sqliteSleep;
|
|
_sqliteVfs.xCurrentTime = _sqliteCurrentTime;
|
|
_sqliteVfs.xGetLastError = _sqliteGetLastError;
|
|
sqlite3_vfs_register(&_sqliteVfs, 0);
|
|
}
|
|
encoded = _uriEncode(name);
|
|
uri = utilCreateString("file:%s?vfs=%s&immutable=1", encoded, VFS_SQLITE_NAME);
|
|
free(encoded);
|
|
|
|
return uri;
|
|
}
|
|
|
|
|
|
bool vfsStat(const char *name, int64_t *size, int64_t *modified) {
|
|
TargetT target;
|
|
bool found = false;
|
|
int64_t ignore = 0;
|
|
|
|
if (!_resolve(name, &target)) {
|
|
return false;
|
|
}
|
|
found = _fileModified(target.path, size, modified);
|
|
if (!found && (target.db != NULL)) {
|
|
found = _fileModified(target.overlay, size, modified);
|
|
if (!found && _assetSize(target.db, target.key, size)) {
|
|
found = _fileModified(target.db->path, &ignore, modified);
|
|
}
|
|
}
|
|
_targetFree(&target);
|
|
|
|
return found;
|
|
}
|
|
|
|
|
|
void vfsStreamClose(VfsStreamT *stream) {
|
|
if (stream == NULL) {
|
|
return;
|
|
}
|
|
if (stream->file != NULL) {
|
|
fclose(stream->file);
|
|
}
|
|
free(stream->key);
|
|
free(stream->buffer);
|
|
free(stream);
|
|
}
|
|
|
|
|
|
// Seekable byte stream for the video demuxer. Chunked assets are read one chunk at a time.
|
|
VfsStreamT *vfsStreamOpen(const char *name) {
|
|
TargetT target;
|
|
VfsStreamT *stream = NULL;
|
|
const char *path = NULL;
|
|
size_t bytes = 0;
|
|
|
|
if (!_resolve(name, &target)) {
|
|
return NULL;
|
|
}
|
|
stream = (VfsStreamT *)calloc(1, sizeof(VfsStreamT));
|
|
if (stream == NULL) {
|
|
utilDie("Out of memory opening %s.", name);
|
|
}
|
|
stream->chunkIndex = -1;
|
|
if ((target.db == NULL) || utilFileExists(target.path)) {
|
|
path = target.path;
|
|
} else if (utilFileExists(target.overlay)) {
|
|
path = target.overlay;
|
|
}
|
|
if (path != NULL) {
|
|
stream->file = fopen(path, "rb");
|
|
if (stream->file == NULL) {
|
|
free(stream);
|
|
stream = NULL;
|
|
} else {
|
|
fileSeek(stream->file, 0, SEEK_END);
|
|
stream->size = (int64_t)fileTell(stream->file);
|
|
fileSeek(stream->file, 0, SEEK_SET);
|
|
}
|
|
} else if (_assetSize(target.db, target.key, &stream->size)) {
|
|
stream->db = target.db;
|
|
stream->key = strdup(target.key);
|
|
if (stream->size <= target.db->chunkBytes) {
|
|
// Small enough to hold whole; the row carries the data.
|
|
stream->buffer = _readAsset(target.db, target.key, &bytes, false);
|
|
stream->chunkIndex = 0;
|
|
stream->chunkLength = (int64_t)bytes;
|
|
stream->whole = true;
|
|
} else {
|
|
stream->buffer = (uint8_t *)malloc((size_t)target.db->chunkBytes);
|
|
if (stream->buffer == NULL) {
|
|
utilDie("Out of memory opening %s.", name);
|
|
}
|
|
}
|
|
} else {
|
|
free(stream);
|
|
stream = NULL;
|
|
}
|
|
_targetFree(&target);
|
|
|
|
return stream;
|
|
}
|
|
|
|
|
|
int64_t vfsStreamRead(VfsStreamT *stream, void *buffer, int64_t bytes) {
|
|
uint8_t *out = (uint8_t *)buffer;
|
|
int64_t total = 0;
|
|
int64_t index = 0;
|
|
int64_t start = 0;
|
|
int64_t take = 0;
|
|
|
|
if (stream->file != NULL) {
|
|
total = (int64_t)fread(buffer, 1, (size_t)bytes, stream->file);
|
|
stream->position += total;
|
|
return total;
|
|
}
|
|
if (bytes > stream->size - stream->position) {
|
|
bytes = stream->size - stream->position;
|
|
}
|
|
while (total < bytes) {
|
|
index = stream->whole ? 0 : stream->position / stream->db->chunkBytes;
|
|
start = stream->whole ? stream->position : stream->position % stream->db->chunkBytes;
|
|
if (index != stream->chunkIndex) {
|
|
if (!_readChunk(stream->db, stream->key, index, stream->buffer, stream->db->chunkBytes, &stream->chunkLength)) {
|
|
break;
|
|
}
|
|
stream->chunkIndex = index;
|
|
}
|
|
take = stream->chunkLength - start;
|
|
if (take > bytes - total) {
|
|
take = bytes - total;
|
|
}
|
|
if (take <= 0) {
|
|
break;
|
|
}
|
|
memcpy(out + total, stream->buffer + start, (size_t)take);
|
|
total += take;
|
|
stream->position += take;
|
|
}
|
|
|
|
return total;
|
|
}
|
|
|
|
|
|
int64_t vfsStreamSeek(VfsStreamT *stream, int64_t offset, int32_t whence) {
|
|
int64_t position = offset;
|
|
|
|
if (whence == SEEK_CUR) {
|
|
position = stream->position + offset;
|
|
} else if (whence == SEEK_END) {
|
|
position = stream->size + offset;
|
|
}
|
|
if ((position < 0) || (position > stream->size)) {
|
|
return -1;
|
|
}
|
|
if ((stream->file != NULL) && (fileSeek(stream->file, position, SEEK_SET) != 0)) {
|
|
return -1;
|
|
}
|
|
stream->position = position;
|
|
|
|
return position;
|
|
}
|
|
|
|
|
|
int64_t vfsStreamSize(VfsStreamT *stream) {
|
|
return stream->size;
|
|
}
|