1 #include <wayfire/config/compound-option.hpp>
2 #include <wayfire/config/file.hpp>
3 #include <wayfire/config/types.hpp>
4 #include <wayfire/config/xml.hpp>
5 #include <wayfire/util/log.hpp>
6 #include <sstream>
7 #include <fstream>
8 #include <cassert>
9 #include <set>
10 
11 #include "option-impl.hpp"
12 
13 #include <sys/file.h>
14 #include <fcntl.h>
15 #include <unistd.h>
16 #include <dirent.h>
17 
18 class line_t : public std::string
19 {
20   public:
21     template<class T>
line_t(T source)22     line_t(T source) : std::string(source)
23     {}
24 
line_t()25     line_t() : std::string()
26     {}
27     line_t(const line_t& other) = default;
28     line_t(line_t&& other) = default;
29     line_t& operator =(const line_t& other) = default;
30     line_t& operator =(line_t&& other) = default;
31 
32   public:
substr(size_t start,size_t length=npos) const33     line_t substr(size_t start, size_t length = npos) const
34     {
35         line_t result = std::string::substr(start, length);
36         result.source_line_number = this->source_line_number;
37         return result;
38     }
39 
40     size_t source_line_number;
41 };
42 using lines_t = std::vector<line_t>;
43 
split_to_lines(const std::string & source)44 static lines_t split_to_lines(const std::string& source)
45 {
46     std::istringstream stream(source);
47     lines_t output;
48     line_t line;
49 
50     size_t line_idx = 1;
51     while (std::getline(stream, line))
52     {
53         line.source_line_number = line_idx;
54         output.push_back(line);
55         ++line_idx;
56     }
57 
58     return output;
59 }
60 
61 /**
62  * Check whether at the given index @idx in @line, there is a character
63  * @ch which isn't escaped (i.e preceded by \).
64  */
is_nonescaped(const std::string & line,char ch,int idx)65 static bool is_nonescaped(const std::string& line, char ch, int idx)
66 {
67     return line[idx] == ch && (idx == 0 || line[idx - 1] != '\\');
68 }
69 
find_first_nonescaped(const std::string & line,char ch)70 static size_t find_first_nonescaped(const std::string& line, char ch)
71 {
72     /* Find first not-escaped # */
73     size_t pos = 0;
74     while (pos != std::string::npos && !is_nonescaped(line, ch, pos))
75     {
76         pos = line.find(ch, pos + 1);
77     }
78 
79     return pos;
80 }
81 
remove_escaped_sharps(const line_t & line)82 line_t remove_escaped_sharps(const line_t& line)
83 {
84     line_t result;
85     result.source_line_number = line.source_line_number;
86 
87     bool had_escape = false;
88     for (auto& ch : line)
89     {
90         if ((ch == '#') && had_escape)
91         {
92             result.pop_back();
93         }
94 
95         result    += ch;
96         had_escape = (ch == '\\');
97     }
98 
99     return result;
100 }
101 
remove_comments(const lines_t & lines)102 static lines_t remove_comments(const lines_t& lines)
103 {
104     lines_t result;
105     for (const auto& line : lines)
106     {
107         auto pos = find_first_nonescaped(line, '#');
108         result.push_back(
109             remove_escaped_sharps(line.substr(0, pos)));
110     }
111 
112     return result;
113 }
114 
remove_trailing_whitespace(const lines_t & lines)115 static lines_t remove_trailing_whitespace(const lines_t& lines)
116 {
117     lines_t result;
118     for (const auto& line : lines)
119     {
120         auto result_line = line;
121         while (!result_line.empty() && std::isspace(result_line.back()))
122         {
123             result_line.pop_back();
124         }
125 
126         result.push_back(result_line);
127     }
128 
129     return result;
130 }
131 
join_lines(const lines_t & lines)132 lines_t join_lines(const lines_t& lines)
133 {
134     lines_t result;
135     bool in_concat_mode = false;
136 
137     for (const auto& line : lines)
138     {
139         if (in_concat_mode)
140         {
141             assert(!result.empty());
142             result.back() += line;
143         } else
144         {
145             result.push_back(line);
146         }
147 
148         if (result.empty() || result.back().empty())
149         {
150             in_concat_mode = false;
151         } else
152         {
153             in_concat_mode = (result.back().back() == '\\');
154             if (in_concat_mode) /* pop last \ */
155             {
156                 result.back().pop_back();
157             }
158 
159             /* If last \ was escaped, we should ignore it */
160             bool was_escaped =
161                 !result.back().empty() && result.back().back() == '\\';
162             in_concat_mode = in_concat_mode && !was_escaped;
163         }
164     }
165 
166     return result;
167 }
168 
skip_empty(const lines_t & lines)169 lines_t skip_empty(const lines_t& lines)
170 {
171     lines_t result;
172     for (auto& line : lines)
173     {
174         if (!line.empty())
175         {
176             result.push_back(line);
177         }
178     }
179 
180     return result;
181 }
182 
ignore_leading_trailing_whitespace(const std::string & string)183 static std::string ignore_leading_trailing_whitespace(const std::string& string)
184 {
185     if (string.empty())
186     {
187         return "";
188     }
189 
190     size_t i = 0;
191     size_t j = string.size() - 1;
192     while (i < j && std::isspace(string[i]))
193     {
194         ++i;
195     }
196 
197     while (i < j && std::isspace(string[j]))
198     {
199         --j;
200     }
201 
202     return string.substr(i, j - i + 1);
203 }
204 
205 enum option_parsing_result
206 {
207     /* Line was valid */
208     OPTION_PARSED_OK,
209     /* Line has wrong format */
210     OPTION_PARSED_WRONG_FORMAT,
211     /* Specified value does not match existing option type */
212     OPTION_PARSED_INVALID_CONTENTS,
213 };
214 
215 /**
216  * Try to parse an option line.
217  * If the option line is valid, the corresponding option is modified or added
218  * to @current_section, and the option is added to @reloaded.
219  *
220  * @return The parse status of the line.
221  */
parse_option_line(wf::config::section_t & current_section,const line_t & line,std::set<std::shared_ptr<wf::config::option_base_t>> & reloaded)222 static option_parsing_result parse_option_line(
223     wf::config::section_t& current_section, const line_t& line,
224     std::set<std::shared_ptr<wf::config::option_base_t>>& reloaded)
225 {
226     size_t equal_sign = line.find_first_of("=");
227     if (equal_sign == std::string::npos)
228     {
229         return OPTION_PARSED_WRONG_FORMAT;
230     }
231 
232     auto name  = ignore_leading_trailing_whitespace(line.substr(0, equal_sign));
233     auto value = ignore_leading_trailing_whitespace(line.substr(equal_sign + 1));
234 
235     auto option = current_section.get_option_or(name);
236     if (!option)
237     {
238         using namespace wf;
239         option = std::make_shared<config::option_t<std::string>>(name, "");
240         option->set_value_str(value);
241         current_section.register_new_option(option);
242     }
243 
244     if (option->is_locked() || option->set_value_str(value))
245     {
246         reloaded.insert(option);
247         return OPTION_PARSED_OK;
248     }
249 
250     return OPTION_PARSED_INVALID_CONTENTS;
251 }
252 
253 /**
254  * Check whether the @line is a valid section start.
255  * If yes, it will either return the section in @config with the same name, or
256  * create a new section and register it in config.
257  *
258  * @return nullptr if line is not a valid section, the section otherwise.
259  */
check_section(wf::config::config_manager_t & config,const line_t & line)260 static std::shared_ptr<wf::config::section_t> check_section(
261     wf::config::config_manager_t& config, const line_t& line)
262 {
263     auto name = ignore_leading_trailing_whitespace(line);
264     if (name.empty() || (name.front() != '[') || (name.back() != ']'))
265     {
266         return {};
267     }
268 
269     auto real_name = name.substr(1, name.length() - 2);
270 
271     auto section = config.get_section(real_name);
272     if (!section)
273     {
274         size_t splitter = real_name.find_first_of(":");
275         if (splitter != std::string::npos)
276         {
277             auto obj_type_name = real_name.substr(0, splitter);
278             auto section_name  = real_name.substr(splitter + 1); // only for the
279                                                                  // empty check
280             if (!obj_type_name.empty() && !section_name.empty())
281             {
282                 auto parent_section = config.get_section(obj_type_name);
283                 if (parent_section)
284                 {
285                     section = parent_section->clone_with_name(real_name);
286                     config.merge_section(section);
287                     return section;
288                 }
289             }
290         }
291 
292         section = std::make_shared<wf::config::section_t>(real_name);
293         config.merge_section(section);
294     }
295 
296     return section;
297 }
298 
load_configuration_options_from_string(config_manager_t & config,const std::string & source,const std::string & source_name)299 void wf::config::load_configuration_options_from_string(
300     config_manager_t& config, const std::string& source,
301     const std::string& source_name)
302 {
303     std::set<std::shared_ptr<option_base_t>> reloaded;
304 
305     auto lines =
306         skip_empty(
307             join_lines(
308                 remove_trailing_whitespace(
309                     remove_comments(
310                         split_to_lines(source)))));
311 
312     std::shared_ptr<wf::config::section_t> current_section;
313 
314     for (const auto& line : lines)
315     {
316         auto next_section = check_section(config, line);
317         if (next_section)
318         {
319             current_section = next_section;
320             continue;
321         }
322 
323         if (!current_section)
324         {
325             LOGE("Error in file ", source_name, ":", line.source_line_number,
326                 ", option declared before a section starts!");
327             continue;
328         }
329 
330         auto status = parse_option_line(*current_section, line, reloaded);
331         switch (status)
332         {
333           case OPTION_PARSED_WRONG_FORMAT:
334             LOGE("Error in file ", source_name, ":",
335                 line.source_line_number, ", invalid option format ",
336                 "(allowed <option_name> = <value>)");
337             break;
338 
339           case OPTION_PARSED_INVALID_CONTENTS:
340             LOGE("Error in file ", source_name, ":",
341                 line.source_line_number, ", invalid option value!");
342             break;
343 
344           default:
345             break;
346         }
347     }
348 
349     // Go through all options and reset options which are loaded from the config
350     // string but are not there anymore.
351     for (auto section : config.get_all_sections())
352     {
353         for (auto opt : section->get_registered_options())
354         {
355             opt->priv->option_in_config_file = (reloaded.count(opt) > 0);
356             if (!opt->priv->option_in_config_file && !opt->is_locked())
357             {
358                 opt->reset_to_default();
359             }
360         }
361     }
362 
363     // After resetting all options which are no longer in the config file, make
364     // sure to rebuild compound options as well.
365     for (auto section : config.get_all_sections())
366     {
367         for (auto opt : section->get_registered_options())
368         {
369             auto as_compound = std::dynamic_pointer_cast<compound_option_t>(opt);
370             if (as_compound)
371             {
372                 update_compound_from_section(*as_compound, section);
373             }
374         }
375     }
376 }
377 
save_configuration_options_to_string(const config_manager_t & config)378 std::string wf::config::save_configuration_options_to_string(
379     const config_manager_t& config)
380 {
381     std::vector<std::string> lines;
382 
383     for (auto& section : config.get_all_sections())
384     {
385         lines.push_back("[" + section->get_name() + "]");
386 
387         // Go through each option and add the necessary lines.
388         // Take care so that regular options overwrite compound options
389         // in case of conflict!
390         std::map<std::string, std::string> option_values;
391         std::set<std::string> all_compound_prefixes;
392         for (auto& option : section->get_registered_options())
393         {
394             auto as_compound = std::dynamic_pointer_cast<compound_option_t>(option);
395             if (as_compound)
396             {
397                 auto value = as_compound->get_value_untyped();
398                 const auto& prefixes = as_compound->get_entries();
399                 for (auto& p : prefixes)
400                 {
401                     all_compound_prefixes.insert(p->get_prefix());
402                 }
403 
404                 for (size_t i = 0; i < value.size(); i++)
405                 {
406                     for (size_t j = 0; j < prefixes.size(); j++)
407                     {
408                         auto full_name = prefixes[j]->get_prefix() + value[i][0];
409                         option_values[full_name] = value[i][j + 1];
410                     }
411                 }
412             }
413         }
414 
415         // An option is part of a compound option if it begins with any of the
416         // prefixes.
417         const auto& is_part_of_compound_option = [&] (const std::string& name)
418         {
419             return std::any_of(
420                 all_compound_prefixes.begin(), all_compound_prefixes.end(),
421                 [&] (const auto& prefix)
422             {
423                 return name.substr(0, prefix.size()) == prefix;
424             });
425         };
426 
427         for (auto& option : section->get_registered_options())
428         {
429             auto as_compound = std::dynamic_pointer_cast<compound_option_t>(option);
430             if (!as_compound)
431             {
432                 // Check whether this option does not conflict with a compound
433                 // option entry.
434                 if (xml::get_option_xml_node(option) ||
435                     !is_part_of_compound_option(option->get_name()))
436                 {
437                     option_values[option->get_name()] = option->get_value_str();
438                 }
439             }
440         }
441 
442         for (auto& [name, value] : option_values)
443         {
444             lines.push_back(name + " = " + value);
445         }
446 
447         lines.push_back("");
448     }
449 
450     /* Check which characters need escaping */
451     for (auto& line : lines)
452     {
453         size_t sharp = line.find_first_of("#");
454         while (sharp != line.npos)
455         {
456             line.insert(line.begin() + sharp, '\\');
457             sharp = line.find_first_of("#", sharp + 2);
458         }
459 
460         if (!line.empty() && (line.back() == '\\'))
461         {
462             line += '\\';
463         }
464     }
465 
466     std::string result;
467     for (const auto& line : lines)
468     {
469         result += line + "\n";
470     }
471 
472     return result;
473 }
474 
load_file_contents(const std::string & file)475 static std::string load_file_contents(const std::string& file)
476 {
477     std::ifstream infile(file);
478     std::string file_contents((std::istreambuf_iterator<char>(infile)),
479         std::istreambuf_iterator<char>());
480 
481     return file_contents;
482 }
483 
load_configuration_options_from_file(config_manager_t & manager,const std::string & file)484 bool wf::config::load_configuration_options_from_file(config_manager_t& manager,
485     const std::string& file)
486 {
487     /* Try to lock the file */
488     auto fd = open(file.c_str(), O_RDONLY);
489     if (flock(fd, LOCK_SH | LOCK_NB))
490     {
491         close(fd);
492         return false;
493     }
494 
495     auto file_contents = load_file_contents(file);
496 
497     /* Release lock */
498     flock(fd, LOCK_UN);
499     close(fd);
500 
501     load_configuration_options_from_string(manager, file_contents, file);
502     return true;
503 }
504 
save_configuration_to_file(const wf::config::config_manager_t & manager,const std::string & file)505 void wf::config::save_configuration_to_file(
506     const wf::config::config_manager_t& manager, const std::string& file)
507 {
508     auto contents = save_configuration_options_to_string(manager);
509     contents.pop_back(); // remove last newline
510 
511     auto fd = open(file.c_str(), O_RDONLY);
512     flock(fd, LOCK_EX);
513 
514     auto fout = std::ofstream(file, std::ios::trunc);
515     fout << contents;
516 
517     flock(fd, LOCK_UN);
518     close(fd);
519 
520     /* Modify the file one last time. Now programs waiting for updates can
521      * acquire a shared lock. */
522     fout << std::endl;
523 }
524 
525 /**
526  * Parse the XML file and return the node which corresponds to the section.
527  */
find_section_start_node(const std::string & file)528 static xmlNodePtr find_section_start_node(const std::string& file)
529 {
530     auto doc = xmlParseFile(file.c_str());
531     if (!doc)
532     {
533         LOGE("Failed to parse XML file ", file);
534         return nullptr;
535     }
536 
537     auto root = xmlDocGetRootElement(doc);
538     if (!root)
539     {
540         LOGE(file, ": missing root element.");
541         xmlFreeDoc(doc);
542         return nullptr;
543     }
544 
545     /* Seek the plugin section */
546     auto section = root->children;
547     while (section != nullptr)
548     {
549         if ((section->type == XML_ELEMENT_NODE) &&
550             (((const char*)section->name == (std::string)"plugin") ||
551              ((const char*)section->name == (std::string)"object")))
552         {
553             break;
554         }
555 
556         section = section->next;
557     }
558 
559     return section;
560 }
561 
load_xml_files(const std::vector<std::string> & xmldirs)562 static wf::config::config_manager_t load_xml_files(
563     const std::vector<std::string>& xmldirs)
564 {
565     wf::config::config_manager_t manager;
566 
567     for (auto& xmldir : xmldirs)
568     {
569         auto xmld = opendir(xmldir.c_str());
570         if (NULL == xmld)
571         {
572             LOGW("Failed to open XML directory ", xmldir);
573             continue;
574         }
575 
576         LOGI("Reading XML configuration options from directory ", xmldir);
577         struct dirent *entry;
578         while ((entry = readdir(xmld)) != NULL)
579         {
580             if ((entry->d_type != DT_LNK) && (entry->d_type != DT_REG))
581             {
582                 continue;
583             }
584 
585             std::string filename = xmldir + '/' + entry->d_name;
586             if ((filename.length() > 4) &&
587                 (filename.rfind(".xml") == filename.length() - 4))
588             {
589                 LOGI("Reading XML configuration options from file ", filename);
590                 auto node = find_section_start_node(filename);
591                 if (node)
592                 {
593                     manager.merge_section(
594                         wf::config::xml::create_section_from_xml_node(node));
595                 }
596             }
597         }
598 
599         closedir(xmld);
600     }
601 
602     return manager;
603 }
604 
override_defaults(wf::config::config_manager_t & manager,const std::string & sysconf)605 void override_defaults(wf::config::config_manager_t& manager,
606     const std::string& sysconf)
607 {
608     auto sysconf_str = load_file_contents(sysconf);
609 
610     wf::config::config_manager_t overrides;
611     load_configuration_options_from_string(overrides, sysconf_str, sysconf);
612     for (auto& section : overrides.get_all_sections())
613     {
614         for (auto& option : section->get_registered_options())
615         {
616             auto full_name   = section->get_name() + '/' + option->get_name();
617             auto real_option = manager.get_option(full_name);
618             if (real_option)
619             {
620                 if (!real_option->set_default_value_str(
621                     option->get_value_str()))
622                 {
623                     LOGW("Invalid value for ", full_name, " in ", sysconf);
624                 } else
625                 {
626                     /* Set the value to the new default */
627                     real_option->reset_to_default();
628                 }
629             } else
630             {
631                 LOGW("Unused default value for ", full_name, " in ", sysconf);
632             }
633         }
634     }
635 }
636 
637 #include <iostream>
638 
build_configuration(const std::vector<std::string> & xmldirs,const std::string & sysconf,const std::string & userconf)639 wf::config::config_manager_t wf::config::build_configuration(
640     const std::vector<std::string>& xmldirs, const std::string& sysconf,
641     const std::string& userconf)
642 {
643     auto manager = load_xml_files(xmldirs);
644     override_defaults(manager, sysconf);
645     load_configuration_options_from_file(manager, userconf);
646     return manager;
647 }
648