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