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