singe/src/vfs.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;
}