1 #include "OptionsDB.h"
2 
3 #include "Directories.h"
4 #include "i18n.h"
5 #include "Logger.h"
6 #include "OptionValidators.h"
7 #include "XMLDoc.h"
8 
9 #include <iostream>
10 #include <iomanip>
11 #include <stdexcept>
12 #include <string>
13 
14 #include <boost/algorithm/string/classification.hpp>
15 #include <boost/algorithm/string/erase.hpp>
16 #include <boost/algorithm/string/predicate.hpp>
17 #include <boost/filesystem/fstream.hpp>
18 #include <boost/filesystem/operations.hpp>
19 #include <boost/range/algorithm_ext/erase.hpp>
20 #include <boost/tokenizer.hpp>
21 
22 namespace {
OptionsRegistry()23     std::vector<OptionsDBFn>& OptionsRegistry() {
24         static std::vector<OptionsDBFn> options_db_registry;
25         return options_db_registry;
26     }
27 
PreviousSectionName(const std::vector<XMLElement * > & elem_stack)28     std::string PreviousSectionName(const std::vector<XMLElement*>& elem_stack) {
29         std::string retval;
30         for (unsigned int i = 1; i < elem_stack.size(); ++i) {
31             retval += elem_stack[i]->Tag();
32             if (i != elem_stack.size() - 1)
33                 retval += '.';
34         }
35         return retval;
36     }
37 
StripQuotation(std::string & str)38     void StripQuotation(std::string& str) {
39         using namespace boost::algorithm;
40         if (starts_with(str, "\"") && ends_with(str, "\"")) {
41             erase_first(str, "\"");
42             erase_last(str, "\"");
43         }
44     }
45 }
46 
47 /////////////////////////////////////////////
48 // Free Functions
49 /////////////////////////////////////////////
RegisterOptions(OptionsDBFn function)50 bool RegisterOptions(OptionsDBFn function) {
51     OptionsRegistry().push_back(function);
52     return true;
53 }
54 
GetOptionsDB()55 OptionsDB& GetOptionsDB() {
56     static OptionsDB options_db;
57     if (!OptionsRegistry().empty()) {
58         for (OptionsDBFn fn : OptionsRegistry())
59             fn(options_db);
60         OptionsRegistry().clear();
61     }
62     return options_db;
63 }
64 
65 
66 /////////////////////////////////////////////
67 // OptionsDB::Option
68 /////////////////////////////////////////////
69 // static(s)
70 std::map<char, std::string> OptionsDB::Option::short_names;
71 
Option()72 OptionsDB::Option::Option()
73 {}
74 
Option(char short_name_,const std::string & name_,const boost::any & value_,const boost::any & default_value_,const std::string & description_,const ValidatorBase * validator_,bool storable_,bool flag_,bool recognized_,const std::string & section)75 OptionsDB::Option::Option(char short_name_, const std::string& name_, const boost::any& value_,
76                           const boost::any& default_value_, const std::string& description_,
77                           const ValidatorBase* validator_, bool storable_, bool flag_, bool recognized_,
78                           const std::string& section) :
79     name(name_),
80     short_name(short_name_),
81     value(value_),
82     default_value(default_value_),
83     description(description_),
84     validator(validator_),
85     storable(storable_),
86     flag(flag_),
87     recognized(recognized_),
88     option_changed_sig_ptr(new boost::signals2::signal<void ()>())
89 {
90     if (short_name_)
91         short_names[short_name_] = name;
92 
93     auto name_it = name.rfind('.');
94     if (name_it != std::string::npos)
95         sections.emplace(name.substr(0, name_it));
96 
97     if (!section.empty())
98         sections.emplace(section);
99     else if (sections.empty())
100         sections.emplace("misc");
101 }
102 
SetFromString(const std::string & str)103 bool OptionsDB::Option::SetFromString(const std::string& str) {
104     bool changed = false;
105     boost::any value_;
106 
107     if (!flag) {
108         value_ = validator->Validate(str);
109         changed = validator->String(value) != validator->String(value_);
110     } else {
111         value_ = boost::lexical_cast<bool>(str);    // if a flag, then the str parameter should just indicate true or false with "1" or "0"
112         changed = (boost::lexical_cast<std::string>(boost::any_cast<bool>(value))
113                    != boost::lexical_cast<std::string>(boost::any_cast<bool>(value_)));
114     }
115 
116     if (changed) {
117         value = std::move(value_);
118         (*option_changed_sig_ptr)();
119     }
120     return changed;
121 }
122 
SetToDefault()123 bool OptionsDB::Option::SetToDefault() {
124     bool changed = !ValueIsDefault();
125     if (changed) {
126         value = default_value;
127         (*option_changed_sig_ptr)();
128     }
129     return changed;
130 }
131 
ValueToString() const132 std::string OptionsDB::Option::ValueToString() const {
133     if (!flag)
134         return validator->String(value);
135     else
136         return boost::lexical_cast<std::string>(boost::any_cast<bool>(value));
137 }
138 
DefaultValueToString() const139 std::string OptionsDB::Option::DefaultValueToString() const {
140     if (!flag)
141         return validator->String(default_value);
142     else
143         return boost::lexical_cast<std::string>(boost::any_cast<bool>(default_value));
144 }
145 
ValueIsDefault() const146 bool OptionsDB::Option::ValueIsDefault() const
147 { return ValueToString() == DefaultValueToString(); }
148 
149 
150 /////////////////////////////////////////////
151 // OptionsDB::OptionSection
152 /////////////////////////////////////////////
153 OptionsDB::OptionSection::OptionSection() = default;
154 
OptionSection(const std::string & name_,const std::string & description_,std::function<bool (const std::string &)> option_predicate_)155 OptionsDB::OptionSection::OptionSection(const std::string& name_, const std::string& description_,
156                                         std::function<bool (const std::string&)> option_predicate_) :
157     name(name_),
158     description(description_),
159     option_predicate(option_predicate_)
160 {}
161 
162 /////////////////////////////////////////////
163 // OptionsDB
164 /////////////////////////////////////////////
165 // static(s)
166 OptionsDB* OptionsDB::s_options_db = nullptr;
167 
OptionsDB()168 OptionsDB::OptionsDB() : m_dirty(false) {
169     if (s_options_db)
170         throw std::runtime_error("Attempted to create a duplicate instance of singleton class OptionsDB.");
171 
172     s_options_db = this;
173 }
174 
Commit(bool only_if_dirty,bool only_non_default)175 bool OptionsDB::Commit(bool only_if_dirty, bool only_non_default) {
176     if (only_if_dirty && !m_dirty)
177         return true;
178     boost::filesystem::ofstream ofs(GetConfigPath());
179     if (ofs) {
180         XMLDoc doc;
181         GetOptionsDB().GetXML(doc, only_non_default, true);
182         doc.WriteDoc(ofs);
183         m_dirty = false;
184         return true;
185     } else {
186         std::cerr << UserString("UNABLE_TO_WRITE_CONFIG_XML") << std::endl;
187         std::cerr << PathToString(GetConfigPath()) << std::endl;
188         ErrorLogger() << UserString("UNABLE_TO_WRITE_CONFIG_XML");
189         ErrorLogger() << PathToString(GetConfigPath());
190         return false;
191     }
192 }
193 
CommitPersistent()194 bool OptionsDB::CommitPersistent() {
195     bool retval = false;
196     auto config_file = GetPersistentConfigPath();
197     XMLDoc doc;
198     GetOptionsDB().GetXML(doc, true, false);   // only output non-default options
199     try {
200         // Remove any previously existing file
201         boost::filesystem::remove(config_file);
202 
203         boost::filesystem::ofstream ofs(GetPersistentConfigPath());
204         if (ofs) {
205             doc.WriteDoc(ofs);
206             retval = true;
207         } else {
208             std::string err_msg = UserString("UNABLE_TO_WRITE_PERSISTENT_CONFIG_XML") + " : " + config_file.string();
209             ErrorLogger() << err_msg;
210             std::cerr << err_msg << std::endl;
211         }
212     } catch (const boost::filesystem::filesystem_error& ec) {
213         ErrorLogger() << "Error during file operations when creating persistent config : " << ec.what();
214     } catch (...) {
215         std::string err_msg = "Unknown exception during persistent config creation";
216         ErrorLogger() << err_msg;
217         std::cerr << err_msg << std::endl;
218     }
219 
220     return retval;
221 }
222 
Validate(const std::string & name,const std::string & value) const223 void OptionsDB::Validate(const std::string& name, const std::string& value) const {
224     auto it = m_options.find(name);
225     if (!OptionExists(it))
226         throw std::runtime_error("Attempted to validate unknown option \"" + name + "\".");
227 
228     if (it->second.validator)
229         it->second.validator->Validate(value);
230     else if (it->second.flag)
231         boost::lexical_cast<bool>(value);
232 }
233 
GetValueString(const std::string & option_name) const234 std::string OptionsDB::GetValueString(const std::string& option_name) const {
235     auto it = m_options.find(option_name);
236     if (!OptionExists(it))
237         throw std::runtime_error(("OptionsDB::GetValueString(): No option called \"" + option_name + "\" could be found.").c_str());
238     return it->second.ValueToString();
239 }
240 
GetDefaultValueString(const std::string & option_name) const241 std::string OptionsDB::GetDefaultValueString(const std::string& option_name) const {
242     auto it = m_options.find(option_name);
243     if (!OptionExists(it))
244         throw std::runtime_error(("OptionsDB::GetDefaultValueString(): No option called \"" + option_name + "\" could be found.").c_str());
245     return it->second.DefaultValueToString();
246 }
247 
GetDescription(const std::string & option_name) const248 const std::string& OptionsDB::GetDescription(const std::string& option_name) const {
249     auto it = m_options.find(option_name);
250     if (!OptionExists(it))
251         throw std::runtime_error(("OptionsDB::GetDescription(): No option called \"" + option_name + "\" could be found.").c_str());
252     return it->second.description;
253 }
254 
GetValidator(const std::string & option_name) const255 std::shared_ptr<const ValidatorBase> OptionsDB::GetValidator(const std::string& option_name) const {
256     auto it = m_options.find(option_name);
257     if (!OptionExists(it))
258         throw std::runtime_error(("OptionsDB::GetValidator(): No option called \"" + option_name + "\" could be found.").c_str());
259     return it->second.validator;
260 }
261 
262 namespace {
263     const std::size_t TERMINAL_LINE_WIDTH = 80;
264 
265     /** Breaks and indents text over multiple lines when it exceeds width limits
266      * @param text String to format, tokenized by spaces, tabs, and newlines (newlines retained but potentially indented)
267      * @param indents amount of space prior to text. First for initial line, second for any new lines.
268      * @param widths width to limit the text to. First for initial line, second for any new lines.
269      * @returns string Formatted results of @p text
270      */
SplitText(const std::string & text,std::pair<std::size_t,std::size_t> indents={ 0, 0 },std::pair<std::size_t,std::size_t> widths={ TERMINAL_LINE_WIDTH, TERMINAL_LINE_WIDTH })271     std::string SplitText(const std::string& text, std::pair<std::size_t, std::size_t> indents = { 0, 0 },
272                           std::pair<std::size_t, std::size_t> widths = { TERMINAL_LINE_WIDTH, TERMINAL_LINE_WIDTH })
273     {
274         boost::char_separator<char> separator { " \t", "\n" };
275         boost::tokenizer<boost::char_separator<char>> tokens { text, separator };
276 
277         std::vector<std::string> lines { "" };
278         for (const auto& token : tokens) {
279             if (token == "\n")
280                 lines.push_back("");
281             else if (widths.second < lines.back().size() + token.size() + indents.second)
282                 lines.push_back(token + " ");
283             else if (!token.empty())
284                 lines.back().append(token + " ");
285         }
286 
287         std::string indent { std::string(indents.second, ' ') };
288         std::stringstream retval;
289         auto first_line = std::move(lines.front());
290         retval << std::string(indents.first, ' ') << first_line << std::endl;
291         for (auto line : lines)
292             if (!line.empty())
293                 retval << indent << line << std::endl;
294 
295         return retval.str();
296     }
297 
OptionNameHasParentSection(const std::string & lhs,const std::string & rhs)298     bool OptionNameHasParentSection(const std::string& lhs, const std::string& rhs) {
299         auto it = lhs.find_last_of('.');
300         if (it == std::string::npos)
301             return false;
302         return lhs.substr(0, it) == rhs;
303     }
304 }
305 
OptionsBySection(bool allow_unrecognized) const306 std::unordered_map<std::string, std::set<std::string>> OptionsDB::OptionsBySection(bool allow_unrecognized) const {
307     // Determine sections after all predicate calls from known options
308     std::unordered_map<std::string, std::unordered_set<std::string>> sections_by_option;
309     for (const auto& option : m_options) {
310         if (!allow_unrecognized && !option.second.recognized)
311             continue;
312 
313         for (const auto& section : option.second.sections)
314             sections_by_option[option.first].emplace(section);
315 
316         for (auto& section : m_sections)
317             if (section.second.option_predicate && section.second.option_predicate(option.first))
318                 sections_by_option[option.first].emplace(section.first);
319     }
320 
321     // tally the total number of options under each section
322     std::unordered_map<std::string, std::size_t> total_options_per_section;
323     for (const auto& option_section : sections_by_option) {
324         auto option_name = option_section.first;
325         auto dot_it = option_name.find_first_of(".");
326         // increment count of each containing parent section
327         while (dot_it != std::string::npos) {
328             total_options_per_section[option_name.substr(0, dot_it)]++;
329             dot_it++;
330             dot_it = option_name.find_first_of(".", dot_it);
331         }
332     }
333 
334     // sort options into common sections
335     std::unordered_map<std::string, std::set<std::string>> options_by_section;
336     for (const auto& option : sections_by_option) {
337         for (const auto& section : option.second) {
338             auto section_name = section;
339             auto defined_section_it = m_sections.find(section_name);
340             bool has_descr = defined_section_it != m_sections.end() ?
341                              !defined_section_it->second.description.empty() :
342                              false;
343 
344             // move options from sparse sections to more common parent
345             auto section_count = total_options_per_section[section_name];
346             auto section_end_it = section_name.find_last_of(".");
347             while (!has_descr && section_count < 4 && section_end_it != std::string::npos) {
348                 auto new_section_name = section_name.substr(0, section_end_it);
349                 // prevent moving into dense sections
350                 if (total_options_per_section[new_section_name] > ( 7 - section_count ))
351                     break;
352                 total_options_per_section[section_name]--;
353                 section_name = new_section_name;
354                 section_end_it = section_name.find_last_of(".");
355                 section_count = total_options_per_section[section_name];
356 
357                 defined_section_it = m_sections.find(section_name);
358                 if (defined_section_it != m_sections.end())
359                     has_descr = !defined_section_it->second.description.empty();
360             }
361 
362             options_by_section[section_name].emplace(option.first);
363         }
364     }
365 
366     // define which section are top level sections ("root"), move top level candidates with single option to misc
367     for (const auto& section_it : total_options_per_section) {
368         auto root_name = section_it.first.substr(0, section_it.first.find_first_of("."));
369         // root_name with no dot element allowed to pass if an option is known, potentially moving to misc section
370         auto total_it = total_options_per_section.find(root_name);
371         if (total_it == total_options_per_section.end())
372             continue;
373 
374         if (total_it->second > 1) {
375             options_by_section["root"].emplace(root_name);
376         } else if (section_it.first != "misc" &&
377                    section_it.first != "root" &&
378                    !m_sections.count(section_it.first))
379         {
380             // move option to misc section
381             auto section_option_it = options_by_section.find(section_it.first);
382             if (section_option_it == options_by_section.end())
383                 continue;
384             for (auto&& option : section_option_it->second)
385                 options_by_section["misc"].emplace(std::move(option));
386             options_by_section.erase(section_it.first);
387         }
388     }
389 
390     return options_by_section;
391 }
392 
GetUsage(std::ostream & os,const std::string & command_line,bool allow_unrecognized) const393 void OptionsDB::GetUsage(std::ostream& os, const std::string& command_line, bool allow_unrecognized) const {
394     // Prevent logger output from garbling console display for low severity messages
395     OverrideAllLoggersThresholds(LogLevel::warn);
396 
397     auto options_by_section = OptionsBySection(allow_unrecognized);
398     if (!command_line.empty() || command_line == "all" || command_line == "raw") {
399         // remove the root section if unneeded
400         if (options_by_section.count("root"))
401             options_by_section.erase("root");
402     }
403 
404     // print description of command_line arg as section
405     if (command_line == "all") {
406         os << UserString("OPTIONS_DB_SECTION_ALL") << " ";
407     } else if (command_line == "raw") {
408         os << UserString("OPTIONS_DB_SECTION_RAW") << " ";
409     } else {
410         auto command_section_it = m_sections.find(command_line);
411         if (command_section_it != m_sections.end() && !command_section_it->second.description.empty())
412             os << UserString(command_section_it->second.description) << " ";
413     }
414 
415     bool print_misc_section = command_line.empty();
416     std::set<std::string> section_list {};
417     // print option sections
418     if (command_line != "all" && command_line != "raw") {
419         std::size_t name_col_width = 20;
420         if (command_line.empty()) {
421             auto root_it = options_by_section.find("root");
422             if (root_it != options_by_section.end()) {
423                 for (const auto& section : root_it->second)
424                     if (section.find_first_of(".") == std::string::npos)
425                         if (section_list.emplace(section).second && name_col_width < section.size())
426                             name_col_width = section.size();
427             }
428         } else {
429             for (const auto& it : options_by_section)
430                 if (OptionNameHasParentSection(it.first, command_line))
431                     if (section_list.emplace(it.first).second && name_col_width < it.first.size())
432                         name_col_width = it.first.size();
433         }
434         name_col_width += 5;
435 
436         if (!section_list.empty())
437             os << UserString("COMMAND_LINE_SECTIONS") << ":" << std::endl;
438 
439         auto indents = std::make_pair(2, name_col_width + 4);
440         auto widths = std::make_pair(TERMINAL_LINE_WIDTH - name_col_width, TERMINAL_LINE_WIDTH);
441         for (const auto& section : section_list) {
442             if (section == "misc") {
443                 print_misc_section = true;
444                 continue;
445             }
446             auto section_it = m_sections.find(section);
447             std::string descr = (section_it == m_sections.end()) ? "" : UserString(section_it->second.description);
448 
449             os << std::setw(2) << "" // indent
450                << std::setw(name_col_width) << std::left << section // section name
451                << SplitText(descr, indents, widths); // section description
452         }
453 
454         if (print_misc_section) {
455             // Add special miscellaneous section to bottom
456             os << std::setw(2) << "" << std::setw(name_col_width) << std::left << "misc";
457             os << SplitText(UserString("OPTIONS_DB_SECTION_MISC"), indents, widths);
458         }
459 
460         // add empty line between groups and options
461         if (!section_list.empty() && !print_misc_section)
462             os << std::endl;
463     }
464 
465 
466     // print options
467     if (!command_line.empty()) {
468         std::set<std::string> option_list;
469         if (command_line == "all" || command_line == "raw") {
470             for (const auto& option_section_it : options_by_section)
471                 for (const auto& option : option_section_it.second)
472                     option_list.emplace(option);
473         } else {
474             auto option_section_it = options_by_section.find(command_line);
475             if (option_section_it != options_by_section.end())
476                 option_list = option_section_it->second;
477             // allow traversal by node when no other results are found
478             if (option_list.empty() && section_list.empty())
479                 FindOptions(option_list, command_line, allow_unrecognized);
480         }
481 
482         // insert command_line as option, if it exists
483         if (command_line != "all" && command_line != "raw" && m_options.count(command_line))
484             option_list.emplace(command_line);
485 
486         if (!option_list.empty())
487             os << UserString("COMMAND_LINE_OPTIONS") << ":" << std::endl;
488 
489         for (const auto& option_name : option_list) {
490             auto option_it = m_options.find(option_name);
491             if (option_it == m_options.end() || (!allow_unrecognized && !option_it->second.recognized))
492                 continue;
493 
494             if (command_line == "raw") {
495                 os << option_name << ", " << option_it->second.description << "," << std::endl;
496                 if (option_it->second.short_name)
497                     os << option_it->second.short_name << ", " << option_it->second.description << "," << std::endl;
498             } else {
499                 // option name(s)
500                 if (option_it->second.short_name)
501                     os << "-" << option_it->second.short_name << " | --" << option_name;
502                 else
503                     os << "--" << option_name;
504 
505                 // option description
506                 if (!option_it->second.description.empty())
507                     os << std::endl << SplitText(UserString(option_it->second.description), {5, 7});
508                 else
509                     os << std::endl;
510 
511                 // option default value
512                 if (option_it->second.validator) {
513                     auto validator_str = UserString("COMMAND_LINE_DEFAULT") + ": " + option_it->second.DefaultValueToString();
514                     os << SplitText(validator_str, {5, 7}, {TERMINAL_LINE_WIDTH - validator_str.size(), 77});
515                 }
516                 os << std::endl;
517             }
518         }
519 
520         if (section_list.empty() && option_list.empty()) {
521             os << UserString("COMMAND_LINE_NOT_FOUND") << ": " << command_line << std::endl << std::endl;
522             os << UserString("COMMAND_LINE_USAGE") << std::endl;
523         }
524     }
525 
526     // reset override in case this function is later repurposed
527     OverrideAllLoggersThresholds(boost::none);
528 }
529 
GetXML(XMLDoc & doc,bool non_default_only,bool include_version) const530 void OptionsDB::GetXML(XMLDoc& doc, bool non_default_only, bool include_version) const {
531     doc = XMLDoc();
532 
533     std::vector<XMLElement*> elem_stack;
534     elem_stack.push_back(&doc.root_node);
535 
536     for (const auto& option : m_options) {
537         if (!option.second.storable)
538             continue;
539 
540         if (!option.second.recognized)
541             continue;
542 
543         std::string::size_type last_dot = option.first.find_last_of('.');
544         std::string section_name = last_dot == std::string::npos ? "" : option.first.substr(0, last_dot);
545         std::string name = option.first.substr(last_dot == std::string::npos ? 0 : last_dot + 1);
546 
547         // "version.gl.check.done" is automatically set to true after other logic is performed
548         if (option.first == "version.gl.check.done")
549             continue;
550 
551         // Skip unwanted config options
552         // BUG Some windows may be shown as a child of an other window, but not initially visible.
553         //   The OptionDB default of "*.visible" in these cases may be false, but setting the option to false
554         //   in a config file may prevent such windows from showing when requested.
555         if (name == "visible")
556             continue;
557 
558         // Storing "version.string" in persistent config would render all config options invalid after a new build
559         if (!include_version && option.first == "version.string")
560             continue;
561 
562         // do want to store version string if requested, regardless of whether
563         // it is default. for other strings, if storing non-default only,
564         // check if option is default and if it is, skip it.
565         if (non_default_only && option.first != "version.string") {
566             bool is_default_nonflag = !option.second.flag && IsDefaultValue(m_options.find(option.first));
567             if (is_default_nonflag)
568                 continue;
569 
570             // Default value of flag options will throw bad_any_cast, fortunately they always default to false
571             if (option.second.flag && !boost::any_cast<bool>(option.second.value))
572                 continue;
573         }
574 
575 
576         while (1 < elem_stack.size()) {
577             std::string prev_section = PreviousSectionName(elem_stack);
578             if (prev_section == section_name) {
579                 section_name.clear();
580                 break;
581             } else if (section_name.find(prev_section + '.') == 0) {
582                 section_name = section_name.substr(prev_section.size() + 1);
583                 break;
584             }
585             elem_stack.pop_back();
586         }
587         if (!section_name.empty()) {
588             std::string::size_type last_pos = 0;
589             std::string::size_type pos = 0;
590             while ((pos = section_name.find('.', last_pos)) != std::string::npos) {
591                 XMLElement temp(section_name.substr(last_pos, pos - last_pos));
592                 elem_stack.back()->children.push_back(temp);
593                 elem_stack.push_back(&elem_stack.back()->Child(temp.Tag()));
594                 last_pos = pos + 1;
595             }
596             XMLElement temp(section_name.substr(last_pos));
597             elem_stack.back()->children.push_back(temp);
598             elem_stack.push_back(&elem_stack.back()->Child(temp.Tag()));
599         }
600 
601         XMLElement temp(name);
602         if (option.second.validator) {
603             temp.SetText(option.second.ValueToString());
604         } else if (option.second.flag) {
605             if (!boost::any_cast<bool>(option.second.value))
606                 continue;
607         }
608         elem_stack.back()->children.push_back(temp);
609         elem_stack.push_back(&elem_stack.back()->Child(temp.Tag()));
610     }
611 }
612 
OptionChangedSignal(const std::string & option)613 OptionsDB::OptionChangedSignalType& OptionsDB::OptionChangedSignal(const std::string& option) {
614     auto it = m_options.find(option);
615     if (it == m_options.end())
616         throw std::runtime_error("OptionsDB::OptionChangedSignal() : Attempted to get signal for nonexistent option \"" + option + "\".");
617     return *it->second.option_changed_sig_ptr;
618 }
619 
Remove(const std::string & name)620 void OptionsDB::Remove(const std::string& name) {
621     auto it = m_options.find(name);
622     if (it != m_options.end()) {
623         Option::short_names.erase(it->second.short_name);
624         m_options.erase(it);
625         m_dirty = true;
626     }
627     OptionRemovedSignal(name);
628 }
629 
RemoveUnrecognized(const std::string & prefix)630 void OptionsDB::RemoveUnrecognized(const std::string& prefix) {
631     auto it = m_options.begin();
632     while (it != m_options.end()) {
633         if (!it->second.recognized && it->first.find(prefix) == 0)
634             Remove((it++)->first); // note postfix operator++
635         else
636             ++it;
637     }
638 }
639 
FindOptions(std::set<std::string> & ret,const std::string & prefix,bool allow_unrecognized) const640 void OptionsDB::FindOptions(std::set<std::string>& ret, const std::string& prefix, bool allow_unrecognized) const {
641     ret.clear();
642     for (auto& option : m_options)
643         if ((option.second.recognized || allow_unrecognized) && option.first.find(prefix) == 0)
644             ret.insert(option.first);
645 }
646 
SetFromCommandLine(const std::vector<std::string> & args)647 void OptionsDB::SetFromCommandLine(const std::vector<std::string>& args) {
648     //bool option_changed = false;
649 
650     for (unsigned int i = 1; i < args.size(); ++i) {
651         std::string current_token(args[i]);
652 
653         if (current_token.find("--") == 0) {
654             std::string option_name = current_token.substr(2);
655 
656             if (option_name.empty())
657                 throw std::runtime_error("A \'--\' was given with no option name.");
658 
659             auto it = m_options.find(option_name);
660 
661             if (it == m_options.end() || !it->second.recognized) {
662                 // unrecognized option: may be registered later on so we'll store it for now
663                 // Check for more parameters (if this is the last one, assume that it is a flag).
664                 std::string value_str("-");
665                 if (i + 1 < static_cast<unsigned int>(args.size())) {
666                     value_str = args[i + 1]; // copy assignment
667                     StripQuotation(value_str);
668                 }
669 
670                 if (value_str.at(0) == '-') { // this is either the last parameter or the next parameter is another option, assume this one is a flag
671                     m_options[option_name] = Option(static_cast<char>(0), option_name, true,
672                                                     boost::lexical_cast<std::string>(false),
673                                                     "", 0, false, true, false, std::string());
674                 } else { // the next parameter is the value, store it as a string to be parsed later
675                     m_options[option_name] = Option(static_cast<char>(0), option_name,
676                                                     value_str, value_str, "",
677                                                     new Validator<std::string>(),
678                                                     false, false, false, std::string()); // don't attempt to store options that have only been specified on the command line
679                 }
680 
681                 WarnLogger() << "Option \"" << option_name << "\", was specified on the command line but was not recognized.  It may not be registered yet or could be a typo.";
682 
683             } else {
684                 // recognized option
685                 Option& option = it->second;
686                 if (option.value.empty())
687                     throw std::runtime_error("The value member of option \"--" + option.name + "\" is undefined.");
688 
689                 if (!option.flag) { // non-flag
690                     try {
691                         // check if parameter exists...
692                         if (i + 1 >= static_cast<unsigned int>(args.size())) {
693                             m_dirty |= option.SetFromString("");
694                             continue;
695                         }
696                         // get parameter value
697                         std::string value_str(args[++i]);
698                         StripQuotation(value_str);
699                         // ensure parameter is actually a parameter, and not the next option name (which would indicate
700                         // that the option was specified without a parameter value, as if it was a flag)
701                         if (!value_str.empty() && value_str.at(0) == '-')
702                             throw std::runtime_error("the option \"" + option.name +
703                                                      "\" was followed by the parameter \"" + value_str +
704                                                      "\", which appears to be an option flag, not a parameter value, because it begins with a \"-\" character.");
705                         m_dirty |= option.SetFromString(value_str);
706                     } catch (const std::exception& e) {
707                         throw std::runtime_error("OptionsDB::SetFromCommandLine() : the following exception was caught when attempting to set option \"" + option.name + "\": " + e.what() + "\n\n");
708                     }
709                 } else { // flag
710                     option.value = true;
711                 }
712             }
713 
714             //option_changed = true;
715         } else if (current_token.find('-') == 0
716 #ifdef FREEORION_MACOSX
717                 && current_token.find("-psn") != 0 // Mac OS X passes a process serial number to all applications using Carbon or Cocoa, it should be ignored here
718 #endif
719             )
720         {
721             std::string single_char_options = current_token.substr(1);
722 
723             if (single_char_options.empty())
724                 throw std::runtime_error("A \'-\' was given with no options.");
725 
726             for (unsigned int j = 0; j < single_char_options.size(); ++j) {
727                 auto short_name_it = Option::short_names.find(single_char_options[j]);
728 
729                 if (short_name_it == Option::short_names.end())
730                     throw std::runtime_error(std::string("Unknown option \"-") + single_char_options[j] + "\" was given.");
731 
732                 auto name_it = m_options.find(short_name_it->second);
733 
734                 if (name_it == m_options.end())
735                     throw std::runtime_error("Option \"--" + short_name_it->second + "\", abbreviated as \"-" + short_name_it->first + "\", could not be found.");
736 
737                 Option& option = name_it->second;
738                 if (option.value.empty())
739                     throw std::runtime_error("The value member of option \"--" + option.name + "\" is undefined.");
740 
741                 if (!option.flag) {
742                     if (j < single_char_options.size() - 1) {
743                         throw std::runtime_error(std::string("Option \"-") + single_char_options[j] + "\" was given with no parameter.");
744                     } else {
745                         if (i + 1 >= static_cast<unsigned int>(args.size()))
746                             m_dirty |= option.SetFromString("");
747                         else
748                             m_dirty |= option.SetFromString(args[++i]);
749                     }
750                 } else {
751                     option.value = true;
752                 }
753             }
754         }
755     }
756 }
757 
SetFromFile(const boost::filesystem::path & file_path,const std::string & version)758 void OptionsDB::SetFromFile(const boost::filesystem::path& file_path,
759                             const std::string& version)
760 {
761     XMLDoc doc;
762     try {
763         boost::filesystem::ifstream ifs(file_path);
764         if (ifs) {
765             doc.ReadDoc(ifs);
766             if (version.empty() || (doc.root_node.ContainsChild("version") &&
767                                     doc.root_node.Child("version").ContainsChild("string") &&
768                                     version == doc.root_node.Child("version").Child("string").Text()))
769             { GetOptionsDB().SetFromXML(doc); }
770         }
771     } catch (...) {
772         std::cerr << UserString("UNABLE_TO_READ_CONFIG_XML")  << ": "
773                   << file_path << std::endl;
774     }
775 }
776 
SetFromXML(const XMLDoc & doc)777 void OptionsDB::SetFromXML(const XMLDoc& doc) {
778     for (const XMLElement& child : doc.root_node.children)
779     { SetFromXMLRecursive(child, ""); }
780 }
781 
SetFromXMLRecursive(const XMLElement & elem,const std::string & section_name)782 void OptionsDB::SetFromXMLRecursive(const XMLElement& elem, const std::string& section_name) {
783     std::string option_name = section_name + (section_name.empty() ? "" : ".") + elem.Tag();
784     if (option_name == "version.string")
785         return;
786 
787     if (!elem.children.empty()) {
788         for (const XMLElement& child : elem.children)
789             SetFromXMLRecursive(child, option_name);
790     }
791 
792     auto it = m_options.find(option_name);
793 
794     if (it == m_options.end() || !it->second.recognized) {
795         if (elem.Text().length() == 0) {
796             // do not retain empty XML options
797             return;
798         } else {
799             // Store unrecognized option to be parsed later if this options is added.
800             m_options[option_name] = Option(static_cast<char>(0), option_name,
801                                             elem.Text(), elem.Text(),
802                                             "", new Validator<std::string>(),
803                                             true, false, false, section_name);
804         }
805 
806         TraceLogger() << "Option \"" << option_name << "\", was in config.xml but was not recognized.  It may not be registered yet or you may need to delete your config.xml if it is out of date.";
807         m_dirty = true;
808         return;
809     }
810 
811     Option& option = it->second;
812     //if (!option.flag && option.value.empty()) {
813     //    ErrorLogger() << "The value member of option \"" << option.name << "\" in config.xml is undefined.";
814     //    return;
815     //}
816 
817     if (option.flag) {
818         static auto lexical_true_str = boost::lexical_cast<std::string>(true);
819         option.value = static_cast<bool>(elem.Text() == lexical_true_str);
820     } else {
821         try {
822             m_dirty |= option.SetFromString(elem.Text());
823         } catch (const std::exception& e) {
824             ErrorLogger() << "OptionsDB::SetFromXMLRecursive() : while processing config.xml the following exception was caught when attempting to set option \""
825                           << option_name << "\" to \"" << elem.Text() << "\": " << e.what();
826         }
827     }
828 }
829 
AddSection(const std::string & name,const std::string & description,std::function<bool (const std::string &)> option_predicate)830 void OptionsDB::AddSection(const std::string& name, const std::string& description,
831                            std::function<bool (const std::string&)> option_predicate)
832 {
833     auto insert_result = m_sections.emplace(name, OptionSection(name, description, option_predicate));
834     // if previously existing section, update description/predicate if empty/null
835     if (!insert_result.second) {
836         if (!description.empty() && insert_result.first->second.description.empty())
837             insert_result.first->second.description = description;
838         if (option_predicate != nullptr && insert_result.first->second.option_predicate == nullptr)
839             insert_result.first->second.option_predicate = option_predicate;
840     }
841 }
842 
843 template <>
Get(const std::string & name) const844 std::vector<std::string> OptionsDB::Get<std::vector<std::string>>(const std::string& name) const
845 {
846     auto it = m_options.find(name);
847     if (!OptionExists(it))
848         throw std::runtime_error("OptionsDB::Get<std::vector<std::string>>() : Attempted to get nonexistent option \"" + name + "\".");
849     try {
850         return boost::any_cast<std::vector<std::string>>(it->second.value);
851     } catch (const boost::bad_any_cast& e) {
852         ErrorLogger() << "bad any cast converting value option named: " << name << ". Returning default value instead";
853         try {
854             return boost::any_cast<std::vector<std::string>>(it->second.default_value);
855         } catch (const boost::bad_any_cast& e) {
856             ErrorLogger() << "bad any cast converting default value of std::vector<std::string> option named: " << name << ". Returning empty vector instead";
857             return std::vector<std::string>();
858         }
859     }
860 }
861 
ListToString(const std::vector<std::string> & input_list)862 std::string ListToString(const std::vector<std::string>& input_list) {
863     // list input strings in comma-separated-value format
864     std::string retval;
865     for (auto it = input_list.begin(); it != input_list.end(); ++it) {
866         if (it != input_list.begin())
867             retval += ",";
868         std::string str(*it);
869         boost::remove_erase_if(str, boost::is_any_of("<&>'\",[]|\a\b\f\n\r\t\b"));  // remove XML protected characters and a few other semi-randomly chosen characters to avoid corrupting enclosing XML document structure
870         retval += str;
871     }
872     return retval;
873 }
874 
StringToList(const std::string & input_string)875 std::vector<std::string> StringToList(const std::string& input_string) {
876     std::vector<std::string> retval;
877     typedef boost::tokenizer<boost::char_separator<char>> Tokenizer;
878     boost::char_separator<char> separator(",");
879     Tokenizer tokens(input_string, separator);
880     for (const auto& token : tokens)
881         retval.push_back(token);
882     return retval;
883 }
884