/* * * Singe 3 * Copyright (C) 2006-2026 Scott Duensing * * 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 #include #include #include #include #include #include #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; }