1 /*
2    Copyright (C) 2011 - 2018 by Sytyi Nick <nsytyi@gmail.com>
3    Part of the Battle for Wesnoth Project https://www.wesnoth.org/
4 
5    This program is free software; you can redistribute it and/or modify
6    it under the terms of the GNU General Public License as published by
7    the Free Software Foundation; either version 2 of the License, or
8    (at your option) any later version.
9    This program is distributed in the hope that it will be useful,
10    but WITHOUT ANY WARRANTY.
11 
12    See the COPYING file for more details.
13 */
14 
15 #include "serialization/schema_validator.hpp"
16 
17 #include "filesystem.hpp"
18 #include "gettext.hpp"
19 #include "log.hpp"
20 #include "serialization/preprocessor.hpp"
21 #include "wml_exception.hpp"
22 
23 namespace schema_validation
24 {
25 static lg::log_domain log_validation("validation");
26 
27 #define ERR_VL LOG_STREAM(err, log_validation)
28 #define WRN_VL LOG_STREAM(warn, log_validation)
29 #define LOG_VL LOG_STREAM(info, log_validation)
30 
at(const std::string & file,int line)31 static std::string at(const std::string& file, int line)
32 {
33 	std::ostringstream ss;
34 	ss << line << " " << file;
35 	return "at " + ::lineno_string(ss.str());
36 }
37 
print_output(const std::string & message,bool flag_exception=false)38 static void print_output(const std::string& message, bool flag_exception = false)
39 {
40 #ifndef VALIDATION_ERRORS_LOG
41 	if(flag_exception) {
42 		throw wml_exception("Validation error occurred", message);
43 	} else {
44 		ERR_VL << message;
45 	}
46 #else
47 	// dirty hack to avoid "unused" error in case of compiling with definition on
48 	flag_exception = true;
49 	if(flag_exception) {
50 		ERR_VL << message;
51 	}
52 #endif
53 }
54 
extra_tag_error(const std::string & file,int line,const std::string & name,int n,const std::string & parent,bool flag_exception)55 static void extra_tag_error(const std::string& file,
56 		int line,
57 		const std::string& name,
58 		int n,
59 		const std::string& parent,
60 		bool flag_exception)
61 {
62 	std::ostringstream ss;
63 	ss << "Extra tag [" << name << "]; there may only be " << n << " [" << name << "] in [" << parent << "]\n"
64 	   << at(file, line) << "\n";
65 	print_output(ss.str(), flag_exception);
66 }
67 
wrong_tag_error(const std::string & file,int line,const std::string & name,const std::string & parent,bool flag_exception)68 static void wrong_tag_error(
69 		const std::string& file, int line, const std::string& name, const std::string& parent, bool flag_exception)
70 {
71 	std::ostringstream ss;
72 	ss << "Tag [" << name << "] may not be used in [" << parent << "]\n" << at(file, line) << "\n";
73 	print_output(ss.str(), flag_exception);
74 }
75 
missing_tag_error(const std::string & file,int line,const std::string & name,int n,const std::string & parent,bool flag_exception)76 static void missing_tag_error(const std::string& file,
77 		int line,
78 		const std::string& name,
79 		int n,
80 		const std::string& parent,
81 		bool flag_exception)
82 {
83 	std::ostringstream ss;
84 	ss << "Missing tag [" << name << "]; there must be " << n << " [" << name << "]s in [" << parent << "]\n"
85 	   << at(file, line) << "\n";
86 	print_output(ss.str(), flag_exception);
87 }
88 
extra_key_error(const std::string & file,int line,const std::string & tag,const std::string & key,bool flag_exception)89 static void extra_key_error(
90 		const std::string& file, int line, const std::string& tag, const std::string& key, bool flag_exception)
91 {
92 	std::ostringstream ss;
93 	ss << "Invalid key '" << key << "=' in tag [" << tag << "]\n" << at(file, line) << "\n";
94 	print_output(ss.str(), flag_exception);
95 }
96 
missing_key_error(const std::string & file,int line,const std::string & tag,const std::string & key,bool flag_exception)97 static void missing_key_error(
98 		const std::string& file, int line, const std::string& tag, const std::string& key, bool flag_exception)
99 {
100 	std::ostringstream ss;
101 	ss << "Missing key '" << key << "=' in tag [" << tag << "]\n" << at(file, line) << "\n";
102 	print_output(ss.str(), flag_exception);
103 }
104 
wrong_value_error(const std::string & file,int line,const std::string & tag,const std::string & key,const std::string & value,bool flag_exception)105 static void wrong_value_error(const std::string& file,
106 		int line,
107 		const std::string& tag,
108 		const std::string& key,
109 		const std::string& value,
110 		bool flag_exception)
111 {
112 	std::ostringstream ss;
113 	ss << "Invalid value '" << value << "' in key '" << key << "=' in tag [" << tag << "]\n" << at(file, line) << "\n";
114 
115 	print_output(ss.str(), flag_exception);
116 }
117 
~schema_validator()118 schema_validator::~schema_validator()
119 {
120 }
121 
schema_validator(const std::string & config_file_name)122 schema_validator::schema_validator(const std::string& config_file_name)
123 	: config_read_(false)
124 	, create_exceptions_(strict_validation_enabled)
125 	, root_()
126 	, stack_()
127 	, counter_()
128 	, cache_()
129 	, types_()
130 {
131 	if(!read_config_file(config_file_name)) {
132 		ERR_VL << "Schema file " << config_file_name << " was not read." << std::endl;
133 		throw abstract_validator::error("Schema file " + config_file_name + " was not read.\n");
134 	} else {
135 		stack_.push(&root_);
136 		counter_.emplace();
137 		cache_.emplace();
138 		root_.expand_all(root_);
139 		LOG_VL << "Schema file " << config_file_name << " was read.\n"
140 			   << "Validator initialized\n";
141 	}
142 }
143 
read_config_file(const std::string & filename)144 bool schema_validator::read_config_file(const std::string& filename)
145 {
146 	config cfg;
147 	try {
148 		preproc_map preproc(game_config::config_cache::instance().get_preproc_map());
149 		filesystem::scoped_istream stream = preprocess_file(filename, &preproc);
150 		read(cfg, *stream);
151 	} catch(const config::error& e) {
152 		ERR_VL << "Failed to read file " << filename << ":\n" << e.what() << "\n";
153 		return false;
154 	}
155 
156 	for(const config& g : cfg.child_range("wml_schema")) {
157 		for(const config& schema : g.child_range("tag")) {
158 			if(schema["name"].str() == "root") {
159 				//@NOTE Don't know, maybe merging of roots needed.
160 				root_ = class_tag(schema);
161 			}
162 		}
163 
164 		for(const config& type : g.child_range("type")) {
165 			try {
166 				types_[type["name"].str()] = boost::regex(type["value"].str());
167 			} catch(const std::exception&) {
168 				// Need to check all type values in schema-generator
169 			}
170 		}
171 	}
172 
173 	config_read_ = true;
174 	return true;
175 }
176 
177 /*
178  * Please, @Note that there is some magic in pushing and poping to/from stacks.
179  * assume they all are on their place due to parser algorithm
180  * and validation logic
181  */
open_tag(const std::string & name,int start_line,const std::string & file,bool addittion)182 void schema_validator::open_tag(const std::string& name, int start_line, const std::string& file, bool addittion)
183 {
184 	if(!stack_.empty()) {
185 		const class_tag* tag = nullptr;
186 
187 		if(stack_.top()) {
188 			tag = stack_.top()->find_tag(name, root_);
189 
190 			if(!tag) {
191 				wrong_tag_error(file, start_line, name, stack_.top()->get_name(), create_exceptions_);
192 			} else {
193 				if(!addittion) {
194 					counter& cnt = counter_.top()[name];
195 					++cnt.cnt;
196 				}
197 			}
198 		}
199 
200 		stack_.push(tag);
201 	} else {
202 		stack_.push(nullptr);
203 	}
204 
205 	counter_.emplace();
206 	cache_.emplace();
207 }
208 
close_tag()209 void schema_validator::close_tag()
210 {
211 	stack_.pop();
212 	counter_.pop();
213 	// cache_ is cleared in another place.
214 }
215 
validate(const config & cfg,const std::string & name,int start_line,const std::string & file)216 void schema_validator::validate(const config& cfg, const std::string& name, int start_line, const std::string& file)
217 {
218 	// close previous errors and print them to output.
219 	for(auto& m : cache_.top()) {
220 		for(auto& list : m.second) {
221 			print(list);
222 		}
223 	}
224 
225 	cache_.pop();
226 
227 	// clear cache
228 	auto cache_it = cache_.top().find(&cfg);
229 	if(cache_it != cache_.top().end()) {
230 		cache_it->second.clear();
231 	}
232 
233 	// Please note that validating unknown tag keys the result will be false
234 	// Checking all elements counters.
235 	if(!stack_.empty() && stack_.top() && config_read_) {
236 		for(const auto& tag : stack_.top()->tags()) {
237 			int cnt = counter_.top()[tag.first].cnt;
238 
239 			if(tag.second.get_min() > cnt) {
240 				cache_.top()[&cfg].emplace_back(
241 					MISSING_TAG, file, start_line, tag.second.get_min(), tag.first, "", name);
242 				continue;
243 			}
244 
245 			if(tag.second.get_max() < cnt) {
246 				cache_.top()[&cfg].emplace_back(
247 					EXTRA_TAG, file, start_line, tag.second.get_max(), tag.first, "", name);
248 			}
249 		}
250 
251 		// Checking if all mandatory keys are present
252 		for(const auto& key : stack_.top()->keys()) {
253 			if(key.second.is_mandatory()) {
254 				if(cfg.get(key.first) == nullptr) {
255 					cache_.top()[&cfg].emplace_back(MISSING_KEY, file, start_line, 0, name, key.first);
256 				}
257 			}
258 		}
259 	}
260 }
261 
validate_key(const config & cfg,const std::string & name,const std::string & value,int start_line,const std::string & file)262 void schema_validator::validate_key(
263 		const config& cfg, const std::string& name, const std::string& value, int start_line, const std::string& file)
264 {
265 	if(!stack_.empty() && stack_.top() && config_read_) {
266 		// checking existing keys
267 		const class_key* key = stack_.top()->find_key(name);
268 		if(key) {
269 			auto itt = types_.find(key->get_type());
270 
271 			if(itt != types_.end()) {
272 				boost::smatch sub;
273 				bool res = boost::regex_match(value, sub, itt->second);
274 
275 				if(!res) {
276 					cache_.top()[&cfg].emplace_back(
277 						WRONG_VALUE, file, start_line, 0, stack_.top()->get_name(), name, value);
278 				}
279 			}
280 		} else {
281 			cache_.top()[&cfg].emplace_back(EXTRA_KEY, file, start_line, 0, stack_.top()->get_name(), name);
282 		}
283 	}
284 }
285 
print(message_info & el)286 void schema_validator::print(message_info& el)
287 {
288 	switch(el.type) {
289 	case WRONG_TAG:
290 		wrong_tag_error(el.file, el.line, el.tag, el.value, create_exceptions_);
291 		break;
292 	case EXTRA_TAG:
293 		extra_tag_error(el.file, el.line, el.tag, el.n, el.value, create_exceptions_);
294 		break;
295 	case MISSING_TAG:
296 		missing_tag_error(el.file, el.line, el.tag, el.n, el.value, create_exceptions_);
297 		break;
298 	case EXTRA_KEY:
299 		extra_key_error(el.file, el.line, el.tag, el.key, create_exceptions_);
300 		break;
301 	case WRONG_VALUE:
302 		wrong_value_error(el.file, el.line, el.tag, el.key, el.value, create_exceptions_);
303 		break;
304 	case MISSING_KEY:
305 		missing_key_error(el.file, el.line, el.tag, el.key, create_exceptions_);
306 	}
307 }
308 } // namespace schema_validation{
309