#pragma once #include #include #include #include #include #include #include namespace oxenmq { using namespace std::literals; class bt_dict_producer; #if defined(__APPLE__) && defined(__MAC_OS_X_VERSION_MIN_REQUIRED) && __MAC_OS_X_VERSION_MIN_REQUIRED < 101500 #define OXENMQ_APPLE_TO_CHARS_WORKAROUND /// Really simplistic version of std::to_chars on Apple, because Apple doesn't allow `std::to_chars` /// to be used if targetting anything before macOS 10.15. The buffer must have at least 20 chars of /// space (for int types up to 64-bit); we return a pointer one past the last char written. template char* apple_to_chars10(char* buf, IntType val) { static_assert(std::is_integral_v && sizeof(IntType) <= 64); if constexpr (std::is_signed_v) { if (val < 0) { buf[0] = '-'; return apple_to_chars10(buf+1, static_cast>(-val)); } } // write it to the buffer in reverse (because we don't know how many chars we'll need yet, but // writing in reverse will figure that out). char* pos = buf; do { *pos++ = '0' + static_cast(val % 10); val /= 10; } while (val > 0); // Reverse the digits into the right order int swaps = (pos - buf) / 2; for (int i = 0; i < swaps; i++) std::swap(buf[i], pos[-1 - i]); return pos; } #endif /// Class that allows you to build a bt-encoded list manually, without copying or allocating memory. /// This is essentially the reverse of bt_list_consumer: where it lets you stream-parse a buffer, /// this class lets you build directly into a buffer that you own. /// /// Out-of-buffer-space errors throw class bt_list_producer { friend class bt_dict_producer; // Our pointers to the next write position and the past-the-end pointer of the buffer. using buf_span = std::pair; // Our data is a begin/end pointer pair for the root list, or a pointer to our parent if a // sublist. std::variant data; // Reference to the write buffer; this is simply a reference to the value inside `data` for the // root element, and a pointer to the root's value for sublists/subdicts. buf_span& buffer; // True indicates we have an open child list/dict bool has_child = false; // The range that contains this currently serialized value; `from` equals wherever the `l` was // written that started this list and `to` is one past the `e` that ends it. Note that `to` // will always be ahead of `buf_span.first` because we always write the `e`s to close open lists // but these `e`s don't advance the write position (they will be overwritten if we append). const char* const from; const char* to; // Sublist constructors bt_list_producer(bt_list_producer* parent, std::string_view prefix = "l"sv); bt_list_producer(bt_dict_producer* parent, std::string_view prefix = "l"sv); // Does the actual appending to the buffer, and throwing if we'd overrun. If advance is false // then we append without moving the buffer pointer (primarily when we append intermediate `e`s // that we will overwrite if more data is added). This means that the next write will overwrite // whatever was previously written by an `advance=false` call. void buffer_append(std::string_view d, bool advance = true); // Appends the 'e's into the buffer to close off open sublists/dicts *without* advancing the // buffer position; we do this after each append so that the buffer always contains valid // encoded data, even while we are still appending to it, and so that appending something raises // a length_error if appending it would not leave enough space for the required e's to close the // open list(s)/dict(s). void append_intermediate_ends(size_t count = 1); // Writes an integer to the given buffer; returns the one-past-the-data pointer. Up to 20 bytes // will be written and must be available in buf. Used for both string and integer // serialization. template char* write_integer(IntType val, char* buf) { static_assert(sizeof(IntType) <= 64); #ifndef OXENMQ_APPLE_TO_CHARS_WORKAROUND auto [ptr, ec] = std::to_chars(buf, buf+20, val); assert(ec == std::errc()); return ptr; #else // Hate apple. return apple_to_chars10(buf, val); #endif } // Serializes an integer value and appends it to the output buffer. Does not call // append_intermediate_ends(). template , int> = 0> void append_impl(IntType val) { char buf[22]; // 'i' + base10 representation + 'e' buf[0] = 'i'; auto* ptr = write_integer(val, buf+1); *ptr++ = 'e'; buffer_append({buf, static_cast(ptr-buf)}); } // Appends a string value, but does not call append_intermediate_ends() void append_impl(std::string_view s); public: bt_list_producer() = delete; bt_list_producer(const bt_list_producer&) = delete; bt_list_producer& operator=(const bt_list_producer&) = delete; bt_list_producer& operator=(bt_list_producer&&) = delete; bt_list_producer(bt_list_producer&& other); ~bt_list_producer(); /// Constructs a list producer that writes into the range [begin, end). If a write would go /// beyond the end of the buffer an exception is raised. Note that this will happen during /// construction if the given buffer is not large enough to contain the `le` encoding of an /// empty list. bt_list_producer(char* begin, char* end); /// Constructs a list producer that writes into the range [begin, begin+size). If a write would /// go beyond the end of the buffer an exception is raised. bt_list_producer(char* begin, size_t len) : bt_list_producer{begin, begin + len} {} /// Returns a string_view into the currently serialized data buffer. Note that the returned /// view includes the `e` list end serialization markers which will be overwritten if the list /// (or an active sublist/subdict) is appended to. std::string_view view() const { return {from, static_cast(to-from)}; } /// Returns the end position in the buffer. const char* end() const { return to; } /// Appends an element containing binary string data void append(std::string_view data); bt_list_producer& operator+=(std::string_view data) { append(data); return *this; } /// Appends an integer template , int> = 0> void append(IntType i) { if (has_child) throw std::logic_error{"Cannot append to list when a sublist is active"}; append_impl(i); append_intermediate_ends(); } template , int> = 0> bt_list_producer& operator+=(IntType i) { append(i); return *this; } /// Appends elements from the range [from, to) to the list. This does *not* append the elements /// as a sublist: for that you should use something like: `l.append_list().append(from, to);` template void append(ForwardIt from, ForwardIt to) { if (has_child) throw std::logic_error{"Cannot append to list when a sublist is active"}; while (from != to) append_impl(*from++); append_intermediate_ends(); } /// Appends a sublist to this list. Returns a new bt_list_producer that references the parent /// list. The parent cannot be added to until the sublist is destroyed. This is meant to be /// used via RAII: /// /// buf data[16]; /// bt_list_producer list{data, sizeof(data)}; /// { /// auto sublist = list.append_list(); /// sublist.append(42); /// } /// list.append(1); /// // `data` now contains: `lli42eei1ee` /// /// If doing more complex lifetime management, take care not to allow the child instance to /// outlive the parent. bt_list_producer append_list(); /// Appends a dict to this list. Returns a new bt_dict_producer that references the parent /// list. The parent cannot be added to until the subdict is destroyed. This is meant to be /// used via RAII (see append_list() for details). /// /// If doing more complex lifetime management, take care not to allow the child instance to /// outlive the parent. bt_dict_producer append_dict(); }; /// Class that allows you to build a bt-encoded dict manually, without copying or allocating memory. /// This is essentially the reverse of bt_dict_consumer: where it lets you stream-parse a buffer, /// this class lets you build directly into a buffer that you own. /// /// Note that bt-encoded dicts *must* be produced in (ASCII) ascending key order, but that this is /// only tracked/enforced for non-release builds (i.e. without -DNDEBUG). class bt_dict_producer : bt_list_producer { friend class bt_list_producer; // Subdict constructors bt_dict_producer(bt_list_producer* parent); bt_dict_producer(bt_dict_producer* parent); // Checks a just-written key string to make sure it is monotonically increasing from the last // key. Does nothing in a release build. #ifdef NDEBUG constexpr void check_incrementing_key(size_t) const {} #else // String view into the buffer where we wrote the previous key. std::string_view last_key; void check_incrementing_key(size_t size); #endif public: // Construction is identical to bt_list_producer using bt_list_producer::bt_list_producer; /// Returns a string_view into the currently serialized data buffer. Note that the returned /// view includes the `e` dict end serialization markers which will be overwritten if the dict /// (or an active sublist/subdict) is appended to. std::string_view view() const { return bt_list_producer::view(); } /// Appends a key-value pair with a string or integer value. The key must be > the last key /// added, but this is only enforced (with an assertion) in debug builds. template || std::is_integral_v, int> = 0> void append(std::string_view key, const T& value) { if (has_child) throw std::logic_error{"Cannot append to list when a sublist is active"}; append_impl(key); check_incrementing_key(key.size()); append_impl(value); append_intermediate_ends(); } /// Appends pairs from the range [from, to) to the dict. Elements must have a .first /// convertible to a string_view, and a .second that is either string view convertible or an /// integer. This does *not* append the elements as a subdict: for that you should use /// something like: `l.append_dict().append(key, from, to);` /// /// Also note that the range *must* be sorted by keys, which means either using an ordered /// container (e.g. std::map) or a manually ordered container (such as a vector or list of /// pairs). unordered_map, however, is not acceptable. template , int> = 0> void append(ForwardIt from, ForwardIt to) { if (has_child) throw std::logic_error{"Cannot append to list when a sublist is active"}; using KeyType = std::remove_cv_tfirst)>>; using ValType = std::decay_tsecond)>; static_assert(std::is_convertible_vfirst), std::string_view>); static_assert(std::is_convertible_v || std::is_integral_v); using BadUnorderedMap = std::unordered_map; static_assert(!( // Disallow unordered_map iterators because they are not going to be ordered. std::is_same_v || std::is_same_v)); while (from != to) { const auto& [k, v] = *from++; append_impl(k); check_incrementing_key(k.size()); append_impl(v); } append_intermediate_ends(); } /// Appends a sub-dict value to this dict with the given key. Returns a new bt_dict_producer /// that references the parent dict. The parent cannot be added to until the subdict is /// destroyed. Key must be (ascii-comparison) larger than the previous key. /// /// This is meant to be used via RAII: /// /// buf data[32]; /// bt_dict_producer dict{data, sizeof(data)}; /// { /// auto subdict = dict.begin_dict("myKey"); /// subdict.append("x", 42); /// } /// dict.append("y", ""); /// // `data` now contains: `d5:myKeyd1:xi42ee1:y0:e` /// /// If doing more complex lifetime management, take care not to allow the child instance to /// outlive the parent. bt_dict_producer append_dict(std::string_view key); /// Appends a list to this dict with the given key (which must be ascii-larger than the previous /// key). Returns a new bt_list_producer that references the parent dict. The parent cannot be /// added to until the sublist is destroyed. /// /// This is meant to be used via RAII (see append_dict() for details). /// /// If doing more complex lifetime management, take care not to allow the child instance to /// outlive the parent. bt_list_producer append_list(std::string_view key); }; } // namespace oxenmq