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