# Automatic runtime validation of entity template files.

Fixes #413.

This was SVN commit r7455.
This commit is contained in:
Ykkrosh 2010-04-14 17:22:32 +00:00
parent f9195d8a29
commit 3117f52d7c
22 changed files with 296 additions and 19 deletions

View File

@ -1,5 +1,7 @@
function TestScript2A() {}
TestScript2A.prototype.Schema = "<ref name='anything'/>";
TestScript2A.prototype.Init = function() {
this.x = eval(this.template.y);
};

View File

@ -1,5 +1,7 @@
function HotloadA() {}
HotloadA.prototype.Schema = "<ref name='anything'/>";
HotloadA.prototype.Init = function() {
this.x = +this.template.x;
};

View File

@ -1,5 +1,7 @@
function HotloadA() {}
HotloadA.prototype.Schema = "<ref name='anything'/>";
HotloadA.prototype.Init = function() {
this.x = +this.template.x;
};

View File

@ -61,6 +61,8 @@ Engine.RegisterComponentType(IID_Test1, "TestScript1_getter", TestScript1_getter
function TestScript1_consts() {}
TestScript1_consts.prototype.Schema = "<ref name='anything'/>";
TestScript1_consts.prototype.GetX = function() {
return (+this.entity) + (+this.template.x);
};

View File

@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<Entity>
<x>12345</x>
<Test1A>12345</Test1A>
</Entity>

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
bogus
<Entity>
<Test1A>12345</Test1A>
</Entity>

View File

@ -1,8 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<Entity>
<x a="a1" b="b1" c="c1">
<Test1A a="a1" b="b1" c="c1">
<d>d1</d>
<e>e1</e>
<f>f1</f>
</x>
</Test1A>
</Entity>

View File

@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<Entity parent="inherit1">
<x a="a2">
<Test1A a="a2">
<d>d2</d>
<g>g2</g>
</x>
</Test1A>
</Entity>

View File

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<Entity>
<Test1B>12345</Test1B>
</Entity>

View File

@ -12,6 +12,7 @@
<UnitMotion/>
<Footprint>
<Circle radius="4"/>
<Height>1.0</Height>
</Footprint>
<Obstruction/>
</Entity>

98
source/ps/XML/RelaxNG.cpp Normal file
View File

@ -0,0 +1,98 @@
/* Copyright (C) 2010 Wildfire Games.
* This file is part of 0 A.D.
*
* 0 A.D. is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 of the License, or
* (at your option) any later version.
*
* 0 A.D. is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with 0 A.D. If not, see <http://www.gnu.org/licenses/>.
*/
#include "precompiled.h"
#include "RelaxNG.h"
#include "lib/timer.h"
#include "lib/utf8.h"
#include "ps/CLogger.h"
#include <libxml/relaxng.h>
TIMER_ADD_CLIENT(xml_validation);
RelaxNGValidator::RelaxNGValidator() :
m_Schema(NULL)
{
}
RelaxNGValidator::~RelaxNGValidator()
{
if (m_Schema)
xmlRelaxNGFree(m_Schema);
}
bool RelaxNGValidator::LoadGrammar(const std::string& grammar)
{
TIMER_ACCRUE(xml_validation);
debug_assert(m_Schema == NULL);
xmlRelaxNGParserCtxtPtr ctxt = xmlRelaxNGNewMemParserCtxt(grammar.c_str(), grammar.size());
m_Schema = xmlRelaxNGParse(ctxt);
xmlRelaxNGFreeParserCtxt(ctxt);
if (m_Schema == NULL)
{
LOGERROR(L"RelaxNGValidator: Failed to compile schema");
return false;
}
return true;
}
bool RelaxNGValidator::Validate(const std::wstring& filename, const std::wstring& document)
{
TIMER_ACCRUE(xml_validation);
if (!m_Schema)
{
LOGERROR(L"RelaxNGValidator: No grammar loaded");
return false;
}
std::string docutf8 = "<?xml version='1.0' encoding='utf-8'?>" + utf8_from_wstring(document);
xmlDocPtr doc = xmlReadMemory(docutf8.c_str(), docutf8.size(), utf8_from_wstring(filename).c_str(), NULL, XML_PARSE_NONET);
if (doc == NULL)
{
LOGERROR(L"RelaxNGValidator: Failed to parse document");
return false;
}
xmlRelaxNGValidCtxtPtr ctxt = xmlRelaxNGNewValidCtxt(m_Schema);
int ret = xmlRelaxNGValidateDoc(ctxt, doc);
xmlRelaxNGFreeValidCtxt(ctxt);
xmlFreeDoc(doc);
if (ret == 0)
{
return true;
}
else if (ret > 0)
{
LOGERROR(L"RelaxNGValidator: Validation failed");
return false;
}
else
{
LOGERROR(L"RelaxNGValidator: Internal error %d", ret);
return false;
}
}

38
source/ps/XML/RelaxNG.h Normal file
View File

@ -0,0 +1,38 @@
/* Copyright (C) 2010 Wildfire Games.
* This file is part of 0 A.D.
*
* 0 A.D. is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 of the License, or
* (at your option) any later version.
*
* 0 A.D. is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with 0 A.D. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef INCLUDED_RELAXNG
#define INCLUDED_RELAXNG
typedef struct _xmlRelaxNG xmlRelaxNG;
typedef xmlRelaxNG *xmlRelaxNGPtr;
class RelaxNGValidator
{
public:
RelaxNGValidator();
~RelaxNGValidator();
bool LoadGrammar(const std::string& grammar);
bool Validate(const std::wstring& filename, const std::wstring& document);
private:
xmlRelaxNGPtr m_Schema;
};
#endif // INCLUDED_RELAXNG

View File

@ -33,9 +33,14 @@
static void errorHandler(void* UNUSED(userData), xmlErrorPtr error)
{
// Strip a trailing newline
std::string message = error->message;
if (message.length() > 0 && message[message.length()-1] == '\n')
message.erase(message.length()-1);
LOG(CLogger::Error, LOG_CATEGORY, L"CXeromyces: Parse %ls: %hs:%d: %hs",
error->level == XML_ERR_WARNING ? L"warning" : L"error",
error->file, error->line, error->message);
error->file, error->line, message.c_str());
// TODO: The (non-fatal) warnings and errors don't get stored in the XMB,
// so the caching is less transparent than it should be
}

View File

@ -0,0 +1,89 @@
/* Copyright (C) 2010 Wildfire Games.
* This file is part of 0 A.D.
*
* 0 A.D. is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 of the License, or
* (at your option) any later version.
*
* 0 A.D. is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with 0 A.D. If not, see <http://www.gnu.org/licenses/>.
*/
#include "lib/self_test.h"
#include "ps/CLogger.h"
#include "ps/XML/Xeromyces.h"
#include "ps/XML/RelaxNG.h"
class TestRelaxNG : public CxxTest::TestSuite
{
public:
void setUp()
{
CXeromyces::Startup();
}
void tearDown()
{
CXeromyces::Terminate();
}
void test_basic()
{
RelaxNGValidator v;
TS_ASSERT(v.LoadGrammar("<element xmlns='http://relaxng.org/ns/structure/1.0' name='test'><empty/></element>"));
TS_ASSERT(v.Validate(L"doc", L"<test/>"));
{
TestLogger logger;
TS_ASSERT(!v.Validate(L"doc", L"<bogus/>"));
TS_ASSERT_WSTR_CONTAINS(logger.GetOutput(), L"Parse error: doc:1: Expecting element test, got bogus");
}
{
TestLogger logger;
TS_ASSERT(!v.Validate(L"doc", L"bogus"));
TS_ASSERT_WSTR_CONTAINS(logger.GetOutput(), L"RelaxNGValidator: Failed to parse document");
}
TS_ASSERT(v.Validate(L"doc", L"<test/>"));
}
void test_interleave()
{
RelaxNGValidator v;
TS_ASSERT(v.LoadGrammar("<element xmlns='http://relaxng.org/ns/structure/1.0' name='test'><interleave><empty/></interleave></element>"));
// This currently (libxml2-2.7.7) leaks memory and makes Valgrind complain - see https://bugzilla.gnome.org/show_bug.cgi?id=615767
}
void test_datatypes()
{
RelaxNGValidator v;
TS_ASSERT(v.LoadGrammar("<element xmlns='http://relaxng.org/ns/structure/1.0' datatypeLibrary='http://www.w3.org/2001/XMLSchema-datatypes' name='test'><data type='decimal'><param name='minInclusive'>1.5</param></data></element>"));
TS_ASSERT(v.Validate(L"doc", L"<test>2.0</test>"));
TestLogger logger;
TS_ASSERT(!v.Validate(L"doc", L"<test>x</test>"));
TS_ASSERT(!v.Validate(L"doc", L"<test>1.0</test>"));
}
void test_broken_grammar()
{
RelaxNGValidator v;
TestLogger logger;
TS_ASSERT(!v.LoadGrammar("whoops"));
TS_ASSERT_WSTR_CONTAINS(logger.GetOutput(), L"RelaxNGValidator: Failed to compile schema");
TS_ASSERT(!v.Validate(L"doc", L"<test/>"));
}
};

View File

@ -39,7 +39,6 @@ public:
CSimulation2Impl(CUnitManager* unitManager, CTerrain* terrain) :
m_SimContext(), m_ComponentManager(m_SimContext)
{
m_SimContext.m_ComponentManager = &m_ComponentManager;
m_SimContext.m_UnitManager = unitManager;
m_SimContext.m_Terrain = terrain;
m_ComponentManager.LoadComponentTypes();

View File

@ -25,6 +25,7 @@
#include "lib/utf8.h"
#include "ps/CLogger.h"
#include "ps/Filesystem.h"
#include "ps/XML/RelaxNG.h"
#include "ps/XML/Xeromyces.h"
static const wchar_t TEMPLATE_ROOT[] = L"simulation/templates/";
@ -40,8 +41,11 @@ public:
DEFAULT_COMPONENT_ALLOCATOR(TemplateManager)
virtual void Init(const CSimContext& UNUSED(context), const CParamNode& UNUSED(paramNode))
virtual void Init(const CSimContext& context, const CParamNode& UNUSED(paramNode))
{
m_Validator.LoadGrammar(context.GetComponentManager().GenerateSchema());
// TODO: handle errors loading the grammar here?
// TODO: support hotloading changes to the grammar
}
virtual void Deinit(const CSimContext& UNUSED(context))
@ -63,8 +67,10 @@ public:
// template data before other components (like the tech components) have been deserialized
}
virtual void Deserialize(const CSimContext& UNUSED(context), const CParamNode& UNUSED(paramNode), IDeserializer& deserialize)
virtual void Deserialize(const CSimContext& context, const CParamNode& paramNode, IDeserializer& deserialize)
{
Init(context, paramNode);
u32 numEntities;
deserialize.NumberU32_Unbounded(numEntities);
for (u32 i = 0; i < numEntities; ++i)
@ -104,12 +110,20 @@ public:
virtual std::vector<std::wstring> FindAllTemplates();
private:
// Entity template XML validator
RelaxNGValidator m_Validator;
// Map from template name (XML filename or special |-separated string) to the most recently
// loaded valid template data.
// (Failed loads won't remove valid entries under the same name, so we behave more nicely
// loaded non-broken template data. This includes files that will fail schema validation.
// (Failed loads won't remove existing entries under the same name, so we behave more nicely
// when hotloading broken files)
std::map<std::wstring, CParamNode> m_TemplateFileData;
// Map from template name to schema validation status.
// (Some files, e.g. inherited parent templates, may not be valid themselves but we still need to load
// them and use them; we only reject invalid templates that were requested directly by GetTemplate/etc)
std::map<std::wstring, bool> m_TemplateSchemaValidity;
// Remember the template used by each entity, so we can return them
// again for deserialization.
// TODO: should store player ID etc.
@ -157,6 +171,13 @@ const CParamNode* CCmpTemplateManager::GetTemplate(std::wstring templateName)
return NULL;
}
// Compute validity, if it's not computed before
if (m_TemplateSchemaValidity.find(templateName) == m_TemplateSchemaValidity.end())
m_TemplateSchemaValidity[templateName] = m_Validator.Validate(templateName, m_TemplateFileData[templateName].ToXML());
// Refuse to return invalid templates
if (!m_TemplateSchemaValidity[templateName])
return NULL;
const CParamNode& templateRoot = m_TemplateFileData[templateName].GetChild("Entity");
if (!templateRoot.IsOk())
{

View File

@ -36,6 +36,11 @@ public:
int32_t m_x;
static std::string GetSchema()
{
return "<ref name='anything'/>";
}
virtual void Init(const CSimContext&, const CParamNode& paramNode)
{
if (paramNode.GetChild("x").IsOk())

View File

@ -59,7 +59,6 @@ public:
{
CSimContext context;
CComponentManager componentManager(context, true);
context.SetComponentManager(&componentManager);
ScriptTestSetup(componentManager.GetScriptInterface());

View File

@ -29,9 +29,11 @@
#include "ps/CLogger.h"
#include "ps/Filesystem.h"
CComponentManager::CComponentManager(const CSimContext& context, bool skipScriptFunctions) :
CComponentManager::CComponentManager(CSimContext& context, bool skipScriptFunctions) :
m_NextScriptComponentTypeId(CID__LastNative), m_ScriptInterface("Engine"), m_SimContext(context), m_CurrentlyHotloading(false)
{
context.SetComponentManager(this);
m_ScriptInterface.SetCallbackData(static_cast<void*> (this));
// For component script tests, the test system sets up its own scripted implementation of

View File

@ -68,7 +68,7 @@ private:
};
public:
CComponentManager(const CSimContext&, bool skipScriptFunctions = false);
CComponentManager(CSimContext&, bool skipScriptFunctions = false);
~CComponentManager();
void LoadComponentTypes();

View File

@ -55,7 +55,6 @@ public:
ComponentTestHelper() :
m_Context(), m_ComponentManager(m_Context), m_Cmp(NULL)
{
m_Context.SetComponentManager(&m_ComponentManager);
m_ComponentManager.LoadComponentTypes();
}

View File

@ -61,15 +61,15 @@ public:
const CParamNode* basic = tempMan->LoadTemplate(ent2, L"basic", -1);
TS_ASSERT(basic != NULL);
TS_ASSERT_WSTR_EQUALS(basic->ToXML(), L"<x>12345</x>");
TS_ASSERT_WSTR_EQUALS(basic->ToXML(), L"<Test1A>12345</Test1A>");
const CParamNode* inherit2 = tempMan->LoadTemplate(ent2, L"inherit2", -1);
TS_ASSERT(inherit2 != NULL);
TS_ASSERT_WSTR_EQUALS(inherit2->ToXML(), L"<x a=\"a2\" b=\"b1\" c=\"c1\"><d>d2</d><e>e1</e><f>f1</f><g>g2</g></x>");
TS_ASSERT_WSTR_EQUALS(inherit2->ToXML(), L"<Test1A a=\"a2\" b=\"b1\" c=\"c1\"><d>d2</d><e>e1</e><f>f1</f><g>g2</g></Test1A>");
const CParamNode* inherit1 = tempMan->LoadTemplate(ent2, L"inherit1", -1);
TS_ASSERT(inherit1 != NULL);
TS_ASSERT_WSTR_EQUALS(inherit1->ToXML(), L"<x a=\"a1\" b=\"b1\" c=\"c1\"><d>d1</d><e>e1</e><f>f1</f></x>");
TS_ASSERT_WSTR_EQUALS(inherit1->ToXML(), L"<Test1A a=\"a1\" b=\"b1\" c=\"c1\"><d>d1</d><e>e1</e><f>f1</f></Test1A>");
const CParamNode* actor = tempMan->LoadTemplate(ent2, L"actor|example1", -1);
TS_ASSERT(actor != NULL);
@ -81,7 +81,7 @@ public:
const CParamNode* previewobstruct = tempMan->LoadTemplate(ent2, L"preview|unitobstruct", -1);
TS_ASSERT(previewobstruct != NULL);
TS_ASSERT_WSTR_EQUALS(previewobstruct->ToXML(), L"<Footprint><Circle radius=\"4\"></Circle></Footprint><Obstruction><Inactive></Inactive></Obstruction><Position><Altitude>0</Altitude><Anchor>upright</Anchor><Floating>false</Floating></Position><VisualActor><Actor>example</Actor></VisualActor>");
TS_ASSERT_WSTR_EQUALS(previewobstruct->ToXML(), L"<Footprint><Circle radius=\"4\"></Circle><Height>1.0</Height></Footprint><Obstruction><Inactive></Inactive></Obstruction><Position><Altitude>0</Altitude><Anchor>upright</Anchor><Floating>false</Floating></Position><VisualActor><Actor>example</Actor></VisualActor>");
const CParamNode* previewactor = tempMan->LoadTemplate(ent2, L"preview|actor|example2", -1);
TS_ASSERT(previewactor != NULL);
@ -104,6 +104,10 @@ public:
TestLogger logger;
TS_ASSERT(tempMan->LoadTemplate(ent2, L"illformed", -1) == NULL);
TS_ASSERT(tempMan->LoadTemplate(ent2, L"invalid", -1) == NULL);
TS_ASSERT(tempMan->LoadTemplate(ent2, L"nonexistent", -1) == NULL);
TS_ASSERT(tempMan->LoadTemplate(ent2, L"inherit-loop", -1) == NULL);