From 00a34e0d0767f6a1f3f79f52ab24367034ee7204 Mon Sep 17 00:00:00 2001 From: Vicki Pfau Date: Sat, 4 Feb 2023 01:18:01 -0800 Subject: [PATCH] Scripting: Add skeleton of storage API --- include/mgba/script/storage.h | 21 +++++ src/script/CMakeLists.txt | 9 ++ src/script/storage.c | 155 +++++++++++++++++++++++++++++++ src/script/test/storage.c | 169 ++++++++++++++++++++++++++++++++++ 4 files changed, 354 insertions(+) create mode 100644 include/mgba/script/storage.h create mode 100644 src/script/storage.c create mode 100644 src/script/test/storage.c diff --git a/include/mgba/script/storage.h b/include/mgba/script/storage.h new file mode 100644 index 000000000..5b6490e82 --- /dev/null +++ b/include/mgba/script/storage.h @@ -0,0 +1,21 @@ +/* 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 + +CXX_GUARD_START + +#include +#include + +struct VFile; +void mScriptContextAttachStorage(struct mScriptContext* context); + +CXX_GUARD_END + +#endif diff --git a/src/script/CMakeLists.txt b/src/script/CMakeLists.txt index d84659453..f79b66d08 100644 --- a/src/script/CMakeLists.txt +++ b/src/script/CMakeLists.txt @@ -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}) diff --git a/src/script/storage.c b/src/script/storage.c new file mode 100644 index 000000000..9bbbbfa5b --- /dev/null +++ b/src/script/storage.c @@ -0,0 +1,155 @@ +/* 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 + +#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* adapter, const char* key); +static void mScriptStorageBucketSet(struct mScriptStorageBucket* adapter, const char* key, struct mScriptValue* value); +static void mScriptStorageBucketSetVoid(struct mScriptStorageBucket* adapter, const char* key, struct mScriptValue* value); +static void mScriptStorageBucketSetSInt(struct mScriptStorageBucket* adapter, const char* key, int64_t value); +static void mScriptStorageBucketSetUInt(struct mScriptStorageBucket* adapter, const char* key, uint64_t value); +static void mScriptStorageBucketSetFloat(struct mScriptStorageBucket* adapter, const char* key, double value); +static void mScriptStorageBucketSetBool(struct mScriptStorageBucket* adapter, const char* key, bool value); + +void mScriptStorageContextDeinit(struct mScriptStorageContext*); +struct mScriptStorageBucket* mScriptStorageGetBucket(struct mScriptStorageContext*, const char* name); + +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_DEFINE_STRUCT(mScriptStorageBucket) + 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_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_DEFINE_STRUCT(mScriptStorageContext) + mSCRIPT_DEFINE_STRUCT_DEINIT(mScriptStorageContext) + mSCRIPT_DEFINE_STRUCT_METHOD(mScriptStorageContext, getBucket) +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 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 mScriptStorageContextDeinit(struct mScriptStorageContext* storage) { + HashTableDeinit(&storage->buckets); +} + +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->root = mScriptValueAlloc(mSCRIPT_TYPE_MS_TABLE); + bucket->name = strdup(name); + HashTableInsert(&storage->buckets, name, bucket); + return bucket; +} + +void mScriptStorageBucketDeinit(void* data) { + struct mScriptStorageBucket* bucket = data; + mScriptValueDeref(bucket->root); + free(bucket->name); + free(bucket); +} diff --git a/src/script/test/storage.c b/src/script/test/storage.c new file mode 100644 index 000000000..9777c6280 --- /dev/null +++ b/src/script/test/storage.c @@ -0,0 +1,169 @@ +/* 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 +#include +#include + +#include "script/test.h" + +#define SETUP_LUA \ + struct mScriptContext context; \ + mScriptContextInit(&context); \ + struct mScriptEngineContext* lua = mScriptContextRegisterEngine(&context, mSCRIPT_ENGINE_LUA); \ + mScriptContextAttachStdlib(&context); \ + mScriptContextAttachStorage(&context) + +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_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(structured), +)