#pragma once #include "json_binary_proxy.hpp" #include "json_bt.hpp" #include "json_conversions.hpp" #include #include #include #include #include #include namespace llarp::rpc { using json_range = std::pair; using rpc_input = std::variant; // Checks that key names are given in ascending order template void check_ascending_names(std::string_view name1, std::string_view name2, const Ignore&...) { if (!(name2 > name1)) throw std::runtime_error{ "Internal error: request values must be retrieved in ascending order"}; } // Wrapper around a reference for get_values that is used to indicate that the value is // required, in which case an exception will be raised if the value is not found. Usage: // // int a_optional = 0, b_required; // get_values(input, // "a", a_optional, // "b", required{b_required}, // // ... // ); template struct required { T& value; required(T& ref) : value{ref} {} }; template constexpr bool is_required_wrapper = false; template constexpr bool is_required_wrapper> = true; template constexpr bool is_std_optional = false; template constexpr bool is_std_optional> = true; // Wrapper around a reference for get_values that adds special handling to act as if the value was // not given at all if the value is given as an empty string. This sucks, but is necessary for // backwards compatibility (especially with wallet2 clients). // // Usage: // // std::string x; // get_values(input, // "x", ignore_empty_string{x}, // // ... // ); template struct ignore_empty_string { T& value; ignore_empty_string(T& ref) : value{ref} {} bool should_ignore(oxenc::bt_dict_consumer& d) { if (d.is_string()) { auto d2{d}; // Copy because we want to leave d intact if (d2.consume_string_view().empty()) return true; } return false; } bool should_ignore(json_range& it_range) { auto& e = *it_range.first; return (e.is_string() && e.get().empty()); } }; template constexpr bool is_ignore_empty_string_wrapper = false; template constexpr bool is_ignore_empty_string_wrapper> = true; // Advances the dict consumer to the first element >= the given name. Returns true if found, // false if it advanced beyond the requested name. This is exactly the same as // `d.skip_until(name)`, but is here so we can also overload an equivalent function for json // iteration. inline bool skip_until(oxenc::bt_dict_consumer& d, std::string_view name) { return d.skip_until(name); } // Equivalent to the above but for a json object iterator. inline bool skip_until(json_range& it_range, std::string_view name) { auto& [it, end] = it_range; while (it != end && it.key() < name) ++it; return it != end && it.key() == name; } // List types that are expandable; for these we emplace_back for each element of the input template constexpr bool is_expandable_list = false; template constexpr bool is_expandable_list> = true; // Types that are constructible from string template constexpr bool is_string_constructible = false; template <> inline constexpr bool is_string_constructible = true; // Fixed size elements: tuples, pairs, and std::array's; we accept list input as long as the // list length matches exactly. template constexpr bool is_tuple_like = false; template constexpr bool is_tuple_like> = true; template constexpr bool is_tuple_like> = true; template constexpr bool is_tuple_like> = true; // True if T is a `std::unordered_map` template constexpr bool is_unordered_string_map = false; template constexpr bool is_unordered_string_map> = true; template void load_tuple_values(oxenc::bt_list_consumer&, TupleLike&, std::index_sequence); // Consumes the next value from the dict consumer into `val` template < typename BTConsumer, typename T, std::enable_if_t< std::is_same_v || std::is_same_v, int> = 0> void load_value(BTConsumer& c, T& target) { if constexpr (std::is_integral_v) target = c.template consume_integer(); else if constexpr (std::is_same_v || std::is_same_v) target = c.consume_string_view(); else if constexpr (is_string_constructible) target = T{c.consume_string()}; else if constexpr (llarp::rpc::json_is_binary) llarp::rpc::load_binary_parameter(c.consume_string_view(), true /*allow raw*/, target); else if constexpr (is_expandable_list) { auto lc = c.consume_list_consumer(); target.clear(); while (!lc.is_finished()) load_value(lc, target.emplace_back()); } else if constexpr (is_tuple_like) { auto lc = c.consume_list_consumer(); load_tuple_values(lc, target, std::make_index_sequence>{}); } else if constexpr (is_unordered_string_map) { auto dc = c.consume_dict_consumer(); target.clear(); while (!dc.is_finished()) load_value(dc, target[std::string{dc.key()}]); } else static_assert(std::is_same_v, "Unsupported load_value type"); } // Copies the next value from the json range into `val`, and advances the iterator. Throws // on unconvertible values. template void load_value(json_range& range_itr, T& target) { auto& key = range_itr.first.key(); auto& current = *range_itr.first; // value currently pointed to by range_itr.first if constexpr (std::is_same_v) { if (current.is_boolean()) target = current.get(); else if (current.is_number_unsigned()) { // Also accept 0 or 1 for bools (mainly to be compatible with bt-encoding which doesn't // have a distinct bool type). auto b = current.get(); if (b <= 1) target = b; else throw std::domain_error{"Invalid value for '" + key + "': expected boolean"}; } else { throw std::domain_error{"Invalid value for '" + key + "': expected boolean"}; } } else if constexpr (std::is_unsigned_v) { if (!current.is_number_unsigned()) throw std::domain_error{"Invalid value for '" + key + "': non-negative value required"}; auto i = current.get(); if (sizeof(T) < sizeof(uint64_t) && i > std::numeric_limits::max()) throw std::domain_error{"Invalid value for '" + key + "': value too large"}; target = i; } else if constexpr (std::is_integral_v) { if (!current.is_number_integer()) throw std::domain_error{"Invalid value for '" + key + "': value is not an integer"}; auto i = current.get(); if (sizeof(T) < sizeof(int64_t)) { if (i < std::numeric_limits::lowest()) throw std::domain_error{ "Invalid value for '" + key + "': negative value magnitude is too large"}; if (i > std::numeric_limits::max()) throw std::domain_error{"Invalid value for '" + key + "': value is too large"}; } target = i; } else if constexpr (std::is_same_v || std::is_same_v) { target = current.get(); } else if constexpr ( llarp::rpc::json_is_binary || is_expandable_list || is_tuple_like || is_unordered_string_map) { try { current.get_to(target); } catch (const std::exception& e) { throw std::domain_error{"Invalid values in '" + key + "'"}; } } else { static_assert(std::is_same_v, "Unsupported load type"); } ++range_itr.first; } template void load_tuple_values(oxenc::bt_list_consumer& c, TupleLike& val, std::index_sequence) { (load_value(c, std::get(val)), ...); } // Takes a json object iterator or bt_dict_consumer and loads the current value at the iterator. // This calls itself recursively, if needed, to unwrap optional/required/ignore_empty_string // wrappers. template void load_curr_value(In& in, T& val) { if constexpr (is_required_wrapper) { load_curr_value(in, val.value); } else if constexpr (is_ignore_empty_string_wrapper) { if (!val.should_ignore(in)) load_curr_value(in, val.value); } else if constexpr (is_std_optional) { load_curr_value(in, val.emplace()); } else { load_value(in, val); } } // Gets the next value from a json object iterator or bt_dict_consumer. Leaves the iterator at // the next value, i.e. found + 1 if found, or the next greater value if not found. (NB: // nlohmann::json objects are backed by an *ordered* map and so both nlohmann iterators and // bt_dict_consumer behave analogously here). template void get_next_value(In& in, [[maybe_unused]] std::string_view name, T& val) { if constexpr (std::is_same_v) ; else if (skip_until(in, name)) load_curr_value(in, val); else if constexpr (is_required_wrapper) throw std::runtime_error{"Required key '" + std::string{name} + "' not found"}; } // Accessor for simple, flat value retrieval from a json or bt_dict_consumer. In the later // case note that the given bt_dict_consumer will be advanced, so you *must* take care to // process keys in order, both for the keys passed in here *and* for use before and after this // call. template void get_values(Input& in, std::string_view name, T&& val, More&&... more) { if constexpr (std::is_same_v) { if (auto* json_in = std::get_if(&in)) { json_range r{json_in->cbegin(), json_in->cend()}; get_values(r, name, val, std::forward(more)...); } else if (auto* dict = std::get_if(&in)) { get_values(*dict, name, val, std::forward(more)...); } else { // A monostate indicates that no parameters field was provided at all get_values(var::get(in), name, val, std::forward(more)...); } } else if constexpr (std::is_same_v) { if (in.front() == 'd') { oxenc::bt_dict_consumer d{in}; get_values(d, name, val, std::forward(more)...); } else { auto json_in = nlohmann::json::parse(in); json_range r{json_in.cbegin(), json_in.cend()}; get_values(r, name, val, std::forward(more)...); } } else { static_assert( std::is_same_v || std::is_same_v || std::is_same_v); get_next_value(in, name, val); if constexpr (sizeof...(More) > 0) { check_ascending_names(name, more...); get_values(in, std::forward(more)...); } } } } // namespace llarp::rpc