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 <vector>
21 : #include <set>
22 : #include <map>
23 : #include <mutex>
24 : #include <stack>
25 : #include <algorithm>
26 :
27 : #include "maths/MD5.h"
28 : #include "ps/CacheLoader.h"
29 : #include "ps/CLogger.h"
30 : #include "ps/Filesystem.h"
31 :
32 : #include "RelaxNG.h"
33 : #include "Xeromyces.h"
34 :
35 : #include <libxml/parser.h>
36 :
37 : static std::mutex g_ValidatorCacheLock;
38 1 : static std::map<const std::string, RelaxNGValidator> g_ValidatorCache;
39 : static bool g_XeromycesStarted = false;
40 :
41 5 : static void errorHandler(void* UNUSED(userData), xmlErrorPtr error)
42 : {
43 : // Strip a trailing newline
44 10 : std::string message = error->message;
45 5 : if (message.length() > 0 && message[message.length()-1] == '\n')
46 5 : message.erase(message.length()-1);
47 :
48 5 : LOGERROR("CXeromyces: Parse %s: %s:%d: %s",
49 : error->level == XML_ERR_WARNING ? "warning" : "error",
50 : error->file, error->line, message);
51 : // TODO: The (non-fatal) warnings and errors don't get stored in the XMB,
52 : // so the caching is less transparent than it should be
53 5 : }
54 :
55 90 : void CXeromyces::Startup()
56 : {
57 90 : ENSURE(!g_XeromycesStarted);
58 90 : xmlInitParser();
59 90 : xmlSetStructuredErrorFunc(NULL, &errorHandler);
60 180 : std::lock_guard<std::mutex> lock(g_ValidatorCacheLock);
61 90 : g_ValidatorCache.insert(std::make_pair(std::string(), RelaxNGValidator()));
62 90 : g_XeromycesStarted = true;
63 90 : }
64 :
65 90 : void CXeromyces::Terminate()
66 : {
67 90 : ENSURE(g_XeromycesStarted);
68 90 : g_XeromycesStarted = false;
69 90 : ClearSchemaCache();
70 180 : std::lock_guard<std::mutex> lock(g_ValidatorCacheLock);
71 90 : g_ValidatorCache.clear();
72 90 : xmlSetStructuredErrorFunc(NULL, NULL);
73 90 : xmlCleanupParser();
74 90 : }
75 :
76 18 : bool CXeromyces::AddValidator(const PIVFS& vfs, const std::string& name, const VfsPath& grammarPath)
77 : {
78 18 : ENSURE(g_XeromycesStarted);
79 :
80 36 : RelaxNGValidator validator;
81 18 : if (!validator.LoadGrammarFile(vfs, grammarPath))
82 : {
83 6 : LOGERROR("CXeromyces: failed adding validator for '%s'", grammarPath.string8());
84 6 : return false;
85 : }
86 : {
87 24 : std::lock_guard<std::mutex> lock(g_ValidatorCacheLock);
88 12 : std::map<const std::string, RelaxNGValidator>::iterator it = g_ValidatorCache.find(name);
89 12 : if (it != g_ValidatorCache.end())
90 0 : g_ValidatorCache.erase(it);
91 12 : g_ValidatorCache.insert(std::make_pair(name, validator));
92 : }
93 12 : return true;
94 : }
95 :
96 0 : bool CXeromyces::ValidateEncoded(const std::string& name, const std::string& filename, const std::string& document)
97 : {
98 0 : std::lock_guard<std::mutex> lock(g_ValidatorCacheLock);
99 0 : return GetValidator(name).ValidateEncoded(filename, document);
100 : }
101 :
102 : /**
103 : * NOTE: Callers MUST acquire the g_ValidatorCacheLock before calling this.
104 : */
105 531 : RelaxNGValidator& CXeromyces::GetValidator(const std::string& name)
106 : {
107 531 : if (g_ValidatorCache.find(name) == g_ValidatorCache.end())
108 0 : return g_ValidatorCache.find("")->second;
109 531 : return g_ValidatorCache.find(name)->second;
110 : }
111 :
112 430 : PSRETURN CXeromyces::Load(const PIVFS& vfs, const VfsPath& filename, const std::string& validatorName /* = "" */)
113 : {
114 430 : ENSURE(g_XeromycesStarted);
115 :
116 860 : CCacheLoader cacheLoader(vfs, L".xmb");
117 :
118 430 : MD5 validatorGrammarHash;
119 : {
120 860 : std::lock_guard<std::mutex> lock(g_ValidatorCacheLock);
121 430 : validatorGrammarHash = GetValidator(validatorName).GetGrammarHash();
122 : }
123 860 : VfsPath xmbPath;
124 430 : Status ret = cacheLoader.TryLoadingCached(filename, validatorGrammarHash, XMBStorage::XMBVersion, xmbPath);
125 :
126 430 : if (ret == INFO::OK)
127 : {
128 : // Found a cached XMB - load it
129 314 : if (m_Data.ReadFromFile(vfs, xmbPath) && Initialise(m_Data))
130 314 : return PSRETURN_OK;
131 : // If this fails then we'll continue and (re)create the loose cache -
132 : // this failure legitimately happens due to partially-written XMB files or XMB version upgrades.
133 : // NB: at this point xmbPath may point to an archived file, we want to write a loose cached version.
134 0 : xmbPath = cacheLoader.LooseCachePath(filename, validatorGrammarHash, XMBStorage::XMBVersion);
135 : }
136 116 : else if (ret == INFO::SKIPPED)
137 : {
138 : // No cached version was found - we'll need to create it
139 : }
140 : else
141 : {
142 69 : ENSURE(ret < 0);
143 :
144 : // No source file or archive cache was found, so we can't load the
145 : // XML file at all
146 69 : LOGERROR("CCacheLoader failed to find archived or source file for: \"%s\"", filename.string8());
147 69 : return PSRETURN_Xeromyces_XMLOpenFailed;
148 : }
149 :
150 : // XMB isn't up to date with the XML, so rebuild it
151 47 : return ConvertFile(vfs, filename, xmbPath, validatorName);
152 : }
153 :
154 0 : bool CXeromyces::GenerateCachedXMB(const PIVFS& vfs, const VfsPath& sourcePath, VfsPath& archiveCachePath, const std::string& validatorName /* = "" */)
155 : {
156 0 : CCacheLoader cacheLoader(vfs, L".xmb");
157 :
158 0 : archiveCachePath = cacheLoader.ArchiveCachePath(sourcePath);
159 :
160 0 : return (ConvertFile(vfs, sourcePath, VfsPath("cache") / archiveCachePath, validatorName) == PSRETURN_OK);
161 : }
162 :
163 47 : PSRETURN CXeromyces::ConvertFile(const PIVFS& vfs, const VfsPath& filename, const VfsPath& xmbPath, const std::string& validatorName)
164 : {
165 94 : CVFSFile input;
166 47 : if (input.Load(vfs, filename))
167 : {
168 0 : LOGERROR("CXeromyces: Failed to open XML file %s", filename.string8());
169 0 : return PSRETURN_Xeromyces_XMLOpenFailed;
170 : }
171 :
172 94 : xmlDocPtr doc = xmlReadMemory((const char*)input.GetBuffer(), input.GetBufferSize(), CStrW(filename.string()).ToUTF8().c_str(), NULL,
173 47 : XML_PARSE_NONET|XML_PARSE_NOCDATA);
174 47 : if (!doc)
175 : {
176 1 : LOGERROR("CXeromyces: Failed to parse XML file %s", filename.string8());
177 1 : return PSRETURN_Xeromyces_XMLParseError;
178 : }
179 :
180 : {
181 92 : std::lock_guard<std::mutex> lock(g_ValidatorCacheLock);
182 46 : RelaxNGValidator& validator = GetValidator(validatorName);
183 46 : if (validator.CanValidate() && !validator.ValidateEncoded(doc))
184 : {
185 0 : LOGERROR("CXeromyces: failed to validate XML file %s", filename.string8());
186 0 : return PSRETURN_Xeromyces_XMLValidationFailed;
187 : }
188 : }
189 :
190 46 : m_Data.LoadXMLDoc(doc);
191 46 : xmlFreeDoc(doc);
192 :
193 : // Save the file to disk, so it can be loaded quickly next time.
194 : // Don't save if invalid, because we want the syntax error every program start.
195 46 : vfs->CreateFile(xmbPath, m_Data.m_Buffer, m_Data.m_Size);
196 :
197 : // Set up the XMBData
198 46 : const bool ok = Initialise(m_Data);
199 46 : ENSURE(ok);
200 :
201 46 : return PSRETURN_OK;
202 : }
203 :
204 56 : PSRETURN CXeromyces::LoadString(const char* xml, const std::string& validatorName /* = "" */)
205 : {
206 56 : ENSURE(g_XeromycesStarted);
207 :
208 56 : xmlDocPtr doc = xmlReadMemory(xml, (int)strlen(xml), "(no file)", NULL, XML_PARSE_NONET|XML_PARSE_NOCDATA);
209 56 : if (!doc)
210 : {
211 1 : LOGERROR("CXeromyces: Failed to parse XML string");
212 1 : return PSRETURN_Xeromyces_XMLParseError;
213 : }
214 :
215 : {
216 110 : std::lock_guard<std::mutex> lock(g_ValidatorCacheLock);
217 55 : RelaxNGValidator& validator = GetValidator(validatorName);
218 55 : if (validator.CanValidate() && !validator.ValidateEncoded(doc))
219 : {
220 0 : LOGERROR("CXeromyces: failed to validate XML string");
221 0 : return PSRETURN_Xeromyces_XMLValidationFailed;
222 : }
223 : }
224 :
225 55 : m_Data.LoadXMLDoc(doc);
226 55 : xmlFreeDoc(doc);
227 :
228 : // Set up the XMBData
229 55 : const bool ok = Initialise(m_Data);
230 55 : ENSURE(ok);
231 :
232 55 : return PSRETURN_OK;
233 3 : }
|