Merge branch 'feature/scripting-storage'

This commit is contained in:
Vicki Pfau 2023-02-08 01:28:59 -08:00
commit 292ae8dcf8
9 changed files with 1135 additions and 0 deletions

View File

@ -55,6 +55,7 @@ if(NOT LIBMGBA_ONLY)
set(USE_SQLITE3 ON CACHE BOOL "Whether or not to enable SQLite3 support")
set(USE_ELF ON CACHE BOOL "Whether or not to enable ELF support")
set(USE_LUA ON CACHE BOOL "Whether or not to enable Lua scripting support")
set(USE_JSON_C ON CACHE BOOL "Whether or not to enable JSON-C support")
set(M_CORE_GBA ON CACHE BOOL "Build Game Boy Advance core")
set(M_CORE_GB ON CACHE BOOL "Build Game Boy core")
set(USE_LZMA ON CACHE BOOL "Whether or not to enable 7-Zip support")
@ -477,6 +478,7 @@ endif()
if(DISABLE_DEPS)
set(USE_GDB_STUB OFF)
set(USE_DISCORD_RPC OFF)
set(USE_JSON_C OFF)
set(USE_SQLITE3 OFF)
set(USE_PNG OFF)
set(USE_ZLIB OFF)
@ -765,11 +767,30 @@ endif()
if(ENABLE_SCRIPTING)
list(APPEND ENABLES SCRIPTING)
find_feature(USE_JSON_C "json-c")
if(NOT USE_LUA VERSION_LESS 5.1)
find_feature(USE_LUA "Lua" ${USE_LUA})
else()
find_feature(USE_LUA "Lua")
endif()
if(USE_JSON_C)
list(APPEND FEATURES JSON_C)
if(TARGET json-c::json-c)
list(APPEND DEPENDENCY_LIB json-c::json-c)
get_target_property(JSON_C_SONAME json-c::json-c IMPORTED_SONAME_NONE)
string(SUBSTRING "${JSON_C_SONAME}" 13 -1 JSON_C_SOVER)
else()
if(${json-c_VERSION} VERSION_LESS 0.13.0)
set(JSON_C_SOVER 3)
elseif(${json-c_VERSION} VERSION_LESS 0.15.0)
set(JSON_C_SOVER 4)
endif()
list(APPEND DEPENDENCY_LIB ${json-c_LIBRARIES})
include_directories(AFTER ${json-c_INCLUDE_DIRS})
link_directories(${json-c_LIBDIRS})
endif()
set(CPACK_DEBIAN_PACKAGE_DEPENDS "${CPACK_DEBIAN_PACKAGE_DEPENDS},libjson-c${JSON_C_SOVER}")
endif()
if(USE_LUA)
list(APPEND FEATURE_DEFINES USE_LUA)
include_directories(AFTER ${LUA_INCLUDE_DIR})
@ -1289,6 +1310,7 @@ if(NOT QUIET AND NOT LIBMGBA_ONLY)
else()
message(STATUS " Lua: ${USE_LUA}")
endif()
message(STATUS " storage API: ${USE_JSON_C}")
endif()
message(STATUS "Frontends:")
message(STATUS " Qt: ${BUILD_QT}")

View File

@ -0,0 +1,28 @@
/* Copyright (c) 2013-2023 Jeffrey Pfau
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
#ifndef M_SCRIPT_STORAGE_H
#define M_SCRIPT_STORAGE_H
#include <mgba-util/common.h>
CXX_GUARD_START
#include <mgba/script/context.h>
#include <mgba/script/macros.h>
struct VFile;
void mScriptContextAttachStorage(struct mScriptContext* context);
void mScriptStorageFlushAll(struct mScriptContext* context);
bool mScriptStorageSaveBucket(struct mScriptContext* context, const char* bucket);
bool mScriptStorageSaveBucketVF(struct mScriptContext* context, const char* bucket, struct VFile* vf);
bool mScriptStorageLoadBucket(struct mScriptContext* context, const char* bucket);
bool mScriptStorageLoadBucketVF(struct mScriptContext* context, const char* bucket, struct VFile* vf);
void mScriptStorageGetBucketPath(const char* bucket, char* out);
CXX_GUARD_END
#endif

View File

@ -79,6 +79,10 @@
#cmakedefine USE_GDB_STUB
#endif
#ifndef USE_JSON_C
#cmakedefine USE_JSON_C
#endif
#ifndef USE_LIBAV
#cmakedefine USE_LIBAV
#endif

View File

@ -20,6 +20,7 @@
#include "scripting/ScriptingTextBufferModel.h"
#include <mgba/script/input.h>
#include <mgba/script/storage.h>
#include <mgba-util/math.h>
#include <mgba-util/string.h>
@ -51,6 +52,9 @@ ScriptingController::ScriptingController(QObject* parent)
m_bufferModel = new ScriptingTextBufferModel(this);
QObject::connect(m_bufferModel, &ScriptingTextBufferModel::textBufferCreated, this, &ScriptingController::textBufferCreated);
connect(&m_storageFlush, &QTimer::timeout, this, &ScriptingController::flushStorage);
m_storageFlush.setInterval(5);
mScriptGamepadInit(&m_gamepad);
init();
@ -144,6 +148,10 @@ void ScriptingController::runCode(const QString& code) {
load(vf, "*prompt");
}
void ScriptingController::flushStorage() {
mScriptStorageFlushAll(&m_scriptContext);
}
bool ScriptingController::eventFilter(QObject* obj, QEvent* ev) {
event(obj, ev);
return false;
@ -293,6 +301,7 @@ void ScriptingController::detachGamepad() {
void ScriptingController::init() {
mScriptContextInit(&m_scriptContext);
mScriptContextAttachStdlib(&m_scriptContext);
mScriptContextAttachStorage(&m_scriptContext);
mScriptContextAttachSocket(&m_scriptContext);
mScriptContextAttachInput(&m_scriptContext);
mScriptContextRegisterEngines(&m_scriptContext);
@ -308,6 +317,8 @@ void ScriptingController::init() {
if (m_engines.count() == 1) {
m_activeEngine = *m_engines.begin();
}
m_storageFlush.start();
}
uint32_t ScriptingController::qtToScriptingKey(const QKeyEvent* event) {

View File

@ -7,6 +7,7 @@
#include <QHash>
#include <QObject>
#include <QTimer>
#include <mgba/script/context.h>
#include <mgba/script/input.h>
@ -55,6 +56,8 @@ public slots:
void reset();
void runCode(const QString& code);
void flushStorage();
protected:
bool eventFilter(QObject*, QEvent*) override;
@ -84,6 +87,8 @@ private:
std::shared_ptr<CoreController> m_controller;
InputController* m_inputController = nullptr;
QTimer m_storageFlush;
};
}

View File

@ -4,12 +4,17 @@ set(SOURCE_FILES
input.c
socket.c
stdlib.c
storage.c
types.c)
set(TEST_FILES
test/classes.c
test/types.c)
if(USE_JSON_C)
list(APPEND SOURCE_FILES storage.c)
endif()
if(USE_LUA)
list(APPEND SOURCE_FILES engines/lua.c)
list(APPEND TEST_FILES
@ -17,6 +22,10 @@ if(USE_LUA)
test/input.c
test/lua.c
test/stdlib.c)
if(USE_JSON_C)
list(APPEND TEST_FILES test/storage.c)
endif()
endif()
source_group("Scripting" FILES ${SOURCE_FILES})

View File

@ -9,6 +9,7 @@
#include <mgba/internal/script/types.h>
#include <mgba/script/context.h>
#include <mgba/script/input.h>
#include <mgba/script/storage.h>
#include <mgba-util/string.h>
struct mScriptContext context;
@ -469,6 +470,7 @@ int main(int argc, char* argv[]) {
mScriptContextInit(&context);
mScriptContextAttachStdlib(&context);
mScriptContextAttachSocket(&context);
mScriptContextAttachStorage(&context);
mScriptContextAttachInput(&context);
mScriptContextSetTextBufferFactory(&context, NULL, NULL);

500
src/script/storage.c Normal file
View File

@ -0,0 +1,500 @@
/* Copyright (c) 2013-2023 Jeffrey Pfau
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
#include <mgba/script/storage.h>
#include <mgba/core/config.h>
#include <mgba-util/vfs.h>
#include <json.h>
#include <sys/stat.h>
#define STORAGE_LEN_MAX 64
struct mScriptStorageBucket {
char* name;
struct mScriptValue* root;
bool dirty;
};
struct mScriptStorageContext {
struct Table buckets;
};
void mScriptStorageBucketDeinit(void*);
struct mScriptValue* mScriptStorageBucketGet(struct mScriptStorageBucket* bucket, const char* key);
static void mScriptStorageBucketSet(struct mScriptStorageBucket* bucket, const char* key, struct mScriptValue* value);
static void mScriptStorageBucketSetVoid(struct mScriptStorageBucket* bucket, const char* key, struct mScriptValue* value);
static void mScriptStorageBucketSetSInt(struct mScriptStorageBucket* bucket, const char* key, int64_t value);
static void mScriptStorageBucketSetUInt(struct mScriptStorageBucket* bucket, const char* key, uint64_t value);
static void mScriptStorageBucketSetFloat(struct mScriptStorageBucket* bucket, const char* key, double value);
static void mScriptStorageBucketSetBool(struct mScriptStorageBucket* bucket, const char* key, bool value);
static bool mScriptStorageBucketReload(struct mScriptStorageBucket* bucket);
static bool mScriptStorageBucketFlush(struct mScriptStorageBucket* bucket);
static void mScriptStorageContextDeinit(struct mScriptStorageContext*);
static void mScriptStorageContextFlushAll(struct mScriptStorageContext*);
struct mScriptStorageBucket* mScriptStorageGetBucket(struct mScriptStorageContext*, const char* name);
static bool mScriptStorageToJson(struct mScriptValue* value, struct json_object** out);
static struct mScriptValue* mScriptStorageFromJson(struct json_object* json);
mSCRIPT_DECLARE_STRUCT(mScriptStorageBucket);
mSCRIPT_DECLARE_STRUCT_METHOD(mScriptStorageBucket, WRAPPER, _get, mScriptStorageBucketGet, 1, CHARP, key);
mSCRIPT_DECLARE_STRUCT_VOID_METHOD(mScriptStorageBucket, setSInt, mScriptStorageBucketSetSInt, 2, CHARP, key, S64, value);
mSCRIPT_DECLARE_STRUCT_VOID_METHOD(mScriptStorageBucket, setUInt, mScriptStorageBucketSetUInt, 2, CHARP, key, U64, value);
mSCRIPT_DECLARE_STRUCT_VOID_METHOD(mScriptStorageBucket, setFloat, mScriptStorageBucketSetFloat, 2, CHARP, key, F64, value);
mSCRIPT_DECLARE_STRUCT_VOID_METHOD(mScriptStorageBucket, setBool, mScriptStorageBucketSetBool, 2, CHARP, key, BOOL, value);
mSCRIPT_DECLARE_STRUCT_VOID_METHOD(mScriptStorageBucket, setStr, mScriptStorageBucketSet, 2, CHARP, key, WSTR, value);
mSCRIPT_DECLARE_STRUCT_VOID_METHOD(mScriptStorageBucket, setList, mScriptStorageBucketSet, 2, CHARP, key, WLIST, value);
mSCRIPT_DECLARE_STRUCT_VOID_METHOD(mScriptStorageBucket, setTable, mScriptStorageBucketSet, 2, CHARP, key, WTABLE, value);
mSCRIPT_DECLARE_STRUCT_VOID_METHOD(mScriptStorageBucket, setVoid, mScriptStorageBucketSetVoid, 2, CHARP, key, NUL, value);
mSCRIPT_DECLARE_STRUCT_METHOD(mScriptStorageBucket, BOOL, reload, mScriptStorageBucketReload, 0);
mSCRIPT_DECLARE_STRUCT_METHOD(mScriptStorageBucket, BOOL, flush, mScriptStorageBucketFlush, 0);
mSCRIPT_DEFINE_STRUCT(mScriptStorageBucket)
mSCRIPT_DEFINE_CLASS_DOCSTRING(
"A single 'bucket' of stored data, appropriate for a single script to store its data. "
"Fields can be set directly on the bucket objct, e.g. if you want to store a value called "
"`foo` on a bucket named `bucket`, you can directly assign to it as `bucket.foo = value`, "
"and retrieve it in the same way later. Primitive types (numbers, strings, lists and tables) "
"can be stored in buckets, but complex data types (e.g. a bucket itself) cannot. Data "
"stored in a bucket is periodically flushed to disk and persists between sessions."
)
mSCRIPT_DEFINE_STRUCT_DEFAULT_SET(mScriptStorageBucket, setSInt)
mSCRIPT_DEFINE_STRUCT_DEFAULT_SET(mScriptStorageBucket, setUInt)
mSCRIPT_DEFINE_STRUCT_DEFAULT_SET(mScriptStorageBucket, setFloat)
mSCRIPT_DEFINE_STRUCT_DEFAULT_SET(mScriptStorageBucket, setBool)
mSCRIPT_DEFINE_STRUCT_DEFAULT_SET(mScriptStorageBucket, setStr)
mSCRIPT_DEFINE_STRUCT_DEFAULT_SET(mScriptStorageBucket, setList)
mSCRIPT_DEFINE_STRUCT_DEFAULT_SET(mScriptStorageBucket, setTable)
mSCRIPT_DEFINE_STRUCT_DEFAULT_SET(mScriptStorageBucket, setVoid)
mSCRIPT_DEFINE_STRUCT_DEFAULT_GET(mScriptStorageBucket)
mSCRIPT_DEFINE_DOCSTRING("Reload the state of the bucket from disk")
mSCRIPT_DEFINE_STRUCT_METHOD(mScriptStorageBucket, reload)
mSCRIPT_DEFINE_DOCSTRING("Flush the bucket to disk manually")
mSCRIPT_DEFINE_STRUCT_METHOD(mScriptStorageBucket, flush)
mSCRIPT_DEFINE_END;
mSCRIPT_DECLARE_STRUCT(mScriptStorageContext);
mSCRIPT_DECLARE_STRUCT_VOID_METHOD(mScriptStorageContext, _deinit, mScriptStorageContextDeinit, 0);
mSCRIPT_DECLARE_STRUCT_METHOD(mScriptStorageContext, S(mScriptStorageBucket), getBucket, mScriptStorageGetBucket, 1, CHARP, key);
mSCRIPT_DECLARE_STRUCT_VOID_METHOD(mScriptStorageContext, flushAll, mScriptStorageContextFlushAll, 0);
mSCRIPT_DEFINE_STRUCT(mScriptStorageContext)
mSCRIPT_DEFINE_STRUCT_DEINIT(mScriptStorageContext)
mSCRIPT_DEFINE_DOCSTRING(
"Get a bucket with the given name. Names can contain letters, numbers, "
"underscores and periods. If a given bucket doesn't exist, it is created."
)
mSCRIPT_DEFINE_STRUCT_METHOD(mScriptStorageContext, getBucket)
mSCRIPT_DEFINE_DOCSTRING("Flush all buckets to disk manually")
mSCRIPT_DEFINE_STRUCT_METHOD(mScriptStorageContext, flushAll)
mSCRIPT_DEFINE_END;
struct mScriptValue* mScriptStorageBucketGet(struct mScriptStorageBucket* bucket, const char* key) {
struct mScriptValue* val = mScriptTableLookup(bucket->root, &mSCRIPT_MAKE_CHARP(key));
if (val) {
mScriptValueRef(val);
}
return val;
}
void mScriptStorageBucketSet(struct mScriptStorageBucket* bucket, const char* key, struct mScriptValue* value) {
struct mScriptValue* vkey = mScriptStringCreateFromUTF8(key);
if (value->type->base == mSCRIPT_TYPE_WRAPPER) {
value = mScriptValueUnwrap(value);
}
mScriptTableInsert(bucket->root, vkey, value);
mScriptValueDeref(vkey);
bucket->dirty = true;
}
void mScriptStorageBucketSetVoid(struct mScriptStorageBucket* bucket, const char* key, struct mScriptValue* value) {
UNUSED(value);
struct mScriptValue* vkey = mScriptStringCreateFromUTF8(key);
mScriptTableInsert(bucket->root, vkey, &mScriptValueNull);
mScriptValueDeref(vkey);
bucket->dirty = true;
}
#define MAKE_SCALAR_SETTER(NAME, TYPE) \
void mScriptStorageBucketSet ## NAME (struct mScriptStorageBucket* bucket, const char* key, mSCRIPT_TYPE_C_ ## TYPE value) { \
struct mScriptValue* vkey = mScriptStringCreateFromUTF8(key); \
struct mScriptValue* vval = mScriptValueAlloc(mSCRIPT_TYPE_MS_ ## TYPE); \
vval->value.mSCRIPT_TYPE_FIELD_ ## TYPE = value; \
mScriptTableInsert(bucket->root, vkey, vval); \
mScriptValueDeref(vkey); \
mScriptValueDeref(vval); \
bucket->dirty = true; \
}
MAKE_SCALAR_SETTER(SInt, S64)
MAKE_SCALAR_SETTER(UInt, U64)
MAKE_SCALAR_SETTER(Float, F64)
MAKE_SCALAR_SETTER(Bool, BOOL)
void mScriptStorageGetBucketPath(const char* bucket, char* out) {
mCoreConfigDirectory(out, PATH_MAX);
strncat(out, PATH_SEP "storage" PATH_SEP, PATH_MAX);
mkdir(out, 0755);
char suffix[STORAGE_LEN_MAX + 6];
snprintf(suffix, sizeof(suffix), "%s.json", bucket);
strncat(out, suffix, PATH_MAX);
}
static struct json_object* _tableToJson(struct mScriptValue* rootVal) {
bool ok = true;
struct TableIterator iter;
struct json_object* rootObj = json_object_new_object();
if (mScriptTableIteratorStart(rootVal, &iter)) {
do {
struct mScriptValue* key = mScriptTableIteratorGetKey(rootVal, &iter);
struct mScriptValue* value = mScriptTableIteratorGetValue(rootVal, &iter);
const char* ckey;
if (key->type == mSCRIPT_TYPE_MS_CHARP) {
ckey = key->value.copaque;
} else if (key->type == mSCRIPT_TYPE_MS_STR) {
ckey = key->value.string->buffer;
} else {
ok = false;
break;
}
struct json_object* obj;
ok = mScriptStorageToJson(value, &obj);
if (!ok || json_object_object_add(rootObj, ckey, obj) < 0) {
ok = false;
}
} while (mScriptTableIteratorNext(rootVal, &iter) && ok);
}
if (!ok) {
json_object_put(rootObj);
return NULL;
}
return rootObj;
}
bool mScriptStorageToJson(struct mScriptValue* value, struct json_object** out) {
if (value->type->base == mSCRIPT_TYPE_WRAPPER) {
value = mScriptValueUnwrap(value);
}
size_t i;
bool ok = true;
struct json_object* obj = NULL;
switch (value->type->base) {
case mSCRIPT_TYPE_SINT:
obj = json_object_new_int64(value->value.s64);
break;
case mSCRIPT_TYPE_UINT:
if (value->type == mSCRIPT_TYPE_MS_BOOL) {
obj = json_object_new_boolean(value->value.u32);
break;
}
obj = json_object_new_uint64(value->value.u64);
break;
case mSCRIPT_TYPE_FLOAT:
obj = json_object_new_double(value->value.f64);
break;
case mSCRIPT_TYPE_STRING:
obj = json_object_new_string_len(value->value.string->buffer, value->value.string->size);
break;
case mSCRIPT_TYPE_LIST:
obj = json_object_new_array_ext(mScriptListSize(value->value.list));
for (i = 0; i < mScriptListSize(value->value.list); ++i) {
struct json_object* listObj;
ok = mScriptStorageToJson(mScriptListGetPointer(value->value.list, i), &listObj);
if (!ok) {
break;
}
json_object_array_add(obj, listObj);
}
break;
case mSCRIPT_TYPE_TABLE:
obj = _tableToJson(value);
if (!obj) {
ok = false;
}
break;
case mSCRIPT_TYPE_VOID:
obj = NULL;
break;
default:
ok = false;
break;
}
if (!ok) {
if (obj) {
json_object_put(obj);
}
*out = NULL;
} else {
*out = obj;
}
return ok;
}
static bool _mScriptStorageBucketFlushVF(struct mScriptStorageBucket* bucket, struct VFile* vf) {
struct json_object* rootObj;
bool ok = mScriptStorageToJson(bucket->root, &rootObj);
if (!ok) {
vf->close(vf);
return false;
}
const char* json = json_object_to_json_string_ext(rootObj, JSON_C_TO_STRING_PRETTY_TAB);
if (!json) {
json_object_put(rootObj);
vf->close(vf);
return false;
}
vf->write(vf, json, strlen(json));
vf->close(vf);
bucket->dirty = false;
json_object_put(rootObj);
return true;
}
bool mScriptStorageBucketFlush(struct mScriptStorageBucket* bucket) {
char path[PATH_MAX];
mScriptStorageGetBucketPath(bucket->name, path);
struct VFile* vf = VFileOpen(path, O_WRONLY | O_CREAT | O_TRUNC);
return _mScriptStorageBucketFlushVF(bucket, vf);
}
bool mScriptStorageSaveBucketVF(struct mScriptContext* context, const char* bucketName, struct VFile* vf) {
struct mScriptValue* value = mScriptContextGetGlobal(context, "storage");
if (!value) {
vf->close(vf);
return false;
}
struct mScriptStorageContext* storage = value->value.opaque;
struct mScriptStorageBucket* bucket = mScriptStorageGetBucket(storage, bucketName);
return _mScriptStorageBucketFlushVF(bucket, vf);
}
bool mScriptStorageSaveBucket(struct mScriptContext* context, const char* bucketName) {
char path[PATH_MAX];
mScriptStorageGetBucketPath(bucketName, path);
struct VFile* vf = VFileOpen(path, O_WRONLY | O_CREAT | O_TRUNC);
return mScriptStorageSaveBucketVF(context, bucketName, vf);
}
struct mScriptValue* mScriptStorageFromJson(struct json_object* json) {
enum json_type type = json_object_get_type(json);
struct mScriptValue* value = NULL;
switch (type) {
case json_type_null:
return &mScriptValueNull;
case json_type_int:
value = mScriptValueAlloc(mSCRIPT_TYPE_MS_S64);
value->value.s64 = json_object_get_int64(json);
break;
case json_type_double:
value = mScriptValueAlloc(mSCRIPT_TYPE_MS_F64);
value->value.f64 = json_object_get_double(json);
break;
case json_type_boolean:
value = mScriptValueAlloc(mSCRIPT_TYPE_MS_BOOL);
value->value.u32 = json_object_get_boolean(json);
break;
case json_type_string:
value = mScriptStringCreateFromBytes(json_object_get_string(json), json_object_get_string_len(json));
break;
case json_type_array:
value = mScriptValueAlloc(mSCRIPT_TYPE_MS_LIST);
{
size_t i;
for (i = 0; i < json_object_array_length(json); ++i) {
struct mScriptValue* vval = mScriptStorageFromJson(json_object_array_get_idx(json, i));
if (!vval) {
mScriptValueDeref(value);
value = NULL;
break;
}
mScriptValueWrap(vval, mScriptListAppend(value->value.list));
mScriptValueDeref(vval);
}
}
break;
case json_type_object:
value = mScriptValueAlloc(mSCRIPT_TYPE_MS_TABLE);
{
json_object_object_foreach(json, jkey, jval) {
struct mScriptValue* vval = mScriptStorageFromJson(jval);
if (!vval) {
mScriptValueDeref(value);
value = NULL;
break;
}
struct mScriptValue* vkey = mScriptStringCreateFromUTF8(jkey);
mScriptTableInsert(value, vkey, vval);
mScriptValueDeref(vkey);
mScriptValueDeref(vval);
}
}
break;
}
return value;
}
static struct mScriptValue* _mScriptStorageLoadJson(struct VFile* vf) {
ssize_t size = vf->size(vf);
if (size < 2) {
vf->close(vf);
return NULL;
}
char* json = calloc(1, size + 1);
if (vf->read(vf, json, size) != size) {
vf->close(vf);
return NULL;
}
vf->close(vf);
struct json_object* obj = json_tokener_parse(json);
free(json);
if (!obj) {
return NULL;
}
struct mScriptValue* root = mScriptStorageFromJson(obj);
json_object_put(obj);
return root;
}
bool mScriptStorageBucketReload(struct mScriptStorageBucket* bucket) {
char path[PATH_MAX];
mScriptStorageGetBucketPath(bucket->name, path);
struct VFile* vf = VFileOpen(path, O_RDONLY);
if (!vf) {
return false;
}
struct mScriptValue* root = _mScriptStorageLoadJson(vf);
if (!root) {
return false;
}
if (bucket->root) {
mScriptValueDeref(bucket->root);
}
bucket->root = root;
bucket->dirty = false;
return true;
}
bool mScriptStorageLoadBucketVF(struct mScriptContext* context, const char* bucketName, struct VFile* vf) {
struct mScriptValue* value = mScriptContextGetGlobal(context, "storage");
if (!value) {
vf->close(vf);
return false;
}
struct mScriptStorageContext* storage = value->value.opaque;
struct mScriptValue* root = _mScriptStorageLoadJson(vf);
if (!root) {
return false;
}
struct mScriptStorageBucket* bucket = mScriptStorageGetBucket(storage, bucketName);
mScriptValueDeref(bucket->root);
bucket->root = root;
bucket->dirty = false;
return true;
}
bool mScriptStorageLoadBucket(struct mScriptContext* context, const char* bucketName) {
char path[PATH_MAX];
mScriptStorageGetBucketPath(bucketName, path);
struct VFile* vf = VFileOpen(path, O_RDONLY);
if (!vf) {
return false;
}
return mScriptStorageLoadBucketVF(context, bucketName, vf);
}
void mScriptContextAttachStorage(struct mScriptContext* context) {
struct mScriptStorageContext* storage = calloc(1, sizeof(*storage));
struct mScriptValue* value = mScriptValueAlloc(mSCRIPT_TYPE_MS_S(mScriptStorageContext));
value->flags = mSCRIPT_VALUE_FLAG_FREE_BUFFER;
value->value.opaque = storage;
HashTableInit(&storage->buckets, 0, mScriptStorageBucketDeinit);
mScriptContextSetGlobal(context, "storage", value);
mScriptContextSetDocstring(context, "storage", "Singleton instance of struct::mScriptStorageContext");
}
void mScriptStorageFlushAll(struct mScriptContext* context) {
struct mScriptValue* value = mScriptContextGetGlobal(context, "storage");
if (!value) {
return;
}
struct mScriptStorageContext* storage = value->value.opaque;
mScriptStorageContextFlushAll(storage);
}
void mScriptStorageContextDeinit(struct mScriptStorageContext* storage) {
HashTableDeinit(&storage->buckets);
}
void mScriptStorageContextFlushAll(struct mScriptStorageContext* storage) {
struct TableIterator iter;
if (HashTableIteratorStart(&storage->buckets, &iter)) {
do {
struct mScriptStorageBucket* bucket = HashTableIteratorGetValue(&storage->buckets, &iter);
mScriptStorageBucketFlush(bucket);
} while (HashTableIteratorNext(&storage->buckets, &iter));
}
}
struct mScriptStorageBucket* mScriptStorageGetBucket(struct mScriptStorageContext* storage, const char* name) {
if (!name) {
return NULL;
}
// Check if name is allowed
// Currently only names matching /[0-9A-Za-z_.]+/ are allowed
size_t i;
for (i = 0; name[i]; ++i) {
if (i >= STORAGE_LEN_MAX) {
return NULL;
}
if (!isalnum(name[i]) && name[i] != '_' && name[i] != '.') {
return NULL;
}
}
struct mScriptStorageBucket* bucket = HashTableLookup(&storage->buckets, name);
if (bucket) {
return bucket;
}
bucket = calloc(1, sizeof(*bucket));
bucket->name = strdup(name);
if (!mScriptStorageBucketReload(bucket)) {
bucket->root = mScriptValueAlloc(mSCRIPT_TYPE_MS_TABLE);
}
HashTableInsert(&storage->buckets, name, bucket);
return bucket;
}
void mScriptStorageBucketDeinit(void* data) {
struct mScriptStorageBucket* bucket = data;
if (bucket->dirty) {
mScriptStorageBucketFlush(bucket);
}
mScriptValueDeref(bucket->root);
free(bucket->name);
free(bucket);
}

554
src/script/test/storage.c Normal file
View File

@ -0,0 +1,554 @@
/* Copyright (c) 2013-2023 Jeffrey Pfau
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
#include "util/test/suite.h"
#include <mgba/internal/script/lua.h>
#include <mgba/script/storage.h>
#include <mgba/script/types.h>
#include "script/test.h"
#define SETUP_LUA \
struct mScriptContext context; \
mScriptContextInit(&context); \
struct mScriptEngineContext* lua = mScriptContextRegisterEngine(&context, mSCRIPT_ENGINE_LUA); \
mScriptContextAttachStdlib(&context); \
mScriptContextAttachStorage(&context); \
char bucketPath[PATH_MAX]; \
mScriptStorageGetBucketPath("xtest", bucketPath); \
remove(bucketPath)
M_TEST_SUITE_SETUP(mScriptStorage) {
if (mSCRIPT_ENGINE_LUA->init) {
mSCRIPT_ENGINE_LUA->init(mSCRIPT_ENGINE_LUA);
}
return 0;
}
M_TEST_SUITE_TEARDOWN(mScriptStorage) {
if (mSCRIPT_ENGINE_LUA->deinit) {
mSCRIPT_ENGINE_LUA->deinit(mSCRIPT_ENGINE_LUA);
}
return 0;
}
M_TEST_DEFINE(basicInt) {
SETUP_LUA;
TEST_PROGRAM("bucket = storage:getBucket('xtest')");
TEST_PROGRAM("assert(bucket)");
TEST_PROGRAM("assert(not bucket.a)");
TEST_PROGRAM("bucket.a = 1");
TEST_PROGRAM("assert(bucket.a == 1)");
mScriptContextDeinit(&context);
}
M_TEST_DEFINE(basicFloat) {
SETUP_LUA;
TEST_PROGRAM("bucket = storage:getBucket('xtest')");
TEST_PROGRAM("assert(bucket)");
TEST_PROGRAM("assert(not bucket.a)");
TEST_PROGRAM("bucket.a = 0.5");
TEST_PROGRAM("assert(bucket.a == 0.5)");
mScriptContextDeinit(&context);
}
M_TEST_DEFINE(basicBool) {
SETUP_LUA;
TEST_PROGRAM("bucket = storage:getBucket('xtest')");
TEST_PROGRAM("assert(bucket)");
TEST_PROGRAM("assert(not bucket.a)");
TEST_PROGRAM("bucket.a = true");
TEST_PROGRAM("assert(bucket.a == true)");
mScriptContextDeinit(&context);
}
M_TEST_DEFINE(basicNil) {
SETUP_LUA;
TEST_PROGRAM("bucket = storage:getBucket('xtest')");
TEST_PROGRAM("assert(bucket)");
TEST_PROGRAM("assert(not bucket.a)");
TEST_PROGRAM("bucket.a = nil");
TEST_PROGRAM("assert(bucket.a == nil)");
mScriptContextDeinit(&context);
}
M_TEST_DEFINE(basicString) {
SETUP_LUA;
TEST_PROGRAM("bucket = storage:getBucket('xtest')");
TEST_PROGRAM("assert(bucket)");
TEST_PROGRAM("assert(not bucket.a)");
TEST_PROGRAM("bucket.a = 'hello'");
TEST_PROGRAM("assert(bucket.a == 'hello')");
mScriptContextDeinit(&context);
}
M_TEST_DEFINE(basicList) {
SETUP_LUA;
TEST_PROGRAM("bucket = storage:getBucket('xtest')");
TEST_PROGRAM("assert(bucket)");
TEST_PROGRAM("assert(not bucket.a)");
TEST_PROGRAM("bucket.a = {1}");
TEST_PROGRAM("assert(#bucket.a == 1)");
TEST_PROGRAM("assert(bucket.a[1] == 1)");
mScriptContextDeinit(&context);
}
M_TEST_DEFINE(basicTable) {
SETUP_LUA;
TEST_PROGRAM("bucket = storage:getBucket('xtest')");
TEST_PROGRAM("assert(bucket)");
TEST_PROGRAM("assert(not bucket.a)");
TEST_PROGRAM("bucket.a = {['a']=1}");
TEST_PROGRAM("assert(#bucket.a == 1)");
TEST_PROGRAM("assert(bucket.a.a == 1)");
mScriptContextDeinit(&context);
}
M_TEST_DEFINE(nullByteString) {
SETUP_LUA;
TEST_PROGRAM("bucket = storage:getBucket('xtest')");
TEST_PROGRAM("assert(bucket)");
TEST_PROGRAM("assert(not bucket.a)");
TEST_PROGRAM("bucket.a = 'a\\x00b'");
TEST_PROGRAM("assert(bucket.a == 'a\\x00b')");
TEST_PROGRAM("assert(#bucket.a == 3)");
mScriptContextDeinit(&context);
}
M_TEST_DEFINE(structured) {
SETUP_LUA;
TEST_PROGRAM("bucket = storage:getBucket('xtest')");
TEST_PROGRAM("assert(bucket)");
TEST_PROGRAM("assert(not bucket.a)");
TEST_PROGRAM(
"bucket.a = {\n"
" ['a'] = 1,\n"
" ['b'] = {1},\n"
" ['c'] = {\n"
" ['d'] = 1\n"
" }\n"
"}"
);
TEST_PROGRAM("assert(bucket.a)");
TEST_PROGRAM("assert(bucket.a.a == 1)");
TEST_PROGRAM("assert(#bucket.a.b == 1)");
TEST_PROGRAM("assert(bucket.a.b[1] == 1)");
TEST_PROGRAM("assert(#bucket.a.c == 1)");
TEST_PROGRAM("assert(bucket.a.c.d == 1)");
mScriptContextDeinit(&context);
}
M_TEST_DEFINE(invalidObject) {
SETUP_LUA;
TEST_PROGRAM("bucket = storage:getBucket('xtest')");
TEST_PROGRAM("assert(bucket)");
TEST_PROGRAM("assert(not bucket.a)");
LOAD_PROGRAM("bucket.a = bucket");
assert_false(lua->run(lua));
mScriptContextDeinit(&context);
}
M_TEST_DEFINE(serializeInt) {
SETUP_LUA;
TEST_PROGRAM("bucket = storage:getBucket('xtest')");
TEST_PROGRAM("assert(bucket)");
TEST_PROGRAM("bucket.a = 1");
struct VFile* vf = VFileOpen("test.json", O_CREAT | O_TRUNC | O_WRONLY);
assert_true(mScriptStorageSaveBucketVF(&context, "xtest", vf));
vf = VFileOpen("test.json", O_RDONLY);
assert_non_null(vf);
ssize_t size = vf->size(vf);
char* buf = calloc(1, size + 1);
assert_int_equal(vf->read(vf, buf, size), size);
assert_string_equal(buf, "{\"a\":1}");
free(buf);
vf->close(vf);
mScriptContextDeinit(&context);
}
M_TEST_DEFINE(serializeFloat) {
SETUP_LUA;
TEST_PROGRAM("bucket = storage:getBucket('xtest')");
TEST_PROGRAM("assert(bucket)");
TEST_PROGRAM("bucket.a = 0.5");
struct VFile* vf = VFileOpen("test.json", O_CREAT | O_TRUNC | O_WRONLY);
assert_true(mScriptStorageSaveBucketVF(&context, "xtest", vf));
vf = VFileOpen("test.json", O_RDONLY);
assert_non_null(vf);
ssize_t size = vf->size(vf);
char* buf = calloc(1, size + 1);
assert_int_equal(vf->read(vf, buf, size), size);
assert_string_equal(buf, "{\"a\":0.5}");
free(buf);
vf->close(vf);
mScriptContextDeinit(&context);
}
M_TEST_DEFINE(serializeBool) {
SETUP_LUA;
TEST_PROGRAM("bucket = storage:getBucket('xtest')");
TEST_PROGRAM("assert(bucket)");
TEST_PROGRAM("bucket.a = true");
struct VFile* vf = VFileOpen("test.json", O_CREAT | O_TRUNC | O_WRONLY);
assert_true(mScriptStorageSaveBucketVF(&context, "xtest", vf));
vf = VFileOpen("test.json", O_RDONLY);
assert_non_null(vf);
ssize_t size = vf->size(vf);
char* buf = calloc(1, size + 1);
assert_int_equal(vf->read(vf, buf, size), size);
assert_string_equal(buf, "{\"a\":true}");
free(buf);
vf->close(vf);
mScriptContextDeinit(&context);
}
M_TEST_DEFINE(serializeNil) {
SETUP_LUA;
TEST_PROGRAM("bucket = storage:getBucket('xtest')");
TEST_PROGRAM("assert(bucket)");
TEST_PROGRAM("bucket.a = nil");
struct VFile* vf = VFileOpen("test.json", O_CREAT | O_TRUNC | O_WRONLY);
assert_true(mScriptStorageSaveBucketVF(&context, "xtest", vf));
vf = VFileOpen("test.json", O_RDONLY);
assert_non_null(vf);
ssize_t size = vf->size(vf);
char* buf = calloc(1, size + 1);
assert_int_equal(vf->read(vf, buf, size), size);
assert_string_equal(buf, "{\"a\":null}");
free(buf);
vf->close(vf);
mScriptContextDeinit(&context);
}
M_TEST_DEFINE(serializeString) {
SETUP_LUA;
TEST_PROGRAM("bucket = storage:getBucket('xtest')");
TEST_PROGRAM("assert(bucket)");
TEST_PROGRAM("bucket.a = 'hello'");
struct VFile* vf = VFileOpen("test.json", O_CREAT | O_TRUNC | O_WRONLY);
assert_true(mScriptStorageSaveBucketVF(&context, "xtest", vf));
vf = VFileOpen("test.json", O_RDONLY);
assert_non_null(vf);
ssize_t size = vf->size(vf);
char* buf = calloc(1, size + 1);
assert_int_equal(vf->read(vf, buf, size), size);
assert_string_equal(buf, "{\"a\":\"hello\"}");
free(buf);
vf->close(vf);
mScriptContextDeinit(&context);
}
M_TEST_DEFINE(serializeList) {
SETUP_LUA;
TEST_PROGRAM("bucket = storage:getBucket('xtest')");
TEST_PROGRAM("assert(bucket)");
TEST_PROGRAM("bucket.a = {1, 2}");
struct VFile* vf = VFileOpen("test.json", O_CREAT | O_TRUNC | O_WRONLY);
assert_true(mScriptStorageSaveBucketVF(&context, "xtest", vf));
vf = VFileOpen("test.json", O_RDONLY);
assert_non_null(vf);
ssize_t size = vf->size(vf);
char* buf = calloc(1, size + 1);
assert_int_equal(vf->read(vf, buf, size), size);
assert_string_equal(buf, "{\"a\":[1,2]}");
free(buf);
vf->close(vf);
mScriptContextDeinit(&context);
}
M_TEST_DEFINE(serializeTable) {
SETUP_LUA;
TEST_PROGRAM("bucket = storage:getBucket('xtest')");
TEST_PROGRAM("assert(bucket)");
TEST_PROGRAM("bucket.a = {['b']=1}");
struct VFile* vf = VFileOpen("test.json", O_CREAT | O_TRUNC | O_WRONLY);
assert_true(mScriptStorageSaveBucketVF(&context, "xtest", vf));
vf = VFileOpen("test.json", O_RDONLY);
assert_non_null(vf);
ssize_t size = vf->size(vf);
char* buf = calloc(1, size + 1);
assert_int_equal(vf->read(vf, buf, size), size);
assert_string_equal(buf, "{\"a\":{\"b\":1}}");
free(buf);
vf->close(vf);
mScriptContextDeinit(&context);
}
M_TEST_DEFINE(serializeNullByteString) {
SETUP_LUA;
TEST_PROGRAM("bucket = storage:getBucket('xtest')");
TEST_PROGRAM("assert(bucket)");
TEST_PROGRAM("bucket.a = 'a\\x00b'");
struct VFile* vf = VFileOpen("test.json", O_CREAT | O_TRUNC | O_WRONLY);
assert_true(mScriptStorageSaveBucketVF(&context, "xtest", vf));
vf = VFileOpen("test.json", O_RDONLY);
assert_non_null(vf);
ssize_t size = vf->size(vf);
char* buf = calloc(1, size + 1);
assert_int_equal(vf->read(vf, buf, size), size);
assert_string_equal(buf, "{\"a\":\"a\\u0000b\"}");
free(buf);
vf->close(vf);
mScriptContextDeinit(&context);
}
M_TEST_DEFINE(deserializeInt) {
SETUP_LUA;
TEST_PROGRAM("bucket = storage:getBucket('xtest')");
TEST_PROGRAM("assert(bucket)");
TEST_PROGRAM("assert(not bucket.a)");
static const char* json = "{\"a\":1}";
struct VFile* vf = VFileFromConstMemory(json, strlen(json));
assert_true(mScriptStorageLoadBucketVF(&context, "xtest", vf));
TEST_PROGRAM("assert(bucket.a == 1)");
mScriptContextDeinit(&context);
}
M_TEST_DEFINE(deserializeFloat) {
SETUP_LUA;
TEST_PROGRAM("bucket = storage:getBucket('xtest')");
TEST_PROGRAM("assert(bucket)");
TEST_PROGRAM("assert(not bucket.a)");
static const char* json = "{\"a\":0.5}";
struct VFile* vf = VFileFromConstMemory(json, strlen(json));
assert_true(mScriptStorageLoadBucketVF(&context, "xtest", vf));
TEST_PROGRAM("assert(bucket.a == 0.5)");
mScriptContextDeinit(&context);
}
M_TEST_DEFINE(deserializeBool) {
SETUP_LUA;
TEST_PROGRAM("bucket = storage:getBucket('xtest')");
TEST_PROGRAM("assert(bucket)");
TEST_PROGRAM("assert(not bucket.a)");
static const char* json = "{\"a\":true}";
struct VFile* vf = VFileFromConstMemory(json, strlen(json));
assert_true(mScriptStorageLoadBucketVF(&context, "xtest", vf));
TEST_PROGRAM("assert(bucket.a == true)");
mScriptContextDeinit(&context);
}
M_TEST_DEFINE(deserializeNil) {
SETUP_LUA;
TEST_PROGRAM("bucket = storage:getBucket('xtest')");
TEST_PROGRAM("assert(bucket)");
TEST_PROGRAM("assert(not bucket.a)");
static const char* json = "{\"a\":null}";
struct VFile* vf = VFileFromConstMemory(json, strlen(json));
assert_true(mScriptStorageLoadBucketVF(&context, "xtest", vf));
TEST_PROGRAM("assert(bucket.a == nil)");
mScriptContextDeinit(&context);
}
M_TEST_DEFINE(deserializeString) {
SETUP_LUA;
TEST_PROGRAM("bucket = storage:getBucket('xtest')");
TEST_PROGRAM("assert(bucket)");
TEST_PROGRAM("assert(not bucket.a)");
static const char* json = "{\"a\":\"hello\"}";
struct VFile* vf = VFileFromConstMemory(json, strlen(json));
assert_true(mScriptStorageLoadBucketVF(&context, "xtest", vf));
TEST_PROGRAM("assert(bucket.a == 'hello')");
mScriptContextDeinit(&context);
}
M_TEST_DEFINE(deserializeList) {
SETUP_LUA;
TEST_PROGRAM("bucket = storage:getBucket('xtest')");
TEST_PROGRAM("assert(bucket)");
TEST_PROGRAM("assert(not bucket.a)");
static const char* json = "{\"a\":[1,2]}";
struct VFile* vf = VFileFromConstMemory(json, strlen(json));
assert_true(mScriptStorageLoadBucketVF(&context, "xtest", vf));
TEST_PROGRAM("assert(#bucket.a == 2)");
TEST_PROGRAM("assert(bucket.a[1] == 1)");
TEST_PROGRAM("assert(bucket.a[2] == 2)");
mScriptContextDeinit(&context);
}
M_TEST_DEFINE(deserializeTable) {
SETUP_LUA;
TEST_PROGRAM("bucket = storage:getBucket('xtest')");
TEST_PROGRAM("assert(bucket)");
TEST_PROGRAM("assert(not bucket.a)");
static const char* json = "{\"a\":{\"b\":1}}";
struct VFile* vf = VFileFromConstMemory(json, strlen(json));
assert_true(mScriptStorageLoadBucketVF(&context, "xtest", vf));
TEST_PROGRAM("assert(bucket.a.b == 1)");
mScriptContextDeinit(&context);
}
M_TEST_DEFINE(deserializeNullByteString) {
SETUP_LUA;
TEST_PROGRAM("bucket = storage:getBucket('xtest')");
TEST_PROGRAM("assert(bucket)");
TEST_PROGRAM("assert(not bucket.a)");
static const char* json = "{\"a\":\"a\\u0000b\"}";
struct VFile* vf = VFileFromConstMemory(json, strlen(json));
assert_true(mScriptStorageLoadBucketVF(&context, "xtest", vf));
TEST_PROGRAM("assert(bucket.a == 'a\\x00b')");
TEST_PROGRAM("assert(bucket.a ~= 'a\\x00c')");
mScriptContextDeinit(&context);
}
M_TEST_DEFINE(deserializeError) {
SETUP_LUA;
TEST_PROGRAM("bucket = storage:getBucket('xtest')");
TEST_PROGRAM("assert(bucket)");
TEST_PROGRAM("assert(not bucket.a)");
static const char* json = "{a:1}";
struct VFile* vf = VFileFromConstMemory(json, strlen(json));
assert_false(mScriptStorageLoadBucketVF(&context, "xtest", vf));
TEST_PROGRAM("assert(not bucket.a)");
mScriptContextDeinit(&context);
}
M_TEST_DEFINE(structuredRoundTrip) {
SETUP_LUA;
TEST_PROGRAM("bucket = storage:getBucket('xtest')");
TEST_PROGRAM("assert(bucket)");
TEST_PROGRAM("assert(not bucket.a)");
TEST_PROGRAM(
"bucket.a = {\n"
" ['a'] = 1,\n"
" ['b'] = {1},\n"
" ['c'] = {\n"
" ['d'] = 1\n"
" }\n"
"}"
);
struct VFile* vf = VFileOpen("test.json", O_CREAT | O_TRUNC | O_WRONLY);
assert_true(mScriptStorageSaveBucketVF(&context, "xtest", vf));
TEST_PROGRAM("bucket.a = nil")
TEST_PROGRAM("assert(not bucket.a)");
vf = VFileOpen("test.json", O_RDONLY);
assert_non_null(vf);
assert_true(mScriptStorageLoadBucketVF(&context, "xtest", vf));
TEST_PROGRAM("assert(bucket.a)");
TEST_PROGRAM("assert(bucket.a.a == 1)");
TEST_PROGRAM("assert(#bucket.a.b == 1)");
TEST_PROGRAM("assert(bucket.a.b[1] == 1)");
TEST_PROGRAM("assert(#bucket.a.c == 1)");
TEST_PROGRAM("assert(bucket.a.c.d == 1)");
mScriptContextDeinit(&context);
}
M_TEST_SUITE_DEFINE_SETUP_TEARDOWN(mScriptStorage,
cmocka_unit_test(basicInt),
cmocka_unit_test(basicFloat),
cmocka_unit_test(basicBool),
cmocka_unit_test(basicNil),
cmocka_unit_test(basicString),
cmocka_unit_test(basicList),
cmocka_unit_test(basicTable),
cmocka_unit_test(nullByteString),
cmocka_unit_test(invalidObject),
cmocka_unit_test(structured),
cmocka_unit_test(serializeInt),
cmocka_unit_test(serializeFloat),
cmocka_unit_test(serializeBool),
cmocka_unit_test(serializeNil),
cmocka_unit_test(serializeString),
cmocka_unit_test(serializeList),
cmocka_unit_test(serializeTable),
cmocka_unit_test(serializeNullByteString),
cmocka_unit_test(deserializeInt),
cmocka_unit_test(deserializeFloat),
cmocka_unit_test(deserializeBool),
cmocka_unit_test(deserializeNil),
cmocka_unit_test(deserializeString),
cmocka_unit_test(deserializeList),
cmocka_unit_test(deserializeTable),
cmocka_unit_test(deserializeNullByteString),
cmocka_unit_test(deserializeError),
cmocka_unit_test(structuredRoundTrip),
)