| /** |
| @section license License |
| |
| Licensed to the Apache Software Foundation (ASF) under one |
| or more contributor license agreements. See the NOTICE file |
| distributed with this work for additional information |
| regarding copyright ownership. The ASF licenses this file |
| to you under the Apache License, Version 2.0 (the |
| "License"); you may not use this file except in compliance |
| with the License. You may obtain a copy of the License at |
| |
| http://www.apache.org/licenses/LICENSE-2.0 |
| |
| Unless required by applicable law or agreed to in writing, software |
| distributed under the License is distributed on an "AS IS" BASIS, |
| WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| See the License for the specific language governing permissions and |
| limitations under the License. |
| |
| Copyright 2019, Oath Inc. |
| */ |
| |
| #include <string> |
| #include <map> |
| #include <numeric> |
| #include <utility> |
| #include <glob.h> |
| |
| #include <swoc/TextView.h> |
| #include <swoc/MemSpan.h> |
| #include <swoc/swoc_file.h> |
| #include <swoc/bwf_std.h> |
| #include <swoc/Scalar.h> |
| #include <yaml-cpp/yaml.h> |
| |
| #include "txn_box/Directive.h" |
| #include "txn_box/Extractor.h" |
| #include "txn_box/Modifier.h" |
| #include "txn_box/Expr.h" |
| #include "txn_box/Config.h" |
| #include "txn_box/Context.h" |
| |
| #include "txn_box/ts_util.h" |
| #include "txn_box/yaml_util.h" |
| |
| using swoc::BufferWriter; |
| using swoc::Errata; |
| using swoc::Rv; |
| using swoc::TextView; |
| namespace bwf = swoc::bwf; |
| using namespace swoc::literals; |
| |
| static constexpr char ARG_PREFIX = '<'; |
| static constexpr char ARG_SUFFIX = '>'; |
| |
| /* ------------------------------------------------------------------------------------ */ |
| swoc::Lexicon<Hook> HookName{ |
| {{Hook::POST_LOAD, {"post-load"}}, |
| {Hook::TXN_START, {"txn-open"}}, |
| {Hook::CREQ, {"ua-req", "creq", "client-req"}}, |
| {Hook::PREQ, {"proxy-req", "preq", "upstream-req", "up-req"}}, |
| {Hook::URSP, {"upstream-rsp", "ursp", "up-rsp"}}, |
| {Hook::PRSP, {"proxy-rsp", "prsp"}}, |
| {Hook::PRE_REMAP, {"pre-remap"}}, |
| {Hook::POST_REMAP, {"post-remap"}}, |
| {Hook::TXN_CLOSE, {"txn-close"}}, |
| {Hook::REMAP, {"remap"}}, |
| {Hook::MSG, {"msg"}}} |
| }; |
| |
| std::array<TSHttpHookID, std::tuple_size<Hook>::value> TS_Hook; |
| |
| BufferWriter & |
| bwformat(BufferWriter &w, bwf::Spec const &spec, Hook hook) |
| { |
| if (spec.has_numeric_type()) { |
| return bwformat(w, spec, IndexFor(hook)); |
| } |
| return bwformat(w, spec, HookName[hook]); |
| } |
| |
| namespace |
| { |
| [[maybe_unused]] bool INITIALIZED = []() -> bool { |
| HookName.set_default(Hook::INVALID); |
| |
| TS_Hook[IndexFor(Hook::TXN_START)] = TS_HTTP_TXN_START_HOOK; |
| TS_Hook[IndexFor(Hook::CREQ)] = TS_HTTP_READ_REQUEST_HDR_HOOK; |
| TS_Hook[IndexFor(Hook::PREQ)] = TS_HTTP_SEND_REQUEST_HDR_HOOK; |
| TS_Hook[IndexFor(Hook::URSP)] = TS_HTTP_READ_RESPONSE_HDR_HOOK; |
| TS_Hook[IndexFor(Hook::PRSP)] = TS_HTTP_SEND_RESPONSE_HDR_HOOK; |
| TS_Hook[IndexFor(Hook::PRE_REMAP)] = TS_HTTP_PRE_REMAP_HOOK; |
| TS_Hook[IndexFor(Hook::POST_REMAP)] = TS_HTTP_POST_REMAP_HOOK; |
| TS_Hook[IndexFor(Hook::TXN_CLOSE)] = TS_HTTP_TXN_CLOSE_HOOK; |
| |
| return true; |
| }(); |
| }; // namespace |
| |
| swoc::Lexicon<ValueType> const ValueTypeNames{ |
| {{ValueType::NIL, "nil"}, |
| {ValueType::STRING, "string"}, |
| {ValueType::INTEGER, "integer"}, |
| {ValueType::BOOLEAN, "boolean"}, |
| {ValueType::FLOAT, "float"}, |
| {ValueType::IP_ADDR, "IP address"}, |
| {ValueType::DURATION, "duration"}, |
| {ValueType::TIMEPOINT, "time point"}, |
| {ValueType::CONS, "cons"}, |
| {ValueType::TUPLE, "tuple"}, |
| {ValueType::GENERIC, "generic"}} |
| }; |
| |
| // -------------------------------------------------------------------------- |
| Config::~Config() |
| { |
| // Invoke all the finalizers to do additional cleanup. |
| for (auto &&f : _finalizers) { |
| f._f(f._ptr); |
| std::destroy_at(&f._f); // clean up the cleaner too, just in case. |
| } |
| } |
| |
| template <typename F> struct on_scope_exit { |
| F _f; |
| explicit on_scope_exit(F &&f) : _f(std::move(f)) {} |
| ~on_scope_exit() { _f(); } |
| }; |
| |
| swoc::Rv<swoc::TextView> |
| parse_arg(TextView &key) |
| { |
| TextView arg{key}; |
| TextView name{arg.take_prefix_at(ARG_PREFIX)}; |
| if (name.size() == key.size()) { // no arg prefix, it's just the name. |
| return {}; |
| } |
| if (arg.empty() || arg.back() != ARG_SUFFIX) { |
| return Errata(S_ERROR, R"(Argument for "{}" is not properly terminated with '{}'.)", name, ARG_SUFFIX); |
| } |
| key = name; |
| return arg.remove_suffix(1); |
| } |
| |
| /* ------------------------------------------------------------------------------------ */ |
| Config::Config() : _arena(_cfg_storage_required + 2048) |
| { |
| _cfg_store = _arena.alloc(_cfg_storage_required); |
| // Set up the run time type information for the directives. |
| _drtv_info = this->alloc_span<Directive::CfgStaticData>(_factory.size()); |
| for (auto const &[name, factory_info] : _factory) { |
| auto &di = _drtv_info[factory_info._idx]; |
| new (&di) Directive::CfgStaticData; |
| di._static = &factory_info; |
| } |
| } |
| |
| swoc::MemSpan<void> |
| Config::allocate_cfg_storage(size_t n, size_t align) |
| { |
| if (align == 1) { |
| return _arena.alloc(n); |
| } |
| |
| // Need to add this logic to @c MemArena - shouldn't have to do it here. |
| // Should only loop twice at most - if it doesn't return it requires the arena to have |
| // enough free space to work on the second try. |
| while (true) { |
| auto span = _arena.remnant(); |
| if (auto remainder = reinterpret_cast<uintptr_t>(span.data()) % align; remainder) { |
| auto pad = align - remainder; |
| if (span.size() >= n + pad) { |
| return _arena.alloc(n + pad).remove_prefix(pad); |
| } |
| _arena.require(n + pad); // Guarantee enough space to align correctly. |
| } else if (span.size() >= n) { |
| return _arena.alloc(n); // already aligned and big enough. |
| } else { |
| _arena.require(n + align); // bump up to make sure new space can be aligned. |
| } |
| } |
| } |
| |
| ReservedSpan |
| Config::reserve_ctx_storage(size_t n) |
| { |
| using Align = swoc::Scalar<8>; |
| // Pre-block to store status of the reserved memory. |
| _ctx_storage_required += Align(swoc::round_up(sizeof(Context::ReservedStatus))); |
| // The actual reservation. |
| ReservedSpan span{_ctx_storage_required, n}; |
| _ctx_storage_required += Align(swoc::round_up(n)); |
| return span; |
| } |
| |
| Config::self_type & |
| Config::localize(Feature &feature) |
| { |
| std::visit([&](auto &t) { this->localize(t); }, static_cast<Feature::variant_type &>(feature)); |
| return *this; |
| } |
| |
| TextView & |
| Config::localize(TextView &text, LocalOpt opt) |
| { |
| if (text.size()) { |
| if (LOCAL_CSTR == opt) { |
| auto span{_arena.alloc(text.size() + 1).rebind<char>()}; |
| memcpy(span, text); |
| span[text.size()] = 0; |
| text = span.subspan(0, text.size()); |
| } else { |
| auto span{_arena.alloc(text.size()).rebind<char>()}; |
| memcpy(span, text); |
| text = span; |
| } |
| } |
| return text; |
| }; |
| |
| Rv<ActiveType> |
| Config::validate(Extractor::Spec &spec) |
| { |
| if (spec._name.empty()) { |
| return Errata(S_ERROR, R"(Extractor name required but not found.)"); |
| } |
| |
| if (spec._idx < 0) { |
| auto name = TextView{spec._name}; |
| auto &&[arg, arg_errata]{parse_arg(name)}; |
| if (!arg_errata.is_ok()) { |
| return std::move(arg_errata); |
| } |
| |
| Extractor *ex = nullptr; |
| if (_local_extractors) { |
| if (auto spot = _local_extractors->find(name); spot != _local_extractors->end()) { |
| ex = spot->second; |
| } |
| } |
| |
| if (nullptr == ex) { |
| ex = Extractor::find(name); |
| } |
| |
| if (nullptr != ex) { |
| spec._exf = ex; |
| spec._name = this->localize(name); |
| spec._ext = this->localize(spec._ext); |
| auto &&[vt, errata]{ex->validate(*this, spec, arg)}; |
| if (!errata.is_ok()) { |
| return std::move(errata); |
| } |
| return vt; |
| } |
| return Errata(S_ERROR, R"(Extractor "{}" not found.)", name); |
| } |
| return {STRING}; // non-negative index => capture group => always a string |
| } |
| |
| Rv<Expr> |
| Config::parse_unquoted_expr(swoc::TextView const &text) |
| { |
| // Integer? |
| TextView parsed; |
| auto n = swoc::svtoi(text, &parsed); |
| if (parsed.size() == text.size()) { |
| return Expr{Feature{n}}; |
| } |
| |
| // bool? |
| auto b = BoolNames[text]; |
| if (b != BoolTag::INVALID) { |
| return Expr{Feature{b == BoolTag::True}}; |
| } |
| |
| // float? |
| double f = swoc::svtod(text, &parsed); |
| if (parsed.size() == text.size()) { |
| return Expr{Feature{f}}; |
| } |
| |
| // IP Address? |
| swoc::IPAddr addr; |
| if (addr.load(text)) { |
| return Expr{Feature{addr}}; |
| } |
| |
| // Presume an extractor. |
| Extractor::Spec spec; |
| bool valid_p = spec.parse(text); |
| if (!valid_p) { |
| return Errata(S_ERROR, R"(Invalid syntax for extractor "{}" - not a valid specifier.)", text); |
| } |
| auto &&[vt, errata] = this->validate(spec); |
| if (!errata.is_ok()) { |
| return std::move(errata); |
| } |
| |
| if (vt.is_cfg_const()) { |
| return Expr{spec._exf->extract(*this, spec)}; |
| } |
| |
| return Expr{spec, vt}; |
| } |
| |
| Rv<Expr> |
| Config::parse_composite_expr(TextView const &text) |
| { |
| ActiveType single_vt; |
| auto parser{swoc::bwf::Format::bind(text)}; |
| std::vector<Extractor::Spec> specs; // hold the specifiers during parse. |
| // Used to handle literals in @a format_string. Can't be const because it must be updated |
| // for each literal. |
| Extractor::Spec literal_spec; |
| |
| literal_spec._type = Extractor::Spec::LITERAL_TYPE; |
| |
| while (parser) { |
| Extractor::Spec spec; |
| std::string_view literal; |
| bool spec_p = false; |
| try { |
| spec_p = parser(literal, spec); |
| } catch (std::exception const &exp) { |
| return Errata(S_ERROR, "Invalid syntax - {}", exp.what()); |
| } catch (...) { |
| return Errata(S_ERROR, "Invalid syntax."); |
| } |
| |
| if (!literal.empty()) { // leading literal text, add as specifier. |
| literal_spec._ext = this->localize(literal, LOCAL_CSTR); |
| specs.push_back(literal_spec); |
| } |
| |
| if (spec_p) { // specifier present. |
| if (spec._idx >= 0) { |
| specs.push_back(spec); // group reference, store it. |
| } else { |
| auto &&[vt, errata] = this->validate(spec); |
| if (errata.is_ok()) { |
| single_vt = vt; // Save for singleton case. |
| specs.push_back(spec); |
| } else { |
| errata.note(R"(While parsing specifier at offset {}.)", text.size() - parser._fmt.size()); |
| return std::move(errata); |
| } |
| } |
| } |
| } |
| |
| // If it is a singleton, return it as one of the singleton types. |
| if (specs.size() == 1) { |
| if (specs[0]._exf) { |
| return Expr{specs[0], single_vt}; |
| } else if (specs[0]._type == Extractor::Spec::LITERAL_TYPE) { |
| FeatureView f{specs[0]._ext}; |
| f._literal_p = true; // because it was localized when parsed from the composite. |
| f._cstr_p = true; // and automatically null terminated. |
| return Expr{f}; |
| } |
| // else it's an indexed specifier, treat as a composite. |
| } |
| // Multiple specifiers, check for overall properties. |
| Expr expr; |
| auto &cexpr = expr._raw.emplace<Expr::COMPOSITE>(); |
| cexpr._specs.swap(specs); |
| |
| for (auto const &s : specs) { |
| expr._max_arg_idx = std::max(expr._max_arg_idx, s._idx); |
| } |
| |
| return expr; |
| } |
| |
| Rv<Expr> |
| Config::parse_scalar_expr(YAML::Node node) |
| { |
| Rv<Expr> zret; |
| TextView text{node.Scalar()}; |
| if (node.IsNull()) { |
| return Expr{}; |
| } else if (node.Tag() == "?"_tv) { // unquoted, must be extractor. |
| zret = this->parse_unquoted_expr(text); |
| } else { |
| zret = this->parse_composite_expr(text); |
| } |
| |
| if (zret.is_ok()) { |
| auto &expr = zret.result(); |
| if (expr._max_arg_idx >= 0) { |
| if (_active_capture._count == 0) { |
| return Errata(S_ERROR, R"(Regular expression capture group used at {} but no regular expression is active.)", node.Mark()); |
| } else if (expr._max_arg_idx >= int(_active_capture._count)) { |
| return Errata( |
| S_ERROR, |
| R"(Regular expression capture group {} used at {} but the maximum capture group is {} in the active regular expression from line {}.)", |
| expr._max_arg_idx, node.Mark(), _active_capture._count - 1, _active_capture._line); |
| } |
| } |
| } |
| return zret; |
| } |
| |
| Rv<Expr> |
| Config::parse_expr_with_mods(YAML::Node node) |
| { |
| auto &&[expr, expr_errata]{this->parse_expr(node[0])}; |
| if (!expr_errata.is_ok()) { |
| expr_errata.note("While processing the expression at {}.", node.Mark()); |
| return std::move(expr_errata); |
| } |
| auto scope{this->feature_scope(expr.result_type())}; |
| for (unsigned idx = 1; idx < node.size(); ++idx) { |
| auto child{node[idx]}; |
| auto &&[mod, mod_errata]{Modifier::load(*this, child, expr.result_type())}; |
| if (!mod_errata.is_ok()) { |
| mod_errata.note(R"(While parsing feature expression at {}.)", child.Mark(), node.Mark()); |
| return std::move(mod_errata); |
| } |
| _active_feature._type = mod->result_type(_active_feature._type); |
| expr._mods.emplace_back(std::move(mod)); |
| } |
| |
| return std::move(expr); |
| } |
| |
| Rv<Expr> |
| Config::parse_expr(YAML::Node expr_node) |
| { |
| std::string_view expr_tag(expr_node.Tag()); |
| |
| // This is the base entry method, so it needs to handle all cases, although most of them |
| // will be delegated. Handle the direct / simple special cases here. |
| |
| if (expr_node.IsNull()) { // explicit NULL |
| return Expr{NIL_FEATURE}; |
| } |
| |
| // If explicitly marked a literal, then no further processing should be done. |
| if (0 == strcasecmp(expr_tag, LITERAL_TAG)) { |
| if (!expr_node.IsScalar()) { |
| return Errata(S_ERROR, R"("!{}" tag used on value at {} which is not a string as required for a literal.)", LITERAL_TAG, |
| expr_node.Mark()); |
| } |
| return Expr{FeatureView::Literal(this->localize(expr_node.Scalar()))}; |
| } else if (0 == strcasecmp(expr_tag, DURATION_TAG)) { |
| if (!expr_node.IsScalar()) { |
| return Errata(S_ERROR, R"("!{}" tag used on value at {} which is not a string as required for a literal.)", LITERAL_TAG, |
| expr_node.Mark()); |
| } |
| auto &&[dt, dt_errata]{Feature{expr_node.Scalar()}.as_duration()}; |
| return {Expr(dt), std::move(dt_errata)}; |
| } else if (0 != strcasecmp(expr_tag, "?"_sv) && 0 != strcasecmp(expr_tag, "!"_sv)) { |
| return Errata(S_ERROR, R"("{}" tag for extractor expression is not supported.)", expr_tag); |
| } |
| |
| if (expr_node.IsScalar()) { |
| return this->parse_scalar_expr(expr_node); |
| } |
| if (!expr_node.IsSequence()) { |
| return Errata(S_ERROR, "Feature expression is not properly structured."); |
| } |
| |
| // It's a sequence, handle the various cases. |
| if (expr_node.size() == 0) { |
| return Expr{NIL_FEATURE}; |
| } |
| if (expr_node.size() == 1) { |
| return this->parse_scalar_expr(expr_node[0]); |
| } |
| |
| if (expr_node[1].IsMap()) { // base expression with modifiers. |
| return this->parse_expr_with_mods(expr_node); |
| } |
| |
| // Else, after all this, it's a tuple, treat each element as an expression. |
| ActiveType l_types; |
| bool literal_p = true; // Is the entire sequence literal? |
| std::vector<Expr> xa; |
| xa.reserve(expr_node.size()); |
| for (auto const &child : expr_node) { |
| auto &&[expr, errata]{this->parse_expr(child)}; |
| if (!errata.is_ok()) { |
| errata.note("While parsing feature expression list at {}.", expr_node.Mark()); |
| return std::move(errata); |
| } |
| l_types |= expr.result_type().base_types(); |
| if (!expr.is_literal()) { |
| literal_p = false; |
| } |
| xa.emplace_back(std::move(expr)); |
| } |
| |
| Expr expr; |
| if (literal_p) { |
| FeatureTuple t = this->alloc_span<Feature>(xa.size()); |
| unsigned idx = 0; |
| for (auto &f : t) { |
| f = std::get<Expr::LITERAL>(xa[idx++]._raw); |
| } |
| expr._raw = t; |
| } else { |
| auto &list = expr._raw.emplace<Expr::LIST>(); |
| list._types = l_types; |
| list._exprs = std::move(xa); |
| } |
| return expr; |
| } |
| |
| Rv<Directive::Handle> |
| Config::load_directive(YAML::Node const &drtv_node) |
| { |
| YAML::Node key_node; |
| for (auto const &[key_name, key_value] : drtv_node) { |
| TextView name{key_name.Scalar()}; |
| auto &&[arg, arg_errata]{parse_arg(name)}; |
| if (!arg_errata.is_ok()) { |
| return std::move(arg_errata); |
| } |
| |
| // Ignorable keys in the directive. Currently just one, so hand code it. Make this better |
| // if there is ever more than one. |
| if (name == Global::DO_KEY) { |
| continue; |
| } |
| // See if this is in the factory. It's not an error if it's not, to enable adding extra |
| // keys to directives. First key that is in the factory determines the directive type. |
| // If none of the keys are in the factory, that's an error and is reported after the loop. |
| if (auto spot{_factory.find(name)}; spot != _factory.end()) { |
| auto &info = spot->second; |
| auto rtti = &_drtv_info[info._idx]; |
| |
| if (!info._hook_mask[IndexFor(this->current_hook())]) { |
| return Errata(S_ERROR, R"(Directive "{}" at {} is not allowed on hook "{}".)", name, drtv_node.Mark(), |
| this->current_hook()); |
| } |
| |
| // If this is the first use of the directive, do config level setup for the directive type. |
| if (rtti->_count == 0) { |
| info._cfg_init_cb(*this, rtti); |
| } |
| ++(rtti->_count); |
| |
| auto &&[drtv, drtv_errata]{info._load_cb(*this, rtti, drtv_node, name, arg, key_value)}; |
| if (!drtv_errata.is_ok()) { |
| drtv_errata.note(R"(While parsing directive at {}.)", drtv_node.Mark()); |
| return std::move(drtv_errata); |
| } |
| drtv->_rtti = rtti; |
| |
| return std::move(drtv); |
| } |
| } |
| return Errata(S_ERROR, R"(Directive at {} has no recognized tag.)", drtv_node.Mark()); |
| } |
| |
| Rv<Directive::Handle> |
| Config::parse_directive(YAML::Node const &drtv_node) |
| { |
| if (drtv_node.IsMap()) { |
| return this->load_directive(drtv_node); |
| } else if (drtv_node.IsSequence()) { |
| Errata zret; |
| auto list{new DirectiveList}; |
| Directive::Handle drtv_list{list}; |
| for (auto child : drtv_node) { |
| auto &&[handle, errata]{this->load_directive(child)}; |
| if (errata.is_ok()) { |
| list->push_back(std::move(handle)); |
| } else { |
| errata.note(R"(While loading directives at {}.)", drtv_node.Mark()); |
| return std::move(errata); |
| } |
| } |
| return drtv_list; |
| } else if (drtv_node.IsNull()) { |
| return Directive::Handle(new NilDirective); |
| } |
| return Errata(S_ERROR, R"(Directive at {} is not an object or a sequence as required.)", drtv_node.Mark()); |
| } |
| |
| Errata |
| Config::load_top_level_directive(YAML::Node drtv_node) |
| { |
| if (drtv_node.IsMap()) { |
| YAML::Node key_node{drtv_node[When::KEY]}; |
| if (key_node) { |
| auto &&[handle, errata]{When::load(*this, this->drtv_info(When::KEY), drtv_node, When::KEY, {}, key_node)}; |
| if (errata.is_ok()) { |
| auto when = static_cast<When *>(handle.get()); |
| // Steal the directive out of the When. |
| _roots[IndexFor(when->_hook)].emplace_back(std::move(when->_directive)); |
| if (Hook::POST_LOAD != _hook) { // post load hooks don't count. |
| _has_top_level_directive_p = true; |
| } |
| } else { |
| return std::move(errata); |
| } |
| } else { |
| return Errata(S_ERROR, R"(Top level directive at {} is not a "when" directive as required.)", drtv_node.Mark()); |
| } |
| } else { |
| return Errata(S_ERROR, R"(Top level directive at {} is not an object as required.)", drtv_node.Mark()); |
| } |
| return {}; |
| } |
| |
| Errata |
| Config::load_remap_directive(YAML::Node drtv_node) |
| { |
| if (drtv_node.IsMap()) { |
| auto &&[drtv, errata]{this->parse_directive(drtv_node)}; |
| if (errata.is_ok()) { |
| // Don't unpack @c when - should not be that common and therefore better to defer them to the |
| // context callbacks, avoids having to cart around the remap rule config to call them later. |
| _roots[IndexFor(Hook::REMAP)].emplace_back(std::move(drtv)); |
| _has_top_level_directive_p = true; |
| } else { |
| return std::move(errata); |
| } |
| } else { |
| return Errata(S_ERROR, R"(Configuration at {} is not a directive object as required.)", drtv_node.Mark()); |
| } |
| return {}; |
| } |
| |
| Errata |
| Config::parse_yaml(YAML::Node root, TextView path) |
| { |
| static constexpr TextView ROOT_PATH{"."}; |
| // Walk the key path and find the target. If the path is the special marker for ROOT_PATH |
| // do not walk at all. |
| for (auto p = (path == ROOT_PATH ? TextView{} : path); p;) { |
| auto key{p.take_prefix_at('.')}; |
| if (auto node{root[key]}; node) { |
| root.reset(node); |
| } else { |
| return Errata(S_ERROR, R"(Key "{}" not found - no such key "{}".)", path, path.prefix(path.size() - p.size()).rtrim('.')); |
| } |
| } |
| |
| Errata errata; |
| |
| // Special case remap loading. |
| auto drtv_loader = &self_type::load_top_level_directive; // global loader. |
| if (_hook == Hook::REMAP) { // loading only remap directives. |
| drtv_loader = &self_type::load_remap_directive; |
| } |
| |
| if (root.IsSequence()) { |
| for (auto child : root) { |
| errata.note((this->*drtv_loader)(std::move(child))); |
| } |
| if (!errata.is_ok()) { |
| errata.note(R"(While loading list of top level directives for "{}" at {}.)", path, root.Mark()); |
| } |
| } else if (root.IsMap()) { |
| errata = (this->*drtv_loader)(root); |
| } else { |
| } |
| return errata; |
| }; |
| |
| Errata |
| Config::define(swoc::TextView name, HookMask const &hooks, Directive::InstanceLoader &&worker, |
| Directive::CfgInitializer &&cfg_init_cb) |
| { |
| auto &info{_factory[name]}; |
| info._idx = _factory.size() - 1; |
| info._hook_mask = hooks; |
| info._load_cb = std::move(worker); |
| info._cfg_init_cb = std::move(cfg_init_cb); |
| return {}; |
| } |
| |
| Directive::CfgStaticData const * |
| Config::drtv_info(swoc::TextView const &name) const |
| { |
| auto spot = _factory.find(name); |
| return spot == _factory.end() ? nullptr : &_drtv_info[spot->second._idx]; |
| } |
| |
| /* ------------------------------------------------------------------------------------ */ |
| Errata |
| Config::load_file(swoc::file::path const &cfg_path, TextView cfg_key, YamlCache *cache) |
| { |
| if (auto spot = _cfg_files.find(cfg_path); spot != _cfg_files.end()) { |
| if (spot->second.has_cfg_key(cfg_key)) { |
| ts::DebugMsg(R"(Skipping "{}":{} - already loaded)", cfg_path, cfg_key); |
| return {}; |
| } else { |
| spot->second.add_cfg_key(cfg_key); |
| } |
| } else { // not found - put it in the table. |
| auto [iter, flag] = _cfg_files.emplace(FileInfoMap::value_type{cfg_path, {}}); |
| iter->second.add_cfg_key(cfg_key); |
| } |
| |
| YAML::Node root; |
| // Try loading and parsing the file. |
| if (cache) { |
| if (auto cache_spot = cache->find(cfg_path); cache_spot != cache->end()) { |
| root = cache_spot->second; |
| } |
| } |
| |
| if (root.IsNull()) { |
| auto &&[yaml_node, yaml_errata]{yaml_load(cfg_path)}; |
| if (!yaml_errata.is_ok()) { |
| yaml_errata.note(R"(While loading file "{}".)", cfg_path); |
| return std::move(yaml_errata); |
| } |
| root = yaml_node; |
| if (cache) { |
| cache->emplace(cfg_path, root); |
| } |
| } |
| |
| // Process the YAML data. |
| auto errata = this->parse_yaml(root, cfg_key); |
| if (!errata.is_ok()) { |
| errata.note(R"(While parsing key "{}" in configuration file "{}".)", cfg_key, cfg_path); |
| return errata; |
| } |
| |
| return {}; |
| } |
| /* ------------------------------------------------------------------------------------ */ |
| Errata |
| Config::load_file_glob(TextView pattern, swoc::TextView cfg_key, YamlCache *cache) |
| { |
| int flags = 0; |
| glob_t files; |
| auto err_f = [](char const *, int) -> int { return 0; }; |
| swoc::file::path abs_pattern = ts::make_absolute(pattern); |
| int result = glob(abs_pattern.c_str(), flags, err_f, &files); |
| if (result == GLOB_NOMATCH) { |
| return Errata(S_WARN, R"(The pattern "{}" did not match any files.)", abs_pattern); |
| } |
| for (size_t idx = 0; idx < files.gl_pathc; ++idx) { |
| auto errata = this->load_file(swoc::file::path(files.gl_pathv[idx]), cfg_key, cache); |
| if (!errata.is_ok()) { |
| errata.note(R"(While processing pattern "{}".)", pattern); |
| return errata; |
| } |
| } |
| globfree(&files); |
| return {}; |
| } |
| /* ------------------------------------------------------------------------------------ */ |
| Errata |
| Config::load_cli_args(Handle handle, const std::vector<std::string> &args, int arg_idx, YamlCache *cache) |
| { |
| using argv_type = char const *; // Overall clearer in context of pointers to pointers. |
| std::unique_ptr<argv_type[]> buff{new argv_type[args.size()]}; |
| swoc::MemSpan<argv_type> argv{buff.get(), args.size()}; |
| int idx = 0; |
| for (auto const &arg : args) { |
| argv[idx++] = arg.c_str(); |
| } |
| return this->load_cli_args(std::move(handle), argv, arg_idx, cache); |
| } |
| |
| Errata |
| Config::load_cli_args(Handle handle, swoc::MemSpan<char const *> argv, int arg_idx, YamlCache *cache) |
| { |
| static constexpr TextView KEY_OPT = "key"; |
| static constexpr TextView CONFIG_OPT = "config"; // An archaism for BC - take out someday. |
| |
| TextView cfg_key{_hook == Hook::REMAP ? REMAP_ROOT_KEY : GLOBAL_ROOT_KEY}; |
| for (unsigned idx = arg_idx; idx < argv.count(); ++idx) { |
| TextView arg{std::string_view(argv[idx])}; |
| if (arg.empty()) { |
| continue; |
| } |
| if (arg.front() == '-') { |
| arg.ltrim('-'); |
| if (arg.empty()) { |
| return Errata(S_ERROR, "Arg {} has an option prefix but no name.", idx); |
| } |
| |
| TextView value; |
| if (auto prefix = arg.prefix_at('='); !prefix.empty()) { |
| value = arg.substr(prefix.size() + 1); |
| arg = prefix; |
| } else if (++idx >= argv.count()) { |
| return Errata(S_ERROR, "Arg {} is an option '{}' that requires a value but none was found.", idx, arg); |
| } else { |
| value = std::string_view{argv[idx]}; |
| } |
| |
| if (arg.starts_with_nocase(KEY_OPT)) { |
| cfg_key = value; |
| } else if (arg.starts_with_nocase(CONFIG_OPT)) { |
| auto errata = this->load_file_glob(value, cfg_key, cache); |
| if (!errata.is_ok()) { |
| return errata; |
| } |
| } else { |
| return Errata(S_ERROR, "Arg {} is an unrecognized option '{}'.", idx, arg); |
| } |
| continue; |
| } |
| auto errata = this->load_file_glob(arg, cfg_key, cache); |
| if (!errata.is_ok()) { |
| return errata; |
| } |
| } |
| |
| // Config loaded, run the post load directives and enable them to break the load by reporting |
| // errors. |
| auto &post_load_directives = this->hook_directives(Hook::POST_LOAD); |
| if (post_load_directives.size() > 0) { |
| std::unique_ptr<Context> ctx{new Context(handle)}; |
| for (auto &&drtv : post_load_directives) { |
| auto errata = drtv->invoke(*ctx); |
| if (!errata.is_ok()) { |
| errata.note("While processing post-load directives."); |
| return errata; |
| } |
| } |
| } |
| // Done with the files - clear them out. |
| _cfg_file_count = _cfg_files.size(); |
| _cfg_files.clear(); |
| return {}; |
| } |
| |
| Config::ActiveFeatureScope |
| Config::feature_scope(ActiveType const &ex_type) |
| { |
| ActiveFeatureScope scope(*this); |
| _active_feature._ref_p = false; |
| _active_feature._type = ex_type; |
| return scope; |
| } |
| |
| Config::ActiveCaptureScope |
| Config::capture_scope(unsigned int count, unsigned int line_no) |
| { |
| ActiveCaptureScope scope(*this); |
| _active_capture._count = count; |
| _active_capture._line = line_no; |
| _active_capture._ref_p = false; |
| return scope; |
| } |
| |
| Config::ActiveFeatureScope::~ActiveFeatureScope() |
| { |
| if (_cfg) { |
| _cfg->_active_feature = _state; |
| } |
| } |