Line data Source code
1 : /* Copyright (C) 2021 Wildfire Games.
2 : * This file is part of 0 A.D.
3 : *
4 : * 0 A.D. is free software: you can redistribute it and/or modify
5 : * it under the terms of the GNU General Public License as published by
6 : * the Free Software Foundation, either version 2 of the License, or
7 : * (at your option) any later version.
8 : *
9 : * 0 A.D. is distributed in the hope that it will be useful,
10 : * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 : * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 : * GNU General Public License for more details.
13 : *
14 : * You should have received a copy of the GNU General Public License
15 : * along with 0 A.D. If not, see <http://www.gnu.org/licenses/>.
16 : */
17 :
18 : #include "precompiled.h"
19 :
20 : #include "RelaxNG.h"
21 :
22 : #include "lib/timer.h"
23 : #include "lib/utf8.h"
24 : #include "ps/CLogger.h"
25 : #include "ps/CStr.h"
26 : #include "ps/Filesystem.h"
27 :
28 : #include <libxml/relaxng.h>
29 : #include <map>
30 : #include <mutex>
31 :
32 1 : TIMER_ADD_CLIENT(xml_validation);
33 :
34 : /*
35 : * libxml2 leaks memory when parsing schemas: https://bugzilla.gnome.org/show_bug.cgi?id=615767
36 : * To minimise that problem, keep a global cache of parsed schemas, so we don't
37 : * leak an indefinitely large amount of memory when repeatedly restarting the simulation.
38 : */
39 : class RelaxNGSchema;
40 1 : static std::map<std::string, std::shared_ptr<RelaxNGSchema>> g_SchemaCache;
41 : static std::mutex g_SchemaCacheLock;
42 :
43 90 : void ClearSchemaCache()
44 : {
45 180 : std::lock_guard<std::mutex> lock(g_SchemaCacheLock);
46 90 : g_SchemaCache.clear();
47 90 : }
48 :
49 15 : static void relaxNGErrorHandler(void* UNUSED(userData), xmlErrorPtr error)
50 : {
51 : // Strip a trailing newline
52 30 : std::string message = error->message;
53 15 : if (message.length() > 0 && message[message.length()-1] == '\n')
54 15 : message.erase(message.length()-1);
55 :
56 15 : LOGERROR("RelaxNGValidator: Validation %s: %s:%d: %s",
57 : error->level == XML_ERR_WARNING ? "warning" : "error",
58 : error->file, error->line, message.c_str());
59 15 : }
60 :
61 : class RelaxNGSchema
62 : {
63 : public:
64 : xmlRelaxNGPtr m_Schema;
65 :
66 28 : RelaxNGSchema(const std::string& grammar)
67 28 : {
68 28 : xmlRelaxNGParserCtxtPtr ctxt = xmlRelaxNGNewMemParserCtxt(grammar.c_str(), (int)grammar.size());
69 28 : m_Schema = xmlRelaxNGParse(ctxt);
70 28 : xmlRelaxNGFreeParserCtxt(ctxt);
71 :
72 28 : if (m_Schema == NULL)
73 1 : LOGERROR("RelaxNGValidator: Failed to compile schema");
74 28 : }
75 :
76 28 : ~RelaxNGSchema()
77 28 : {
78 28 : if (m_Schema)
79 27 : xmlRelaxNGFree(m_Schema);
80 28 : }
81 : };
82 :
83 125 : RelaxNGValidator::RelaxNGValidator() :
84 125 : m_Schema(NULL)
85 : {
86 125 : }
87 :
88 329 : RelaxNGValidator::~RelaxNGValidator()
89 : {
90 329 : }
91 :
92 29 : bool RelaxNGValidator::LoadGrammar(const std::string& grammar)
93 : {
94 58 : std::shared_ptr<RelaxNGSchema> schema;
95 :
96 : {
97 58 : std::lock_guard<std::mutex> lock(g_SchemaCacheLock);
98 29 : std::map<std::string, std::shared_ptr<RelaxNGSchema>>::iterator it = g_SchemaCache.find(grammar);
99 29 : if (it == g_SchemaCache.end())
100 : {
101 28 : schema = std::make_shared<RelaxNGSchema>(grammar);
102 28 : g_SchemaCache[grammar] = schema;
103 : }
104 : else
105 : {
106 1 : schema = it->second;
107 : }
108 : }
109 :
110 29 : m_Schema = schema->m_Schema;
111 29 : if (!m_Schema)
112 1 : return false;
113 :
114 28 : MD5 hash;
115 28 : hash.Update((const u8*)grammar.c_str(), grammar.length());
116 28 : m_Hash = hash;
117 :
118 28 : return true;
119 : }
120 :
121 18 : bool RelaxNGValidator::LoadGrammarFile(const PIVFS& vfs, const VfsPath& grammarPath)
122 : {
123 36 : CVFSFile file;
124 18 : if (file.Load(vfs, grammarPath) != PSRETURN_OK)
125 6 : return false;
126 :
127 12 : return LoadGrammar(file.DecodeUTF8());
128 : }
129 :
130 29 : bool RelaxNGValidator::Validate(const std::string& filename, const std::string& document) const
131 : {
132 58 : std::string docutf8 = "<?xml version='1.0' encoding='utf-8'?>" + document;
133 :
134 58 : return ValidateEncoded(filename, docutf8);
135 : }
136 :
137 29 : bool RelaxNGValidator::ValidateEncoded(const std::string& filename, const std::string& document) const
138 : {
139 58 : TIMER_ACCRUE(xml_validation);
140 :
141 29 : if (!m_Schema)
142 : {
143 1 : LOGERROR("RelaxNGValidator: No grammar loaded");
144 1 : return false;
145 : }
146 :
147 28 : xmlDocPtr doc = xmlReadMemory(document.c_str(), (int)document.size(), filename.c_str(), NULL, XML_PARSE_NONET);
148 28 : if (doc == NULL)
149 : {
150 1 : LOGERROR("RelaxNGValidator: Failed to parse document '%s'", filename.c_str());
151 1 : return false;
152 : }
153 :
154 27 : bool ret = ValidateEncoded(doc);
155 27 : xmlFreeDoc(doc);
156 27 : return ret;
157 : }
158 :
159 42 : bool RelaxNGValidator::ValidateEncoded(xmlDocPtr doc) const
160 : {
161 42 : xmlRelaxNGValidCtxtPtr ctxt = xmlRelaxNGNewValidCtxt(m_Schema);
162 42 : xmlRelaxNGSetValidStructuredErrors(ctxt, &relaxNGErrorHandler, NULL);
163 42 : int ret = xmlRelaxNGValidateDoc(ctxt, doc);
164 42 : xmlRelaxNGFreeValidCtxt(ctxt);
165 :
166 42 : if (ret == 0)
167 : {
168 35 : return true;
169 : }
170 7 : else if (ret > 0)
171 : {
172 7 : LOGERROR("RelaxNGValidator: Validation failed for '%s'", doc->name);
173 7 : return false;
174 : }
175 : else
176 : {
177 0 : LOGERROR("RelaxNGValidator: Internal error %d", ret);
178 0 : return false;
179 : }
180 : }
181 :
182 101 : bool RelaxNGValidator::CanValidate() const
183 : {
184 101 : return m_Schema != NULL;
185 3 : }
|