1 #include "Macros.h"
2 #include "DaeValidator.h"
3 #include "PathUtil.h"
4 #include "Strings.h"
5 #include "StringUtil.h"
6 #include <cmath>
7 #include "no_warning_iomanip"
8 #include "no_warning_iostream"
9 #include <set>
10 #include "no_warning_sstream"
11 
12 using namespace std;
13 
14 namespace std
15 {
16 	template<>
17 	struct less<tuple<size_t, string>>
18 	{
operator ()std::less19 		bool operator () (const tuple<size_t, string>& a, const tuple<size_t, string>& b) const
20 		{
21 			return get<1>(a) < get<1>(b);
22 		}
23 	};
24 }
25 
26 namespace opencollada
27 {
DaeValidator(const list<string> & daePaths)28 	DaeValidator::DaeValidator(const list<string> & daePaths)
29 	{
30 		mDaePaths.reserve(daePaths.size());
31 		mDaePaths.insert(mDaePaths.end(), daePaths.begin(), daePaths.end());
32 	}
33 
34 #if IS_MSVC_AND_MSVC_VERSION_LT(1900)
35 	typedef unsigned long long uint64_t;
36 #endif
37 	static const vector<tuple<uint64_t, string>> table =
38 	{
39 		make_tuple(1, "B"),
40 		make_tuple(1024, "kB"),
41 		make_tuple(1048576, "MB"),
42 		make_tuple(1073741824, "GB"),
43 		make_tuple(1099511627776, "TB")
44 	};
45 
46 	class Size
47 	{
48 	public:
Size(size_t size)49 		Size(size_t size)
50 			: mSize(size)
51 		{}
52 
str() const53 		string str() const
54 		{
55 			stringstream s;
56 			for (const auto & entry : table)
57 			{
58 				if (mSize < (get<0>(entry) * 1024))
59 				{
60 					s << round(mSize / static_cast<double>(get<0>(entry))) << get<1>(entry);
61 					break;
62 				}
63 			}
64 			return s.str();
65 		}
66 
67 	private:
68 		size_t mSize = 0;
69 	};
70 
for_each_dae(const function<int (const Dae &)> & task) const71 	int DaeValidator::for_each_dae(const function<int(const Dae &)> & task) const
72 	{
73 		int result = 0;
74 		size_t count = 1;
75 		for (const auto & daePath : mDaePaths)
76 		{
77 			if (mDaePaths.size() > 1)
78 			{
79 				cout << "[" << count << "/" << mDaePaths.size() << " " << static_cast<size_t>(static_cast<float>(count) / static_cast<float>(mDaePaths.size()) * 100.0f) << "%]" << endl;
80 				++count;
81 			}
82 
83 			cout << "Processing " << daePath << " (" << Size(Path::GetFileSize(daePath)).str() << ")" << endl;
84 
85 			Dae dae;
86 			dae.readFile(daePath);
87 			if (dae)
88 			{
89 				result |= task(dae);
90 			}
91 			else
92 			{
93 				result |= 1;
94 				cerr << "Error loading " << daePath << endl;
95 			}
96 		}
97 		return result;
98 	}
99 
checkAll() const100 	int DaeValidator::checkAll() const
101 	{
102 		return for_each_dae([&](const Dae & dae) {
103 			return checkAll(dae);
104 		});
105 	}
106 
checkAll(const Dae & dae) const107 	int DaeValidator::checkAll(const Dae & dae) const
108 	{
109 		return
110 			checkSchema(dae) |
111 			checkUniqueIds(dae) |
112 			checkUniqueSids(dae) |
113 			checkLinks(dae);
114 	}
115 
checkSchema(const string & schema_uri) const116 	int DaeValidator::checkSchema(const string & schema_uri) const
117 	{
118 		if (schema_uri.empty())
119 		{
120 			return for_each_dae([&](const Dae & dae) {
121 				return checkSchema(dae);
122 			});
123 		}
124 
125 		XmlSchema schema;
126 		schema.readFile(schema_uri);
127 		if (schema)
128 		{
129 			return for_each_dae([&](const Dae & dae) {
130 				return ValidateAgainstSchema(dae, schema);
131 			});
132 		}
133 
134 		cerr << "Error loading " << schema_uri << endl;
135 		return 1;
136 	}
137 
checkSchema(const Dae & dae) const138 	int DaeValidator::checkSchema(const Dae & dae) const
139 	{
140 		cout << "Checking schema..." << endl;
141 
142 		int result = 0;
143 
144 		Dae::Version version = dae.getVersion();
145 		if (version == Dae::Version::COLLADA14)
146 		{
147 			result |= ValidateAgainstSchema(dae, Dae::GetColladaSchema141());
148 		}
149 		else if (version == Dae::Version::COLLADA15)
150 		{
151 			//result |= ValidateAgainstSchema(dae, Dae::GetColladaSchema15());
152 			cerr << "COLLADA 1.5 not supported yet." << endl;
153 			return 1;
154 		}
155 		else
156 		{
157 			cerr << "Can't determine COLLADA version used by input file" << endl;
158 			return 1;
159 		}
160 
161 		vector<XmlNode> subDocs;
162 
163 		// Find xsi:schemaLocation attributes in dae and try to validate against specified xsd documents
164 		const auto & elements = dae.root().selectNodes("//*[@xsi:schemaLocation]");
165 		for (const auto & element : elements)
166 		{
167 			if (auto schemaLocation = element.attribute("schemaLocation"))
168 			{
169 				vector<string> parts = String::Split(schemaLocation.value());
170 				// Parse pairs of namespace/xsd
171 				for (size_t i = 1; i < parts.size(); i += 2)
172 				{
173 					const string & ns = parts[i - 1];
174 					const string & xsdUri = parts[i];
175 
176 					if (ns != Dae::GetColladaNamespace141() && ns != Dae::GetColladaNamespace15())
177 					{
178 						// "insert" does nothing if element already exists.
179 						mSchemas.insert(pair<string, XmlSchema>(ns, XmlSchema()));
180 						mSchemaLocations.insert(pair<string, string>(ns, xsdUri));
181 					}
182 				}
183 			}
184 		}
185 
186 		// Preload uninitialized .xsd files
187 		auto itSchema = mSchemas.begin();
188 		auto itSchemaLocation = mSchemaLocations.begin();
189 		for (; itSchema != mSchemas.end(); ++itSchema, ++itSchemaLocation)
190 		{
191 			const auto & schemaUri = itSchemaLocation->second;
192 			auto & schema = itSchema->second;
193 
194 			// Don't try to load schemas that already failed in a previous run
195 			if (schema.failedToLoad())
196 			{
197 				cout << "Error loading " << schemaUri << endl;
198 				result |= 1;
199 				continue;
200 			}
201 
202 			string uri = schemaUri;
203 
204 			if (!schema)
205 			{
206 				schema.readFile(uri);
207 			}
208 
209 			if (!schema)
210 			{
211 				// Try to find schema document in executable directory
212 				Uri xsdUri(schemaUri);
213 				if (xsdUri.isValid())
214 				{
215 					uri = Path::Join(Path::GetExecutableDirectory(), xsdUri.pathFile());
216 					schema.readFile(uri);
217 				}
218 			}
219 
220 			if (!schema)
221 			{
222 				// Try to find schema document in COLLADA document directory
223 				Uri xsdUri(schemaUri);
224 				string xsdFile = xsdUri.pathFile();
225 				xsdUri = dae.getURI();
226 				xsdUri.setPathFile(xsdFile);
227 				uri = xsdUri.str();
228 				schema.readFile(uri);
229 			}
230 
231 			if (schema && uri != schemaUri)
232 			{
233 				cout << "Using " << uri << endl;
234 			}
235 			else if (!schema)
236 			{
237 				cout << "Error loading " << schemaUri << endl;
238 				result |= 1;
239 			}
240 		}
241 
242 		// Validate "sub documents"
243 		for (const auto & schema : mSchemas)
244 		{
245 			// Ignore schemas that failed to load
246 			if (!schema.second)
247 				continue;
248 
249 			const auto & ns = schema.first;
250 			stringstream xpath;
251 			xpath << "//*[namespace-uri()='" << ns << "' and not(namespace-uri(./..)='" << ns << "')]";
252 			const auto & nodes = dae.root().selectNodes(xpath.str());
253 			for (auto node : nodes)
254 			{
255 				auto autoRestoreRoot = dae.setTempRoot(node);
256 				result |= ValidateAgainstSchema(dae, schema.second);
257 			}
258 		}
259 
260 		return result;
261 	}
262 
checkUniqueIds() const263 	int DaeValidator::checkUniqueIds() const
264 	{
265 		return for_each_dae([&](const Dae & dae) {
266 			return checkUniqueIds(dae);
267 		});
268 	}
269 
checkUniqueIds(const Dae & dae) const270 	int DaeValidator::checkUniqueIds(const Dae & dae) const
271 	{
272 		cout << "Checking unique ids..." << endl;
273 
274 		int result = 0;
275 		map<string, size_t> ids;
276 		const auto & nodes = dae.root().selectNodes("//*[@id]");
277 		for (const auto & node : nodes)
278 		{
279 			string id = node.attribute("id").value();
280 			size_t line = node.line();
281 
282 			int checkEscapeCharResult = CheckEscapeChar(id);
283 			if (checkEscapeCharResult != 0)
284 			{
285 				cerr << dae.getURI() << ":" << line << ": \"" << id << "\" contains non-escaped characters." << endl;
286 				result |= checkEscapeCharResult;
287 			}
288 
289 			auto it = ids.find(id);
290 			if (it != ids.end())
291 			{
292 				cerr << dae.getURI() << ":" << line << ": Duplicated id \"" << id << "\". See first declaration at line " << it->second << "." << endl;
293 				result |= 1;
294 			}
295 			else
296 			{
297 				ids[id] = line;
298 			}
299 		}
300 		return result;
301 	}
302 
checkUniqueSids() const303 	int DaeValidator::checkUniqueSids() const
304 	{
305 		return for_each_dae([&](const Dae & dae) {
306 			return checkUniqueSids(dae);
307 		});
308 	}
309 
checkUniqueSids(const Dae & dae) const310 	int DaeValidator::checkUniqueSids(const Dae & dae) const
311 	{
312 		cout << "Checking unique sids..." << endl;
313 
314 		int result = 0;
315 		const auto & parents = dae.root().selectNodes("//*[@sid]/..");
316 		for (auto parent : parents)
317 		{
318 			const auto & children = parent.selectNodes("*[@sid]");
319 			map<string, size_t> sids;
320 			for (auto child : children)
321 			{
322 				string sid = child.attribute("sid").value();
323 				size_t line = child.line();
324 
325 				auto it = sids.find(sid);
326 				if (it != sids.end())
327 				{
328 					cerr << dae.getURI() << ":" << line << ": Duplicated sid \"" << sid << "\". See first declaration at line " << it->second << "." << endl;
329 					result |= 1;
330 				}
331 				else
332 				{
333 					sids[sid] = line;
334 				}
335 			}
336 		}
337 		return result;
338 	}
339 
checkLinks() const340 	int DaeValidator::checkLinks() const
341 	{
342 		return for_each_dae([&](const Dae & dae) {
343 			return checkLinks(dae);
344 		});
345 	}
346 
checkLinks(const Dae & dae) const347 	int DaeValidator::checkLinks(const Dae & dae) const
348 	{
349 		cout << "Checking links..." << endl;
350 
351 		const auto & ids = dae.getIds();
352 
353 		int result = 0;
354 		for (const auto & t : dae.getAnyURIs())
355 		{
356 			const auto & line = get<0>(t);
357 			const auto & uri = get<1>(t);
358 			if (!Path::Exists(uri.nativePath()))
359 			{
360 				cerr << dae.getURI() << ":" << line << ": Can't resolve " << uri << endl;
361 				result |= 1;
362 			}
363 			else if (!uri.fragment().empty())
364 			{
365 				Uri no_fragment_uri(uri);
366 				no_fragment_uri.setFragment("");
367 				if (no_fragment_uri == dae.getURI())
368 				{
369 					auto id = ids.find(uri.fragment());
370 					if (id == ids.end())
371 					{
372 						cerr << dae.getURI() << ":" << line << ": Can't resolve #" << uri.fragment() << endl;
373 						result |= 1;
374 					}
375 				}
376 				else
377 				{
378 					auto it = dae.getExternalDAEs().find(no_fragment_uri);
379 					if (it != dae.getExternalDAEs().end())
380 					{
381 						if (it->second)
382 						{
383 							auto ext_ids = it->second.getIds();
384 							auto id = ext_ids.find(uri.fragment());
385 							if (id == ext_ids.end())
386 							{
387 								cerr << dae.getURI() << ":" << line << ": Can't resolve " << uri << endl;
388 								result |= 1;
389 							}
390 						}
391 						else
392 						{
393 							cerr << dae.getURI() << ":" << line << ": " << uri << ": referenced file exists but has not been successfully loaded." << endl;
394 							result |= 1;
395 						}
396 					}
397 				}
398 			}
399 		}
400 
401 		// IDREF
402 		for (const auto & IDREF : dae.getIDREFs())
403 		{
404 			const auto & line = get<0>(IDREF);
405 			const auto & idref = get<1>(IDREF);
406 
407 			auto id = ids.find(idref);
408 			if (id == ids.end())
409 			{
410 				cerr << dae.getURI() << ":" << line << ": Can't resolve #" << idref << endl;
411 				result |= 1;
412 			}
413 		}
414 
415 		return result;
416 	}
417 
ValidateAgainstSchema(const Dae & dae,const XmlSchema & schema)418 	int DaeValidator::ValidateAgainstSchema(const Dae & dae, const XmlSchema & schema)
419 	{
420 		return schema.validate(dae) ? 0 : 1;
421 	}
422 
CheckEscapeChar(const std::string & s)423 	int DaeValidator::CheckEscapeChar(const std::string & s)
424 	{
425 		if (s.find_first_of(" #$%&/:;<=>?@[\\:]^`{|}~") != string::npos)
426 			return 1;
427 		return 0;
428 	}
429 }
430