1
0
forked from 0ad/0ad

Correctly serialize/deserialize user-defined JS objects.

Generalize component/AI serialization system to any user-defined JS
object. This includes Vector2D/3D, fixing an old issue.
As with components/AI, JS Objects may implement a Serialize/Deserialize
function to store custom data instead of the default, which attemps to
serialize all enumerable properties.

Fixes #4698

Differential Revision: https://code.wildfiregames.com/D2746
This was SVN commit r24462.
This commit is contained in:
wraitii 2020-12-27 17:18:13 +00:00
parent 1432343eeb
commit 9fc6c3c897
19 changed files with 316 additions and 165 deletions

View File

@ -3,7 +3,7 @@ function TestScript1_values() {}
TestScript1_values.prototype.Init = function() {
this.x = +this.template.x;
this.str = "this is a string";
this.things = { a: 1, b: "2", c: [3, "4", [5, []]] };
this.things = { "a": 1, "b": "2", "c": [3, "4", [5, []]] };
};
TestScript1_values.prototype.GetX = function() {
@ -22,11 +22,11 @@ TestScript1_entity.prototype.GetX = function() {
try {
delete this.entity;
Engine.TS_FAIL("Missed exception");
} catch (e) { }
} catch (e) { /* OK */ }
try {
this.entity = -1;
Engine.TS_FAIL("Missed exception");
} catch (e) { }
} catch (e) { /* OK */ }
// and return the value
return this.entity;
@ -40,7 +40,7 @@ function TestScript1_nontree() {}
TestScript1_nontree.prototype.Init = function() {
var n = [1];
this.x = [n, n, null, { y: n }];
this.x = [n, n, null, { "y": n }];
this.x[2] = this.x;
};
@ -61,7 +61,11 @@ TestScript1_custom.prototype.Init = function() {
};
TestScript1_custom.prototype.Serialize = function() {
return {c:1};
return { "c": 1 };
};
TestScript1_custom.prototype.Deserialize = function(data) {
this.c = data.c;
};
Engine.RegisterComponentType(IID_Test1, "TestScript1_custom", TestScript1_custom);
@ -72,7 +76,7 @@ function TestScript1_getter() {}
TestScript1_getter.prototype.Init = function() {
this.x = 100;
this.__defineGetter__('x', function () { print("FAIL\n"); die(); return 200; });
this.__defineGetter__('x', function() { print("FAIL\n"); die(); return 200; });
};
Engine.RegisterComponentType(IID_Test1, "TestScript1_getter", TestScript1_getter);

View File

@ -0,0 +1,9 @@
function test_serialization()
{
let test_val = new Vector2D(1, 2);
let rt = Engine.SerializationRoundTrip(test_val);
TS_ASSERT_EQUALS(test_val.constructor, rt.constructor);
TS_ASSERT_EQUALS(rt.add(test_val).x, 2);
}
test_serialization();

View File

@ -618,13 +618,12 @@ Trigger.prototype.InitDanubius = function()
this.fillShipsTimer = undefined;
// Be able to distinguish between the left and right riverside
// TODO: The Vector2D types don't survive deserialization, so use an object with x and y properties only!
let mapSize = TriggerHelper.GetMapSizeTerrain();
this.mapCenter = clone(new Vector2D(mapSize / 2, mapSize / 2));
this.mapCenter = new Vector2D(mapSize / 2, mapSize / 2);
this.riverDirection = clone(Vector2D.sub(
this.riverDirection = Vector2D.sub(
TriggerHelper.GetEntityPosition2D(this.GetTriggerPoints(triggerPointRiverDirection)[0]),
this.mapCenter));
this.mapCenter);
this.StartCelticRitual();
this.GarrisonAllGallicBuildings();

View File

@ -36,6 +36,12 @@ Player.prototype.Serialize = function()
return state;
};
Player.prototype.Deserialize = function(state)
{
for (let prop in state)
this[prop] = state[prop];
};
/**
* Which units will be shown with special icons at the top.
*/

View File

@ -48,7 +48,7 @@ function LoadMapSettings(settings)
}
let cmpEndGameManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_EndGameManager);
let gameSettings = { "victoryConditions": settings.VictoryConditions };
let gameSettings = { "victoryConditions": clone(settings.VictoryConditions) };
if (gameSettings.victoryConditions.indexOf("capture_the_relic") != -1)
{
gameSettings.relicCount = settings.RelicCount;

View File

@ -644,16 +644,16 @@ bool ScriptInterface::SetPropertyInt_(JS::HandleValue obj, int name, JS::HandleV
return JS_DefinePropertyById(rq.cx, object, id, value, attrs);
}
bool ScriptInterface::GetProperty(JS::HandleValue obj, const char* name, JS::MutableHandleValue out) const
{
return GetProperty_(obj, name, out);
}
bool ScriptInterface::GetProperty(JS::HandleValue obj, const char* name, JS::MutableHandleObject out) const
{
ScriptRequest rq(this);
return GetProperty(rq, obj, name, out);
}
bool ScriptInterface::GetProperty(const ScriptRequest& rq, JS::HandleValue obj, const char* name, JS::MutableHandleObject out)
{
JS::RootedValue val(rq.cx);
if (!GetProperty_(obj, name, &val))
if (!GetProperty(rq, obj, name, &val))
return false;
if (!val.isObject())
{
@ -665,14 +665,14 @@ bool ScriptInterface::GetProperty(JS::HandleValue obj, const char* name, JS::Mut
return true;
}
bool ScriptInterface::GetPropertyInt(JS::HandleValue obj, int name, JS::MutableHandleValue out) const
{
return GetPropertyInt_(obj, name, out);
}
bool ScriptInterface::GetProperty_(JS::HandleValue obj, const char* name, JS::MutableHandleValue out) const
bool ScriptInterface::GetProperty(JS::HandleValue obj, const char* name, JS::MutableHandleValue out) const
{
ScriptRequest rq(this);
return GetProperty(rq, obj, name, out);
}
bool ScriptInterface::GetProperty(const ScriptRequest& rq, JS::HandleValue obj, const char* name, JS::MutableHandleValue out)
{
if (!obj.isObject())
return false;
JS::RootedObject object(rq.cx, &obj.toObject());
@ -680,9 +680,14 @@ bool ScriptInterface::GetProperty_(JS::HandleValue obj, const char* name, JS::Mu
return JS_GetProperty(rq.cx, object, name, out);
}
bool ScriptInterface::GetPropertyInt_(JS::HandleValue obj, int name, JS::MutableHandleValue out) const
bool ScriptInterface::GetPropertyInt(JS::HandleValue obj, int name, JS::MutableHandleValue out) const
{
ScriptRequest rq(this);
return GetPropertyInt(rq,obj, name, out);
}
bool ScriptInterface::GetPropertyInt(const ScriptRequest& rq, JS::HandleValue obj, int name, JS::MutableHandleValue out)
{
JS::RootedId nameId(rq.cx, INT_TO_JSID(name));
if (!obj.isObject())
return false;

View File

@ -210,23 +210,26 @@ public:
*/
template<typename T>
bool GetProperty(JS::HandleValue obj, const char* name, T& out) const;
/**
* Get the named property of the given object.
*/
bool GetProperty(JS::HandleValue obj, const char* name, JS::MutableHandleValue out) const;
bool GetProperty(JS::HandleValue obj, const char* name, JS::MutableHandleObject out) const;
template<typename T>
static bool GetProperty(const ScriptRequest& rq, JS::HandleValue obj, const char* name, T& out);
static bool GetProperty(const ScriptRequest& rq, JS::HandleValue obj, const char* name, JS::MutableHandleValue out);
static bool GetProperty(const ScriptRequest& rq, JS::HandleValue obj, const char* name, JS::MutableHandleObject out);
/**
* Get the integer-named property on the given object.
*/
template<typename T>
bool GetPropertyInt(JS::HandleValue obj, int name, T& out) const;
/**
* Get the named property of the given object.
*/
bool GetPropertyInt(JS::HandleValue obj, int name, JS::MutableHandleValue out) const;
bool GetPropertyInt(JS::HandleValue obj, int name, JS::MutableHandleObject out) const;
template<typename T>
static bool GetPropertyInt(const ScriptRequest& rq, JS::HandleValue obj, int name, T& out);
static bool GetPropertyInt(const ScriptRequest& rq, JS::HandleValue obj, int name, JS::MutableHandleValue out);
static bool GetPropertyInt(const ScriptRequest& rq, JS::HandleValue obj, int name, JS::MutableHandleObject out);
/**
* Check the named property has been defined on the given object.
@ -444,8 +447,6 @@ private:
bool SetProperty_(JS::HandleValue obj, const char* name, JS::HandleValue value, bool constant, bool enumerate) const;
bool SetProperty_(JS::HandleValue obj, const wchar_t* name, JS::HandleValue value, bool constant, bool enumerate) const;
bool SetPropertyInt_(JS::HandleValue obj, int name, JS::HandleValue value, bool constant, bool enumerate) const;
bool GetProperty_(JS::HandleValue obj, const char* name, JS::MutableHandleValue out) const;
bool GetPropertyInt_(JS::HandleValue obj, int name, JS::MutableHandleValue value) const;
struct CustomType
{
@ -589,8 +590,14 @@ template<typename T>
bool ScriptInterface::GetProperty(JS::HandleValue obj, const char* name, T& out) const
{
ScriptRequest rq(this);
return GetProperty(rq, obj, name, out);
}
template<typename T>
bool ScriptInterface::GetProperty(const ScriptRequest& rq, JS::HandleValue obj, const char* name, T& out)
{
JS::RootedValue val(rq.cx);
if (!GetProperty_(obj, name, &val))
if (!GetProperty(rq, obj, name, &val))
return false;
return FromJSVal(rq, val, out);
}
@ -599,8 +606,14 @@ template<typename T>
bool ScriptInterface::GetPropertyInt(JS::HandleValue obj, int name, T& out) const
{
ScriptRequest rq(this);
return GetPropertyInt(rq, obj, name, out);
}
template<typename T>
bool ScriptInterface::GetPropertyInt(const ScriptRequest& rq, JS::HandleValue obj, int name, T& out)
{
JS::RootedValue val(rq.cx);
if (!GetPropertyInt_(obj, name, &val))
if (!GetPropertyInt(rq, obj, name, &val))
return false;
return FromJSVal(rq, val, out);
}

View File

@ -669,12 +669,7 @@ public:
serializer.Bool("useSharedScript", m_HasSharedComponent);
if (m_HasSharedComponent)
{
JS::RootedValue sharedData(rq.cx);
if (!m_ScriptInterface->CallFunction(m_SharedAIObj, "Serialize", &sharedData))
LOGERROR("AI shared script Serialize call failed");
serializer.ScriptVal("sharedData", &sharedData);
}
serializer.ScriptVal("sharedData", &m_SharedAIObj);
for (size_t i = 0; i < m_Players.size(); ++i)
{
serializer.String("name", m_Players[i]->m_AIName, 1, 256);
@ -690,19 +685,8 @@ public:
serializer.ScriptVal("command", &val);
}
bool hasCustomSerialize = m_ScriptInterface->HasProperty(m_Players[i]->m_Obj, "Serialize");
if (hasCustomSerialize)
{
JS::RootedValue scriptData(rq.cx);
if (!m_ScriptInterface->CallFunction(m_Players[i]->m_Obj, "Serialize", &scriptData))
LOGERROR("AI script Serialize call failed");
serializer.ScriptVal("data", &scriptData);
}
else
{
serializer.ScriptVal("data", &m_Players[i]->m_Obj);
}
}
// AI pathfinder
Serializer(serializer, "non pathfinding pass classes", m_NonPathfindingPassClasses);
@ -739,10 +723,7 @@ public:
if (m_HasSharedComponent)
{
TryLoadSharedComponent();
JS::RootedValue sharedData(rq.cx);
deserializer.ScriptVal("sharedData", &sharedData);
if (!m_ScriptInterface->CallFunctionVoid(m_SharedAIObj, "Deserialize", sharedData))
LOGERROR("AI shared script Deserialize call failed");
deserializer.ScriptObjectAssign("sharedData", m_SharedAIObj);
}
for (size_t i = 0; i < numAis; ++i)
@ -768,25 +749,7 @@ public:
m_Players.back()->m_Commands.push_back(m_ScriptInterface->WriteStructuredClone(val));
}
bool hasCustomDeserialize = m_ScriptInterface->HasProperty(m_Players.back()->m_Obj, "Deserialize");
if (hasCustomDeserialize)
{
JS::RootedValue scriptData(rq.cx);
deserializer.ScriptVal("data", &scriptData);
if (m_Players[i]->m_UseSharedComponent)
{
if (!m_ScriptInterface->CallFunctionVoid(m_Players.back()->m_Obj, "Deserialize", scriptData, m_SharedAIObj))
LOGERROR("AI script Deserialize call failed");
}
else if (!m_ScriptInterface->CallFunctionVoid(m_Players.back()->m_Obj, "Deserialize", scriptData))
{
LOGERROR("AI script deserialize() call failed");
}
}
else
{
deserializer.ScriptVal("data", &m_Players.back()->m_Obj);
}
deserializer.ScriptObjectAssign("data", m_Players.back()->m_Obj);
}
// AI pathfinder

View File

@ -1,4 +1,4 @@
/* Copyright (C) 2019 Wildfire Games.
/* Copyright (C) 2020 Wildfire Games.
* This file is part of 0 A.D.
*
* 0 A.D. is free software: you can redistribute it and/or modify
@ -16,6 +16,8 @@
*/
#include "simulation2/system/ComponentTest.h"
#include "simulation2/serialization/StdDeserializer.h"
#include "simulation2/serialization/StdSerializer.h"
#include "ps/Filesystem.h"
@ -58,6 +60,21 @@ public:
TS_ASSERT(componentManager->LoadScript(VfsPath(L"simulation/helpers") / pathname));
}
static JS::Value Script_SerializationRoundTrip(ScriptInterface::CmptPrivate* pCmptPrivate, JS::HandleValue value)
{
ScriptInterface& scriptInterface = *(pCmptPrivate->pScriptInterface);
ScriptRequest rq(scriptInterface);
JS::RootedValue val(rq.cx);
val = value;
std::stringstream stream;
CStdSerializer serializer(scriptInterface, stream);
serializer.ScriptVal("", &val);
CStdDeserializer deserializer(scriptInterface, stream);
deserializer.ScriptVal("", &val);
return val;
}
void test_global_scripts()
{
if (!VfsDirectoryExists(L"globalscripts/tests/"))
@ -73,6 +90,9 @@ public:
CSimContext context;
CComponentManager componentManager(context, g_ScriptContext, true);
ScriptTestSetup(componentManager.GetScriptInterface());
componentManager.GetScriptInterface().RegisterFunction<JS::Value, JS::HandleValue, Script_SerializationRoundTrip> ("SerializationRoundTrip");
load_script(componentManager.GetScriptInterface(), path);
}
}
@ -100,6 +120,7 @@ public:
componentManager.GetScriptInterface().RegisterFunction<void, VfsPath, Script_LoadComponentScript> ("LoadComponentScript");
componentManager.GetScriptInterface().RegisterFunction<void, VfsPath, Script_LoadHelperScript> ("LoadHelperScript");
componentManager.GetScriptInterface().RegisterFunction<JS::Value, JS::HandleValue, Script_SerializationRoundTrip> ("SerializationRoundTrip");
componentManager.LoadComponentTypes();

View File

@ -25,19 +25,6 @@
CComponentTypeScript::CComponentTypeScript(const ScriptInterface& scriptInterface, JS::HandleValue instance) :
m_ScriptInterface(scriptInterface), m_Instance(scriptInterface.GetGeneralJSContext(), instance)
{
// Cache the property detection for efficiency
ScriptRequest rq(m_ScriptInterface);
m_HasCustomSerialize = m_ScriptInterface.HasProperty(m_Instance, "Serialize");
m_HasCustomDeserialize = m_ScriptInterface.HasProperty(m_Instance, "Deserialize");
m_HasNullSerialize = false;
if (m_HasCustomSerialize)
{
JS::RootedValue val(rq.cx);
if (m_ScriptInterface.GetProperty(m_Instance, "Serialize", &val) && val.isNull())
m_HasNullSerialize = true;
}
}
void CComponentTypeScript::Init(const CParamNode& paramNode, entity_id_t ent)
@ -66,54 +53,18 @@ void CComponentTypeScript::HandleMessage(const CMessage& msg, bool global)
void CComponentTypeScript::Serialize(ISerializer& serialize)
{
// If the component set Serialize = null, then do no work here
if (m_HasNullSerialize)
return;
ScriptRequest rq(m_ScriptInterface);
// Support a custom "Serialize" function, which returns a new object that will be
// serialized instead of the component itself
if (m_HasCustomSerialize)
{
JS::RootedValue val(rq.cx);
if (!m_ScriptInterface.CallFunction(m_Instance, "Serialize", &val))
LOGERROR("Script Serialize call failed");
serialize.ScriptVal("object", &val);
}
else
{
serialize.ScriptVal("object", &m_Instance);
}
serialize.ScriptVal("comp", &m_Instance);
}
void CComponentTypeScript::Deserialize(const CParamNode& paramNode, IDeserializer& deserialize, entity_id_t ent)
{
ScriptRequest rq(m_ScriptInterface);
deserialize.ScriptObjectAssign("comp", m_Instance);
m_ScriptInterface.SetProperty(m_Instance, "entity", (int)ent, true, false);
m_ScriptInterface.SetProperty(m_Instance, "template", paramNode, true, false);
// Support a custom "Deserialize" function, to which we pass the deserialized data
// instead of automatically adding the deserialized properties onto the object
if (m_HasCustomDeserialize)
{
JS::RootedValue val(rq.cx);
// If Serialize = null, we'll still call Deserialize but with undefined argument
if (!m_HasNullSerialize)
deserialize.ScriptVal("object", &val);
if (!m_ScriptInterface.CallFunctionVoid(m_Instance, "Deserialize", val))
LOGERROR("Script Deserialize call failed");
}
else
{
if (!m_HasNullSerialize)
{
// Use ScriptObjectAppend so we don't lose the carefully-constructed
// prototype/parent of this object
deserialize.ScriptObjectAppend("object", m_Instance);
}
}
}

View File

@ -1,4 +1,4 @@
/* Copyright (C) 2017 Wildfire Games.
/* Copyright (C) 2020 Wildfire Games.
* This file is part of 0 A.D.
*
* 0 A.D. is free software: you can redistribute it and/or modify
@ -70,9 +70,6 @@ public:
private:
const ScriptInterface& m_ScriptInterface;
JS::PersistentRootedValue m_Instance;
bool m_HasCustomSerialize;
bool m_HasCustomDeserialize;
bool m_HasNullSerialize;
};
#endif // INCLUDED_SCRIPTCOMPONENT

View File

@ -219,8 +219,36 @@ void CBinarySerializerScriptImpl::HandleScriptVal(JS::HandleValue val)
if (protokey == JSProto_Object)
{
// Standard Object prototype
// Object class - check for user-defined prototype
JS::RootedObject proto(rq.cx);
if (!JS_GetPrototype(rq.cx, obj, &proto))
throw PSERROR_Serialize_ScriptError("JS_GetPrototype failed");
SPrototypeSerialization protoInfo = GetPrototypeInfo(rq, proto);
if (protoInfo.name == "Object")
m_Serializer.NumberU8_Unbounded("type", SCRIPT_TYPE_OBJECT);
else
{
m_Serializer.NumberU8_Unbounded("type", SCRIPT_TYPE_OBJECT_PROTOTYPE);
m_Serializer.String("proto", wstring_from_utf8(protoInfo.name), 0, 256);
// Does it have custom Serialize function?
// if so, we serialize the data it returns, rather than the object's properties directly
if (protoInfo.hasCustomSerialize)
{
// If serialize is null, don't serialize anything more
if (!protoInfo.hasNullSerialize)
{
JS::RootedValue data(rq.cx);
if (!m_ScriptInterface.CallFunction(val, "Serialize", &data))
throw PSERROR_Serialize_ScriptError("Prototype Serialize function failed");
m_Serializer.ScriptVal("data", &data);
}
// Break here to skip the custom object property serialization logic below.
break;
}
}
}
else if (protokey == JSProto_Number)
{
@ -438,7 +466,14 @@ i32 CBinarySerializerScriptImpl::GetScriptBackrefTag(JS::HandleObject obj)
}
tagValue = JS::Int32Value(m_ScriptBackrefsNext);
JS_SetPropertyById(rq.cx, obj, symbolId, tagValue);
// TODO: this fails if the object cannot be written to.
// This means we could end up in an infinite loop...
if (!JS_DefinePropertyById(rq.cx, obj, symbolId, tagValue, JSPROP_READONLY))
{
// For now just warn, this should be user-fixable and may not actually error out.
JS::RootedValue objVal(rq.cx, JS::ObjectValue(*obj.get()));
LOGWARNING("Serialization symbol cannot be written on object %s", m_ScriptInterface.ToString(&objVal));
}
++m_ScriptBackrefsNext;
// Return a non-tag number so callers know they need to serialize the object

View File

@ -1,4 +1,4 @@
/* Copyright (C) 2017 Wildfire Games.
/* Copyright (C) 2020 Wildfire Games.
* This file is part of 0 A.D.
*
* 0 A.D. is free software: you can redistribute it and/or modify
@ -149,9 +149,23 @@ void CDebugSerializer::PutString(const char* name, const std::string& value)
void CDebugSerializer::PutScriptVal(const char* name, JS::MutableHandleValue value)
{
std::string source = m_ScriptInterface.ToString(value, true);
ScriptRequest rq(m_ScriptInterface);
JS::RootedValue serialize(rq.cx);
if (m_ScriptInterface.GetProperty(value, "Serialize", &serialize) && !serialize.isNullOrUndefined())
{
// If the value has a Serialize property, pretty-parse that and return the value as a raw string.
// This gives more debug data for components in case of OOS.
m_ScriptInterface.CallFunction(value, "Serialize", &serialize);
std::string serialized_source = m_ScriptInterface.ToString(&serialize, true);
std::string source = m_ScriptInterface.ToString(value, false);
m_Stream << INDENT << name << ": " << serialized_source << " (raw: " << source << ")\n";
}
else
{
std::string source = m_ScriptInterface.ToString(value, true);
m_Stream << INDENT << name << ": " << source << "\n";
}
}
void CDebugSerializer::PutRaw(const char* name, const u8* data, size_t len)

View File

@ -1,4 +1,4 @@
/* Copyright (C) 2017 Wildfire Games.
/* Copyright (C) 2020 Wildfire Games.
* This file is part of 0 A.D.
*
* 0 A.D. is free software: you can redistribute it and/or modify
@ -58,8 +58,11 @@ public:
/// Deserialize a JS::Value, replacing 'out'
virtual void ScriptVal(const char* name, JS::MutableHandleValue out) = 0;
/// Deserialize an object value, appending properties to object 'objVal'
virtual void ScriptObjectAppend(const char* name, JS::HandleValue objVal) = 0;
/**
* Deserialize an object and assign its properties to objVal
* (Essentially equivalent to Object.assign(objVal, serialized))
*/
virtual void ScriptObjectAssign(const char* name, JS::HandleValue objVal) = 0;
/// Deserialize a JSString
virtual void ScriptString(const char* name, JS::MutableHandleString out) = 0;

View File

@ -31,7 +31,7 @@ enum
SCRIPT_TYPE_BACKREF = 8,
SCRIPT_TYPE_TYPED_ARRAY = 9, // ArrayBufferView subclasses - see below
SCRIPT_TYPE_ARRAY_BUFFER = 10, // ArrayBuffer containing actual typed array data (may be shared by multiple views)
SCRIPT_TYPE_OBJECT_PROTOTYPE = 11, // user-defined prototype - currently unused
SCRIPT_TYPE_OBJECT_PROTOTYPE = 11, // User-defined prototype - see GetPrototypeInfo
SCRIPT_TYPE_OBJECT_NUMBER = 12, // standard Number class
SCRIPT_TYPE_OBJECT_STRING = 13, // standard String class
SCRIPT_TYPE_OBJECT_BOOLEAN = 14, // standard Boolean class
@ -53,4 +53,42 @@ enum
SCRIPT_TYPED_ARRAY_UINT8_CLAMPED = 8
};
struct SPrototypeSerialization
{
std::string name = "";
bool hasCustomSerialize = false;
bool hasCustomDeserialize = false;
bool hasNullSerialize = false;
};
inline SPrototypeSerialization GetPrototypeInfo(const ScriptRequest& rq, JS::HandleObject prototype)
{
SPrototypeSerialization ret;
JS::RootedValue constructor(rq.cx, JS::ObjectOrNullValue(JS_GetConstructor(rq.cx, prototype)));
if (!ScriptInterface::GetProperty(rq, constructor, "name", ret.name))
throw PSERROR_Serialize_ScriptError("Could not get constructor name.");
// Nothing to do for basic Object objects.
if (ret.name == "Object")
return ret;
if (!JS_HasProperty(rq.cx, prototype, "Serialize", &ret.hasCustomSerialize) ||
!JS_HasProperty(rq.cx, prototype, "Deserialize", &ret.hasCustomDeserialize))
throw PSERROR_Serialize_ScriptError("JS_HasProperty failed");
if (ret.hasCustomSerialize)
{
JS::RootedValue serialize(rq.cx);
if (!JS_GetProperty(rq.cx, prototype, "Serialize", &serialize))
throw PSERROR_Serialize_ScriptError("JS_GetProperty failed");
if (serialize.isNull())
ret.hasNullSerialize = true;
else if (!ret.hasCustomDeserialize)
throw PSERROR_Serialize_ScriptError("Cannot serialize script with non-null Serialize but no Deserialize.");
}
return ret;
}
#endif // INCLUDED_SERIALIZEDSCRIPTTYPES

View File

@ -19,13 +19,13 @@
#include "StdDeserializer.h"
#include "SerializedScriptTypes.h"
#include "StdSerializer.h" // for DEBUG_SERIALIZER_ANNOTATE
#include "lib/byte_order.h"
#include "ps/CStr.h"
#include "scriptinterface/ScriptInterface.h"
#include "scriptinterface/ScriptExtraHeaders.h" // For typed arrays and ArrayBuffer
#include "lib/byte_order.h"
#include "simulation2/serialization/ISerializer.h"
#include "simulation2/serialization/SerializedScriptTypes.h"
#include "simulation2/serialization/StdSerializer.h" // for DEBUG_SERIALIZER_ANNOTATE
CStdDeserializer::CStdDeserializer(const ScriptInterface& scriptInterface, std::istream& stream) :
m_ScriptInterface(scriptInterface), m_Stream(stream)
@ -110,7 +110,7 @@ void CStdDeserializer::GetScriptBackref(size_t tag, JS::MutableHandleObject ret)
////////////////////////////////////////////////////////////////
JS::Value CStdDeserializer::ReadScriptVal(const char* UNUSED(name), JS::HandleObject appendParent)
JS::Value CStdDeserializer::ReadScriptVal(const char* UNUSED(name), JS::HandleObject preexistingObject)
{
ScriptRequest rq(m_ScriptInterface);
@ -126,22 +126,69 @@ JS::Value CStdDeserializer::ReadScriptVal(const char* UNUSED(name), JS::HandleOb
case SCRIPT_TYPE_ARRAY:
case SCRIPT_TYPE_OBJECT:
case SCRIPT_TYPE_OBJECT_PROTOTYPE:
{
JS::RootedObject obj(rq.cx);
if (appendParent)
{
obj.set(appendParent);
}
else if (type == SCRIPT_TYPE_ARRAY)
if (type == SCRIPT_TYPE_ARRAY)
{
u32 length;
NumberU32_Unbounded("array length", length);
obj.set(JS::NewArrayObject(rq.cx, length));
}
else // SCRIPT_TYPE_OBJECT
else if (type == SCRIPT_TYPE_OBJECT)
{
obj.set(JS_NewPlainObject(rq.cx));
}
else // SCRIPT_TYPE_OBJECT_PROTOTYPE
{
CStrW prototypeName;
String("proto", prototypeName, 0, 256);
// If an object was passed, no need to construct a new one.
if (preexistingObject != nullptr)
obj.set(preexistingObject);
else
{
JS::RootedValue constructor(rq.cx);
if (!ScriptInterface::GetGlobalProperty(rq, prototypeName.ToUTF8(), &constructor))
throw PSERROR_Deserialize_ScriptError("Deserializer failed to get constructor object");
JS::RootedObject newObj(rq.cx);
if (!JS::Construct(rq.cx, constructor, JS::HandleValueArray::empty(), &newObj))
throw PSERROR_Deserialize_ScriptError("Deserializer failed to construct object");
obj.set(newObj);
}
JS::RootedObject prototype(rq.cx);
JS_GetPrototype(rq.cx, obj, &prototype);
SPrototypeSerialization info = GetPrototypeInfo(rq, prototype);
if (preexistingObject != nullptr && prototypeName != wstring_from_utf8(info.name))
throw PSERROR_Deserialize_ScriptError("Deserializer failed: incorrect pre-existing object");
if (info.hasCustomDeserialize)
{
AddScriptBackref(obj);
// If Serialize is null, we'll still call Deserialize but with undefined argument
JS::RootedValue data(rq.cx);
if (!info.hasNullSerialize)
ScriptVal("data", &data);
JS::RootedValue objVal(rq.cx, JS::ObjectValue(*obj));
m_ScriptInterface.CallFunctionVoid(objVal, "Deserialize", data);
return JS::ObjectValue(*obj);
}
else if (info.hasNullSerialize)
{
// If we serialized null, this means we're pretty much a default-constructed object.
// Nothing to do.
AddScriptBackref(obj);
return JS::ObjectValue(*obj);
}
}
if (!obj)
throw PSERROR_Deserialize_ScriptError("Deserializer failed to create new object");
@ -431,7 +478,7 @@ void CStdDeserializer::ScriptVal(const char* name, JS::MutableHandleValue out)
out.set(ReadScriptVal(name, nullptr));
}
void CStdDeserializer::ScriptObjectAppend(const char* name, JS::HandleValue objVal)
void CStdDeserializer::ScriptObjectAssign(const char* name, JS::HandleValue objVal)
{
ScriptRequest rq(m_ScriptInterface);

View File

@ -33,7 +33,7 @@ public:
virtual ~CStdDeserializer();
virtual void ScriptVal(const char* name, JS::MutableHandleValue out);
virtual void ScriptObjectAppend(const char* name, JS::HandleValue objVal);
virtual void ScriptObjectAssign(const char* name, JS::HandleValue objVal);
virtual void ScriptString(const char* name, JS::MutableHandleString out);
virtual std::istream& GetStream();
@ -47,7 +47,7 @@ protected:
virtual void Get(const char* name, u8* data, size_t len);
private:
JS::Value ReadScriptVal(const char* name, JS::HandleObject appendParent);
JS::Value ReadScriptVal(const char* name, JS::HandleObject preexistingObject);
void ReadStringLatin1(const char* name, std::vector<JS::Latin1Char>& str);
void ReadStringUTF16(const char* name, utf16string& str);

View File

@ -1,4 +1,4 @@
/* Copyright (C) 2019 Wildfire Games.
/* Copyright (C) 2020 Wildfire Games.
* This file is part of 0 A.D.
*
* 0 A.D. is free software: you can redistribute it and/or modify
@ -773,7 +773,7 @@ public:
entities:\n\
- id: 1\n\
TestScript1_values:\n\
object: {\n\
comp: {\n\
\"x\": 1234,\n\
\"str\": \"this is a string\",\n\
\"things\": {\n\
@ -792,17 +792,17 @@ entities:\n\
\n\
- id: 2\n\
TestScript1_entity:\n\
object: {}\n\
comp: {}\n\
\n\
- id: 3\n\
TestScript1_nontree:\n\
object: ({x:[[2], [2], [], {y:[2]}]})\n\
comp: ({x:[[2], [2], [], {y:[2]}]})\n\
\n\
- id: 4\n\
TestScript1_custom:\n\
object: {\n\
comp: {\n\
\"c\": 1\n\
}\n\
} (raw: ({y:2}))\n\
\n"
);

View File

@ -324,6 +324,14 @@ public:
TSM_ASSERT(msg, !stream.bad() && !stream.fail());
TSM_ASSERT_EQUALS(msg, stream.peek(), EOF);
std::stringstream stream2;
CStdSerializer serialize2(script, stream2);
CStdDeserializer deserialize2(script, stream2);
// Round-trip the deserialized value again. This helps ensure prototypes are correctly deserialized.
serialize2.ScriptVal("script2", &newobj);
deserialize2.ScriptVal("script2", &newobj);
std::string source;
TSM_ASSERT(msg, script.CallFunction(newobj, "toSource", source));
TS_ASSERT_STR_EQUALS(source, expected);
@ -411,6 +419,44 @@ public:
helper_script_roundtrip("Boolean with props", "var b=new Boolean('true'); b.foo='bar'; b", "(new Boolean(true))");
}
void test_script_fancy_objects()
{
// This asserts that objects are deserialized with their correct prototypes.
helper_script_roundtrip("Custom Object", ""
"function customObj() { this.a = this.customFunc.name; };"
"customObj.prototype.customFunc = function customFunc(){};"
"new customObj();", "({a:\"customFunc\"})");
helper_script_roundtrip("Custom Class", ""
"class customObj {"
" constructor() { this.a = this.customFunc.name; }"
" customFunc(){};"
"}; new customObj();", "({a:\"customFunc\"})");
helper_script_roundtrip("Custom Class with Serialize/Deserialize()", ""
"class customObj {"
" constructor() { this.a = this.customFunc.name; }"
" Serialize() { return { 'foo': 'bar' }; }"
" Deserialize(data) { this.foo = data.foo; }"
" customFunc(){};"
"}; new customObj();", "({a:\"customFunc\", foo:\"bar\"})");
helper_script_roundtrip("Custom Class with null serialize & deserialize()", ""
"class customObj {"
" constructor() { this.a = this.customFunc.name; }"
" Deserialize(data) { this.test = 'test'; };"
" customFunc(){};"
"}; customObj.prototype.Serialize=null;"
"new customObj();", "({a:\"customFunc\", test:\"test\"})");
helper_script_roundtrip("Custom Class with arguments but still works", ""
"class customObj {"
" constructor(test) { this.a = test; }"
" Serialize() { return { 'data': this.a }; };"
" Deserialize(data) { this.a = data.data; };"
"}; new customObj(4);", "({a:4})");
}
void test_script_objects_properties()
{
helper_script_roundtrip("Object with null in prop name", "({\"foo\\0bar\":1})", "({\'foo\\x00bar\':1})");