preset.cpp 7.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206
  1. #include "arg.h"
  2. #include "preset.h"
  3. #include "peg-parser.h"
  4. #include "log.h"
  5. #include <fstream>
  6. #include <sstream>
  7. #include <filesystem>
  8. static std::string rm_leading_dashes(const std::string & str) {
  9. size_t pos = 0;
  10. while (pos < str.size() && str[pos] == '-') {
  11. ++pos;
  12. }
  13. return str.substr(pos);
  14. }
  15. std::vector<std::string> common_preset::to_args() const {
  16. std::vector<std::string> args;
  17. for (const auto & [opt, value] : options) {
  18. args.push_back(opt.args.back()); // use the last arg as the main arg
  19. if (opt.value_hint == nullptr && opt.value_hint_2 == nullptr) {
  20. // flag option, no value
  21. if (common_arg_utils::is_falsey(value)) {
  22. // use negative arg if available
  23. if (!opt.args_neg.empty()) {
  24. args.back() = opt.args_neg.back();
  25. } else {
  26. // otherwise, skip the flag
  27. // TODO: maybe throw an error instead?
  28. args.pop_back();
  29. }
  30. }
  31. }
  32. if (opt.value_hint != nullptr) {
  33. // single value
  34. args.push_back(value);
  35. }
  36. if (opt.value_hint != nullptr && opt.value_hint_2 != nullptr) {
  37. throw std::runtime_error(string_format(
  38. "common_preset::to_args(): option '%s' has two values, which is not supported yet",
  39. opt.args.back()
  40. ));
  41. }
  42. }
  43. return args;
  44. }
  45. std::string common_preset::to_ini() const {
  46. std::ostringstream ss;
  47. ss << "[" << name << "]\n";
  48. for (const auto & [opt, value] : options) {
  49. auto espaced_value = value;
  50. string_replace_all(espaced_value, "\n", "\\\n");
  51. ss << rm_leading_dashes(opt.args.back()) << " = ";
  52. ss << espaced_value << "\n";
  53. }
  54. ss << "\n";
  55. return ss.str();
  56. }
  57. static std::map<std::string, std::map<std::string, std::string>> parse_ini_from_file(const std::string & path) {
  58. std::map<std::string, std::map<std::string, std::string>> parsed;
  59. if (!std::filesystem::exists(path)) {
  60. throw std::runtime_error("preset file does not exist: " + path);
  61. }
  62. std::ifstream file(path);
  63. if (!file.good()) {
  64. throw std::runtime_error("failed to open server preset file: " + path);
  65. }
  66. std::string contents((std::istreambuf_iterator<char>(file)), std::istreambuf_iterator<char>());
  67. static const auto parser = build_peg_parser([](auto & p) {
  68. // newline ::= "\r\n" / "\n" / "\r"
  69. auto newline = p.rule("newline", p.literal("\r\n") | p.literal("\n") | p.literal("\r"));
  70. // ws ::= [ \t]*
  71. auto ws = p.rule("ws", p.chars("[ \t]", 0, -1));
  72. // comment ::= [;#] (!newline .)*
  73. auto comment = p.rule("comment", p.chars("[;#]", 1, 1) + p.zero_or_more(p.negate(newline) + p.any()));
  74. // eol ::= ws comment? (newline / EOF)
  75. auto eol = p.rule("eol", ws + p.optional(comment) + (newline | p.end()));
  76. // ident ::= [a-zA-Z_] [a-zA-Z0-9_.-]*
  77. auto ident = p.rule("ident", p.chars("[a-zA-Z_]", 1, 1) + p.chars("[a-zA-Z0-9_.-]", 0, -1));
  78. // value ::= (!eol-start .)*
  79. auto eol_start = p.rule("eol-start", ws + (p.chars("[;#]", 1, 1) | newline | p.end()));
  80. auto value = p.rule("value", p.zero_or_more(p.negate(eol_start) + p.any()));
  81. // header-line ::= "[" ws ident ws "]" eol
  82. auto header_line = p.rule("header-line", "[" + ws + p.tag("section-name", p.chars("[^]]")) + ws + "]" + eol);
  83. // kv-line ::= ident ws "=" ws value eol
  84. auto kv_line = p.rule("kv-line", p.tag("key", ident) + ws + "=" + ws + p.tag("value", value) + eol);
  85. // comment-line ::= ws comment (newline / EOF)
  86. auto comment_line = p.rule("comment-line", ws + comment + (newline | p.end()));
  87. // blank-line ::= ws (newline / EOF)
  88. auto blank_line = p.rule("blank-line", ws + (newline | p.end()));
  89. // line ::= header-line / kv-line / comment-line / blank-line
  90. auto line = p.rule("line", header_line | kv_line | comment_line | blank_line);
  91. // ini ::= line* EOF
  92. auto ini = p.rule("ini", p.zero_or_more(line) + p.end());
  93. return ini;
  94. });
  95. common_peg_parse_context ctx(contents);
  96. const auto result = parser.parse(ctx);
  97. if (!result.success()) {
  98. throw std::runtime_error("failed to parse server config file: " + path);
  99. }
  100. std::string current_section = COMMON_PRESET_DEFAULT_NAME;
  101. std::string current_key;
  102. ctx.ast.visit(result, [&](const auto & node) {
  103. if (node.tag == "section-name") {
  104. const std::string section = std::string(node.text);
  105. current_section = section;
  106. parsed[current_section] = {};
  107. } else if (node.tag == "key") {
  108. const std::string key = std::string(node.text);
  109. current_key = key;
  110. } else if (node.tag == "value" && !current_key.empty() && !current_section.empty()) {
  111. parsed[current_section][current_key] = std::string(node.text);
  112. current_key.clear();
  113. }
  114. });
  115. return parsed;
  116. }
  117. static std::map<std::string, common_arg> get_map_key_opt(common_params_context & ctx_params) {
  118. std::map<std::string, common_arg> mapping;
  119. for (const auto & opt : ctx_params.options) {
  120. for (const auto & env : opt.get_env()) {
  121. mapping[env] = opt;
  122. }
  123. for (const auto & arg : opt.get_args()) {
  124. mapping[rm_leading_dashes(arg)] = opt;
  125. }
  126. }
  127. return mapping;
  128. }
  129. static bool is_bool_arg(const common_arg & arg) {
  130. return !arg.args_neg.empty();
  131. }
  132. static std::string parse_bool_arg(const common_arg & arg, const std::string & key, const std::string & value) {
  133. // if this is a negated arg, we need to reverse the value
  134. for (const auto & neg_arg : arg.args_neg) {
  135. if (rm_leading_dashes(neg_arg) == key) {
  136. return common_arg_utils::is_truthy(value) ? "false" : "true";
  137. }
  138. }
  139. // otherwise, not negated
  140. return value;
  141. }
  142. common_presets common_presets_load(const std::string & path, common_params_context & ctx_params) {
  143. common_presets out;
  144. auto key_to_opt = get_map_key_opt(ctx_params);
  145. auto ini_data = parse_ini_from_file(path);
  146. for (auto section : ini_data) {
  147. common_preset preset;
  148. if (section.first.empty()) {
  149. preset.name = COMMON_PRESET_DEFAULT_NAME;
  150. } else {
  151. preset.name = section.first;
  152. }
  153. LOG_DBG("loading preset: %s\n", preset.name.c_str());
  154. for (const auto & [key, value] : section.second) {
  155. LOG_DBG("option: %s = %s\n", key.c_str(), value.c_str());
  156. if (key_to_opt.find(key) != key_to_opt.end()) {
  157. auto & opt = key_to_opt[key];
  158. if (is_bool_arg(opt)) {
  159. preset.options[opt] = parse_bool_arg(opt, key, value);
  160. } else {
  161. preset.options[opt] = value;
  162. }
  163. LOG_DBG("accepted option: %s = %s\n", key.c_str(), preset.options[opt].c_str());
  164. } else {
  165. // TODO: maybe warn about unknown key?
  166. }
  167. }
  168. out[preset.name] = preset;
  169. }
  170. return out;
  171. }