1
0
Fork 0

Codechange: keep how we convert string <-> JSON private (#11269)

pull/11273/head
Patric Stout 2023-09-08 19:03:10 +02:00 committed by GitHub
parent d725fa14a2
commit 00f13282a9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 156 additions and 162 deletions

View File

@ -14,9 +14,22 @@
#include "../script_instance.hpp"
#include "../../string_func.h"
#include <nlohmann/json.hpp>
#include "../../safeguards.h"
/* static */ bool ScriptAdmin::MakeJSON(nlohmann::json &json, HSQUIRRELVM vm, SQInteger index, int depth)
/**
* Convert a Squirrel structure into a JSON object.
*
* This function is not "static", so it can be tested in unittests.
*
* @param json The resulting JSON object.
* @param vm The VM to operate on.
* @param index The index we are currently working for.
* @param depth The current depth in the squirrel struct.
* @return True iff the conversion was successful.
*/
bool ScriptAdminMakeJSON(nlohmann::json &json, HSQUIRRELVM vm, SQInteger index, int depth = 0)
{
if (depth == SQUIRREL_MAX_DEPTH) {
ScriptLog::Error("Send parameters can only be nested to 25 deep. No data sent."); // SQUIRREL_MAX_DEPTH = 25
@ -47,7 +60,7 @@
while (SQ_SUCCEEDED(sq_next(vm, index - 1))) {
nlohmann::json tmp;
bool res = MakeJSON(tmp, vm, -1, depth + 1);
bool res = ScriptAdminMakeJSON(tmp, vm, -1, depth + 1);
sq_pop(vm, 2);
if (!res) {
sq_pop(vm, 1);
@ -72,7 +85,7 @@
std::string key = std::string(buf);
nlohmann::json value;
bool res = MakeJSON(value, vm, -1, depth + 1);
bool res = ScriptAdminMakeJSON(value, vm, -1, depth + 1);
sq_pop(vm, 2);
if (!res) {
sq_pop(vm, 1);
@ -113,7 +126,7 @@
}
nlohmann::json json;
if (!ScriptAdmin::MakeJSON(json, vm, -1)) {
if (!ScriptAdminMakeJSON(json, vm, -1)) {
sq_pushinteger(vm, 0);
return 1;
}

View File

@ -11,7 +11,6 @@
#define SCRIPT_ADMIN_HPP
#include "script_object.hpp"
#include <nlohmann/json.hpp>
/**
* Class that handles communication with the AdminPort.
@ -36,17 +35,6 @@ public:
*/
static bool Send(void *table);
#endif /* DOXYGEN_API */
protected:
/**
* Convert a Squirrel structure into a JSON object.
* @param json The resulting JSON object.
* @param vm The VM to operate on.
* @param index The index we are currently working for.
* @param depth The current depth in the squirrel struct.
* @return True iff the conversion was successful.
*/
static bool MakeJSON(nlohmann::json &data, HSQUIRRELVM vm, SQInteger index, int depth = 0);
};
#endif /* SCRIPT_ADMIN_HPP */

View File

@ -20,6 +20,8 @@
#include "../../engine_cmd.h"
#include "table/strings.h"
#include <nlohmann/json.hpp>
#include "../../safeguards.h"
bool ScriptEventEnginePreview::IsEngineValid() const
@ -127,33 +129,12 @@ ScriptEventAdminPort::ScriptEventAdminPort(const std::string &json) :
json(json)
{
}
SQInteger ScriptEventAdminPort::GetObject(HSQUIRRELVM vm)
{
auto json = nlohmann::json::parse(this->json, nullptr, false);
if (!json.is_object()) {
ScriptLog::Error("The root element in the JSON data from AdminPort has to be an object.");
sq_pushnull(vm);
return 1;
}
auto top = sq_gettop(vm);
if (!this->ReadValue(vm, json)) {
/* Rewind the stack, removing anything that might be left on top. */
sq_settop(vm, top);
ScriptLog::Error("Received invalid JSON data from AdminPort.");
sq_pushnull(vm);
return 1;
}
return 1;
}
bool ScriptEventAdminPort::ReadValue(HSQUIRRELVM vm, nlohmann::json &json)
/**
* Convert a JSON part fo Squirrel.
* @param vm The VM used.
* @param json The JSON part to convert to Squirrel.
*/
static bool ScriptEventAdminPortReadValue(HSQUIRRELVM vm, nlohmann::json &json)
{
switch (json.type()) {
case nlohmann::json::value_t::null:
@ -181,7 +162,7 @@ bool ScriptEventAdminPort::ReadValue(HSQUIRRELVM vm, nlohmann::json &json)
for (auto &[key, value] : json.items()) {
sq_pushstring(vm, key.data(), key.size());
if (!this->ReadValue(vm, value)) {
if (!ScriptEventAdminPortReadValue(vm, value)) {
return false;
}
@ -193,7 +174,7 @@ bool ScriptEventAdminPort::ReadValue(HSQUIRRELVM vm, nlohmann::json &json)
sq_newarray(vm, 0);
for (auto &value : json) {
if (!this->ReadValue(vm, value)) {
if (!ScriptEventAdminPortReadValue(vm, value)) {
return false;
}
@ -209,3 +190,28 @@ bool ScriptEventAdminPort::ReadValue(HSQUIRRELVM vm, nlohmann::json &json)
return true;
}
SQInteger ScriptEventAdminPort::GetObject(HSQUIRRELVM vm)
{
auto json = nlohmann::json::parse(this->json, nullptr, false);
if (!json.is_object()) {
ScriptLog::Error("The root element in the JSON data from AdminPort has to be an object.");
sq_pushnull(vm);
return 1;
}
auto top = sq_gettop(vm);
if (!ScriptEventAdminPortReadValue(vm, json)) {
/* Rewind the stack, removing anything that might be left on top. */
sq_settop(vm, top);
ScriptLog::Error("Received invalid JSON data from AdminPort.");
sq_pushnull(vm);
return 1;
}
return 1;
}

View File

@ -14,8 +14,6 @@
#include "script_goal.hpp"
#include "script_window.hpp"
#include <nlohmann/json.hpp>
/**
* Event Vehicle Crash, indicating a vehicle of yours is crashed.
* It contains the crash site, the crashed vehicle and the reason for the crash.
@ -912,13 +910,6 @@ public:
private:
std::string json; ///< The JSON string.
/**
* Convert a JSON part fo Squirrel.
* @param vm The VM used.
* @param json The JSON part to convert to Squirrel.
*/
bool ReadValue(HSQUIRRELVM vm, nlohmann::json &json);
};
/**

View File

@ -20,6 +20,7 @@
#include "../3rdparty/fmt/format.h"
#include <squirrel.h>
#include <nlohmann/json.hpp>
/**
* A controller to start enough so we can use Squirrel for testing.
@ -40,146 +41,141 @@ public:
ScriptAllocatorScope scope{&engine};
};
extern bool ScriptAdminMakeJSON(nlohmann::json &json, HSQUIRRELVM vm, SQInteger index, int depth = 0);
/**
* Small wrapper around ScriptAdmin.
*
* MakeJSON is protected; so for tests, we make a public function with
* which we call into the protected one. This prevents accidental use
* by the rest of the code, while still being able to test it.
* Small wrapper around ScriptAdmin's MakeJSON that prepares the Squirrel
* engine if it was called from actual scripting..
*/
class TestScriptAdmin : public ScriptAdmin {
public:
static std::optional<std::string> MakeJSON(std::string_view squirrel)
{
auto vm = sq_open(1024);
/* sq_compile creates a closure with our snipper, which is a table.
* Add "return " to get the table on the stack. */
std::string buffer = fmt::format("return {}", squirrel);
static std::optional<std::string> TestScriptAdminMakeJSON(std::string_view squirrel)
{
auto vm = sq_open(1024);
/* sq_compile creates a closure with our snipper, which is a table.
* Add "return " to get the table on the stack. */
std::string buffer = fmt::format("return {}", squirrel);
/* Insert an (empty) class for testing. */
sq_pushroottable(vm);
sq_pushstring(vm, "DummyClass", -1);
sq_newclass(vm, SQFalse);
sq_newslot(vm, -3, SQFalse);
sq_pop(vm, 1);
/* Insert an (empty) class for testing. */
sq_pushroottable(vm);
sq_pushstring(vm, "DummyClass", -1);
sq_newclass(vm, SQFalse);
sq_newslot(vm, -3, SQFalse);
sq_pop(vm, 1);
/* Compile the snippet. */
REQUIRE(sq_compilebuffer(vm, buffer.c_str(), buffer.size(), "test", SQTrue) == SQ_OK);
/* Execute the snippet, capturing the return value. */
sq_pushroottable(vm);
REQUIRE(sq_call(vm, 1, SQTrue, SQTrue) == SQ_OK);
/* Ensure the snippet pushed a table on the stack. */
REQUIRE(sq_gettype(vm, -1) == OT_TABLE);
/* Feed the snippet into the MakeJSON function. */
nlohmann::json json;
if (!ScriptAdmin::MakeJSON(json, vm, -1)) {
sq_close(vm);
return std::nullopt;
}
/* Compile the snippet. */
REQUIRE(sq_compilebuffer(vm, buffer.c_str(), buffer.size(), "test", SQTrue) == SQ_OK);
/* Execute the snippet, capturing the return value. */
sq_pushroottable(vm);
REQUIRE(sq_call(vm, 1, SQTrue, SQTrue) == SQ_OK);
/* Ensure the snippet pushed a table on the stack. */
REQUIRE(sq_gettype(vm, -1) == OT_TABLE);
/* Feed the snippet into the MakeJSON function. */
nlohmann::json json;
if (!ScriptAdminMakeJSON(json, vm, -1)) {
sq_close(vm);
return json.dump();
return std::nullopt;
}
/**
* Validate ScriptEventAdminPort can convert JSON to Squirrel.
*
* This function is not actually part of ScriptAdmin, but we will use MakeJSON,
* and as such need to be inside this class.
*
* The easiest way to do validate, is to first use ScriptEventAdminPort (the function
* we are testing) to convert the JSON to a Squirrel table. Then to use MakeJSON
* to convert it back to JSON.
*
* Sadly, Squirrel has no way to easily compare if two tables are identical, so we
* use the JSON -> Squirrel -> JSON method to validate the conversion. But mind you,
* a failure in the final JSON might also mean a bug in MakeJSON.
*
* @param json The JSON-string to convert to Squirrel
* @return The Squirrel table converted to a JSON-string.
*/
static std::optional<std::string> TestScriptEventAdminPort(const std::string &json)
{
auto vm = sq_open(1024);
sq_close(vm);
return json.dump();
}
/* Run the conversion JSON -> Squirrel (this will now be on top of the stack). */
ScriptEventAdminPort(json).GetObject(vm);
if (sq_gettype(vm, -1) == OT_NULL) {
sq_close(vm);
return std::nullopt;
}
REQUIRE(sq_gettype(vm, -1) == OT_TABLE);
nlohmann::json squirrel_json;
REQUIRE(ScriptAdmin::MakeJSON(squirrel_json, vm, -1) == true);
/**
* Validate ScriptEventAdminPort can convert JSON to Squirrel.
*
* This function is not actually part of ScriptAdmin, but we will use MakeJSON,
* and as such need to be inside this class.
*
* The easiest way to do validate, is to first use ScriptEventAdminPort (the function
* we are testing) to convert the JSON to a Squirrel table. Then to use MakeJSON
* to convert it back to JSON.
*
* Sadly, Squirrel has no way to easily compare if two tables are identical, so we
* use the JSON -> Squirrel -> JSON method to validate the conversion. But mind you,
* a failure in the final JSON might also mean a bug in MakeJSON.
*
* @param json The JSON-string to convert to Squirrel
* @return The Squirrel table converted to a JSON-string.
*/
static std::optional<std::string> TestScriptEventAdminPort(const std::string &json)
{
auto vm = sq_open(1024);
/* Run the conversion JSON -> Squirrel (this will now be on top of the stack). */
ScriptEventAdminPort(json).GetObject(vm);
if (sq_gettype(vm, -1) == OT_NULL) {
sq_close(vm);
return squirrel_json.dump();
return std::nullopt;
}
REQUIRE(sq_gettype(vm, -1) == OT_TABLE);
};
nlohmann::json squirrel_json;
REQUIRE(ScriptAdminMakeJSON(squirrel_json, vm, -1) == true);
sq_close(vm);
return squirrel_json.dump();
}
TEST_CASE("Squirrel -> JSON conversion")
{
TestScriptController controller;
CHECK(TestScriptAdmin::MakeJSON(R"sq({ test = null })sq") == R"json({"test":null})json");
CHECK(TestScriptAdmin::MakeJSON(R"sq({ test = 1 })sq") == R"json({"test":1})json");
CHECK(TestScriptAdmin::MakeJSON(R"sq({ test = -1 })sq") == R"json({"test":-1})json");
CHECK(TestScriptAdmin::MakeJSON(R"sq({ test = true })sq") == R"json({"test":true})json");
CHECK(TestScriptAdmin::MakeJSON(R"sq({ test = "a" })sq") == R"json({"test":"a"})json");
CHECK(TestScriptAdmin::MakeJSON(R"sq({ test = [ ] })sq") == R"json({"test":[]})json");
CHECK(TestScriptAdmin::MakeJSON(R"sq({ test = [ 1 ] })sq") == R"json({"test":[1]})json");
CHECK(TestScriptAdmin::MakeJSON(R"sq({ test = [ 1, "a", true, { test = 1 }, [], null ] })sq") == R"json({"test":[1,"a",true,{"test":1},[],null]})json");
CHECK(TestScriptAdmin::MakeJSON(R"sq({ test = { } })sq") == R"json({"test":{}})json");
CHECK(TestScriptAdmin::MakeJSON(R"sq({ test = { test = 1 } })sq") == R"json({"test":{"test":1}})json");
CHECK(TestScriptAdmin::MakeJSON(R"sq({ test = { test = 1, test = 2 } })sq") == R"json({"test":{"test":2}})json");
CHECK(TestScriptAdmin::MakeJSON(R"sq({ test = { test = 1, test2 = [ 2 ] } })sq") == R"json({"test":{"test":1,"test2":[2]}})json");
CHECK(TestScriptAdminMakeJSON(R"sq({ test = null })sq") == R"json({"test":null})json");
CHECK(TestScriptAdminMakeJSON(R"sq({ test = 1 })sq") == R"json({"test":1})json");
CHECK(TestScriptAdminMakeJSON(R"sq({ test = -1 })sq") == R"json({"test":-1})json");
CHECK(TestScriptAdminMakeJSON(R"sq({ test = true })sq") == R"json({"test":true})json");
CHECK(TestScriptAdminMakeJSON(R"sq({ test = "a" })sq") == R"json({"test":"a"})json");
CHECK(TestScriptAdminMakeJSON(R"sq({ test = [ ] })sq") == R"json({"test":[]})json");
CHECK(TestScriptAdminMakeJSON(R"sq({ test = [ 1 ] })sq") == R"json({"test":[1]})json");
CHECK(TestScriptAdminMakeJSON(R"sq({ test = [ 1, "a", true, { test = 1 }, [], null ] })sq") == R"json({"test":[1,"a",true,{"test":1},[],null]})json");
CHECK(TestScriptAdminMakeJSON(R"sq({ test = { } })sq") == R"json({"test":{}})json");
CHECK(TestScriptAdminMakeJSON(R"sq({ test = { test = 1 } })sq") == R"json({"test":{"test":1}})json");
CHECK(TestScriptAdminMakeJSON(R"sq({ test = { test = 1, test = 2 } })sq") == R"json({"test":{"test":2}})json");
CHECK(TestScriptAdminMakeJSON(R"sq({ test = { test = 1, test2 = [ 2 ] } })sq") == R"json({"test":{"test":1,"test2":[2]}})json");
/* Cases that should fail, as we cannot convert a class to JSON. */
CHECK(TestScriptAdmin::MakeJSON(R"sq({ test = DummyClass })sq") == std::nullopt);
CHECK(TestScriptAdmin::MakeJSON(R"sq({ test = [ 1, DummyClass ] })sq") == std::nullopt);
CHECK(TestScriptAdmin::MakeJSON(R"sq({ test = { test = 1, test2 = DummyClass } })sq") == std::nullopt);
CHECK(TestScriptAdminMakeJSON(R"sq({ test = DummyClass })sq") == std::nullopt);
CHECK(TestScriptAdminMakeJSON(R"sq({ test = [ 1, DummyClass ] })sq") == std::nullopt);
CHECK(TestScriptAdminMakeJSON(R"sq({ test = { test = 1, test2 = DummyClass } })sq") == std::nullopt);
}
TEST_CASE("JSON -> Squirrel conversion")
{
TestScriptController controller;
CHECK(TestScriptAdmin::TestScriptEventAdminPort(R"json({ "test": null })json") == R"json({"test":null})json");
CHECK(TestScriptAdmin::TestScriptEventAdminPort(R"json({ "test": 1 })json") == R"json({"test":1})json");
CHECK(TestScriptAdmin::TestScriptEventAdminPort(R"json({ "test": -1 })json") == R"json({"test":-1})json");
CHECK(TestScriptAdmin::TestScriptEventAdminPort(R"json({ "test": true })json") == R"json({"test":true})json");
CHECK(TestScriptAdmin::TestScriptEventAdminPort(R"json({ "test": "a" })json") == R"json({"test":"a"})json");
CHECK(TestScriptAdmin::TestScriptEventAdminPort(R"json({ "test": [] })json") == R"json({"test":[]})json");
CHECK(TestScriptAdmin::TestScriptEventAdminPort(R"json({ "test": [ 1 ] })json") == R"json({"test":[1]})json");
CHECK(TestScriptAdmin::TestScriptEventAdminPort(R"json({ "test": [ 1, "a", true, { "test": 1 }, [], null ] })json") == R"json({"test":[1,"a",true,{"test":1},[],null]})json");
CHECK(TestScriptAdmin::TestScriptEventAdminPort(R"json({ "test": {} })json") == R"json({"test":{}})json");
CHECK(TestScriptAdmin::TestScriptEventAdminPort(R"json({ "test": { "test": 1 } })json") == R"json({"test":{"test":1}})json");
CHECK(TestScriptAdmin::TestScriptEventAdminPort(R"json({ "test": { "test": 2 } })json") == R"json({"test":{"test":2}})json");
CHECK(TestScriptAdmin::TestScriptEventAdminPort(R"json({ "test": { "test": 1, "test2": [ 2 ] } })json") == R"json({"test":{"test":1,"test2":[2]}})json");
CHECK(TestScriptEventAdminPort(R"json({ "test": null })json") == R"json({"test":null})json");
CHECK(TestScriptEventAdminPort(R"json({ "test": 1 })json") == R"json({"test":1})json");
CHECK(TestScriptEventAdminPort(R"json({ "test": -1 })json") == R"json({"test":-1})json");
CHECK(TestScriptEventAdminPort(R"json({ "test": true })json") == R"json({"test":true})json");
CHECK(TestScriptEventAdminPort(R"json({ "test": "a" })json") == R"json({"test":"a"})json");
CHECK(TestScriptEventAdminPort(R"json({ "test": [] })json") == R"json({"test":[]})json");
CHECK(TestScriptEventAdminPort(R"json({ "test": [ 1 ] })json") == R"json({"test":[1]})json");
CHECK(TestScriptEventAdminPort(R"json({ "test": [ 1, "a", true, { "test": 1 }, [], null ] })json") == R"json({"test":[1,"a",true,{"test":1},[],null]})json");
CHECK(TestScriptEventAdminPort(R"json({ "test": {} })json") == R"json({"test":{}})json");
CHECK(TestScriptEventAdminPort(R"json({ "test": { "test": 1 } })json") == R"json({"test":{"test":1}})json");
CHECK(TestScriptEventAdminPort(R"json({ "test": { "test": 2 } })json") == R"json({"test":{"test":2}})json");
CHECK(TestScriptEventAdminPort(R"json({ "test": { "test": 1, "test2": [ 2 ] } })json") == R"json({"test":{"test":1,"test2":[2]}})json");
/* Check if spaces are properly ignored. */
CHECK(TestScriptAdmin::TestScriptEventAdminPort(R"json({"test":1})json") == R"json({"test":1})json");
CHECK(TestScriptAdmin::TestScriptEventAdminPort(R"json({"test": 1})json") == R"json({"test":1})json");
CHECK(TestScriptEventAdminPort(R"json({"test":1})json") == R"json({"test":1})json");
CHECK(TestScriptEventAdminPort(R"json({"test": 1})json") == R"json({"test":1})json");
/* Valid JSON but invalid Squirrel (read: floats). */
CHECK(TestScriptAdmin::TestScriptEventAdminPort(R"json({ "test": 1.1 })json") == std::nullopt);
CHECK(TestScriptAdmin::TestScriptEventAdminPort(R"json({ "test": [ 1, 3, 1.1 ] })json") == std::nullopt);
CHECK(TestScriptEventAdminPort(R"json({ "test": 1.1 })json") == std::nullopt);
CHECK(TestScriptEventAdminPort(R"json({ "test": [ 1, 3, 1.1 ] })json") == std::nullopt);
/* Root element has to be an object. */
CHECK(TestScriptAdmin::TestScriptEventAdminPort(R"json( 1 )json") == std::nullopt);
CHECK(TestScriptAdmin::TestScriptEventAdminPort(R"json( "a" )json") == std::nullopt);
CHECK(TestScriptAdmin::TestScriptEventAdminPort(R"json( [ 1 ] )json") == std::nullopt);
CHECK(TestScriptAdmin::TestScriptEventAdminPort(R"json( null )json") == std::nullopt);
CHECK(TestScriptAdmin::TestScriptEventAdminPort(R"json( true )json") == std::nullopt);
CHECK(TestScriptEventAdminPort(R"json( 1 )json") == std::nullopt);
CHECK(TestScriptEventAdminPort(R"json( "a" )json") == std::nullopt);
CHECK(TestScriptEventAdminPort(R"json( [ 1 ] )json") == std::nullopt);
CHECK(TestScriptEventAdminPort(R"json( null )json") == std::nullopt);
CHECK(TestScriptEventAdminPort(R"json( true )json") == std::nullopt);
/* Cases that should fail, as it is invalid JSON. */
CHECK(TestScriptAdmin::TestScriptEventAdminPort(R"json({"test":test})json") == std::nullopt);
CHECK(TestScriptAdmin::TestScriptEventAdminPort(R"json({ "test": 1 )json") == std::nullopt); // Missing closing }
CHECK(TestScriptAdmin::TestScriptEventAdminPort(R"json( "test": 1})json") == std::nullopt); // Missing opening {
CHECK(TestScriptAdmin::TestScriptEventAdminPort(R"json({ "test" = 1})json") == std::nullopt);
CHECK(TestScriptAdmin::TestScriptEventAdminPort(R"json({ "test": [ 1 })json") == std::nullopt); // Missing closing ]
CHECK(TestScriptAdmin::TestScriptEventAdminPort(R"json({ "test": 1 ] })json") == std::nullopt); // Missing opening [
CHECK(TestScriptEventAdminPort(R"json({"test":test})json") == std::nullopt);
CHECK(TestScriptEventAdminPort(R"json({ "test": 1 )json") == std::nullopt); // Missing closing }
CHECK(TestScriptEventAdminPort(R"json( "test": 1})json") == std::nullopt); // Missing opening {
CHECK(TestScriptEventAdminPort(R"json({ "test" = 1})json") == std::nullopt);
CHECK(TestScriptEventAdminPort(R"json({ "test": [ 1 })json") == std::nullopt); // Missing closing ]
CHECK(TestScriptEventAdminPort(R"json({ "test": 1 ] })json") == std::nullopt); // Missing opening [
}