mirror of https://github.com/oxen-io/oxen-mq.git
commit
2dcc3f72a2
|
@ -17,7 +17,7 @@ cmake_minimum_required(VERSION 3.7)
|
||||||
set(CMAKE_OSX_DEPLOYMENT_TARGET 10.12 CACHE STRING "macOS deployment target (Apple clang only)")
|
set(CMAKE_OSX_DEPLOYMENT_TARGET 10.12 CACHE STRING "macOS deployment target (Apple clang only)")
|
||||||
|
|
||||||
project(liboxenmq
|
project(liboxenmq
|
||||||
VERSION 1.2.6
|
VERSION 1.2.8
|
||||||
LANGUAGES CXX C)
|
LANGUAGES CXX C)
|
||||||
|
|
||||||
include(GNUInstallDirs)
|
include(GNUInstallDirs)
|
||||||
|
@ -113,13 +113,15 @@ target_include_directories(oxenmq
|
||||||
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/cppzmq>
|
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/cppzmq>
|
||||||
)
|
)
|
||||||
|
|
||||||
target_compile_options(oxenmq PRIVATE -Wall -Wextra -Werror)
|
target_compile_options(oxenmq PRIVATE -Wall -Wextra)
|
||||||
set_target_properties(oxenmq PROPERTIES
|
|
||||||
CXX_STANDARD 17
|
option(WARNINGS_AS_ERRORS "treat all warnings as errors" ON)
|
||||||
CXX_STANDARD_REQUIRED ON
|
if(WARNINGS_AS_ERRORS)
|
||||||
CXX_EXTENSIONS OFF
|
target_compile_options(oxenmq PRIVATE -Werror)
|
||||||
POSITION_INDEPENDENT_CODE ON
|
endif()
|
||||||
)
|
|
||||||
|
target_compile_features(oxenmq PUBLIC cxx_std_17)
|
||||||
|
set_target_properties(oxenmq PROPERTIES POSITION_INDEPENDENT_CODE ON)
|
||||||
|
|
||||||
function(link_dep_libs target linktype libdirs)
|
function(link_dep_libs target linktype libdirs)
|
||||||
foreach(lib ${ARGN})
|
foreach(lib ${ARGN})
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
Name: oxenmq
|
Name: oxenmq
|
||||||
Version: 1.2.6
|
Version: 1.2.8
|
||||||
Release: 2%{?dist}
|
Release: 1%{?dist}
|
||||||
Summary: zeromq-based Oxen message passing library
|
Summary: zeromq-based Oxen message passing library
|
||||||
|
|
||||||
License: BSD
|
License: BSD
|
||||||
|
@ -87,11 +87,18 @@ build software using oxenmq.
|
||||||
|
|
||||||
|
|
||||||
%changelog
|
%changelog
|
||||||
|
* Mon Oct 25 2021 Technical Tumbleweed <necro_nemesis@hotmail.com> -1.2.8~1
|
||||||
|
- Merge dev changes 1.2.8
|
||||||
|
* Fri Oct 15 2021 Technical Tumbleweed <necro_nemesis@hotmail.com> -1.2.7~1
|
||||||
|
- Merge dev changes 1.2.7
|
||||||
|
- change branch Catch2 to 2.x
|
||||||
|
- bump version
|
||||||
|
|
||||||
* Mon Aug 09 2021 Jason Rhinelander <jason@imaginary.ca> - 1.2.6-2
|
* Mon Aug 09 2021 Jason Rhinelander <jason@imaginary.ca> - 1.2.6-2
|
||||||
- Split oxenmq into lib and devel package
|
- Split oxenmq into lib and devel package
|
||||||
- Versioned the library, as we do for debs
|
- Versioned the library, as we do for debs
|
||||||
- Updated various package descriptions and build commands
|
- Updated various package descriptions and build commands
|
||||||
|
|
||||||
* Thu Jul 22 2021 Technical Tumbleweed (necro_nemesis@hotmail.com) oxenmq
|
* Thu Jul 22 2021 Technical Tumbleweed <necro_nemesis@hotmail.com> oxenmq
|
||||||
-First oxenmq RPM
|
-First oxenmq RPM
|
||||||
-Second build update to v1.2.6
|
-Second build update to v1.2.6
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
set(LIBZMQ_PREFIX ${CMAKE_BINARY_DIR}/libzmq)
|
set(LIBZMQ_PREFIX ${CMAKE_BINARY_DIR}/libzmq)
|
||||||
set(ZeroMQ_VERSION 4.3.3)
|
set(ZeroMQ_VERSION 4.3.4)
|
||||||
set(LIBZMQ_URL https://github.com/zeromq/libzmq/releases/download/v${ZeroMQ_VERSION}/zeromq-${ZeroMQ_VERSION}.tar.gz)
|
set(LIBZMQ_URL https://github.com/zeromq/libzmq/releases/download/v${ZeroMQ_VERSION}/zeromq-${ZeroMQ_VERSION}.tar.gz)
|
||||||
set(LIBZMQ_HASH SHA512=4c18d784085179c5b1fcb753a93813095a12c8d34970f2e1bfca6499be6c9d67769c71c68b7ca54ff181b20390043170e89733c22f76ff1ea46494814f7095b1)
|
set(LIBZMQ_HASH SHA512=e198ef9f82d392754caadd547537666d4fba0afd7d027749b3adae450516bcf284d241d4616cad3cb4ad9af8c10373d456de92dc6d115b037941659f141e7c0e)
|
||||||
|
|
||||||
message(${LIBZMQ_URL})
|
message(${LIBZMQ_URL})
|
||||||
|
|
||||||
|
|
2
cppzmq
2
cppzmq
|
@ -1 +1 @@
|
||||||
Subproject commit 76bf169fd67b8e99c1b0e6490029d9cd5ef97666
|
Subproject commit 33ed54228e98a82db88da5ac9aca001147521c20
|
|
@ -163,9 +163,8 @@ struct address {
|
||||||
|
|
||||||
/// Returns a QR-code friendly address string. This returns an all-uppercase version of the
|
/// Returns a QR-code friendly address string. This returns an all-uppercase version of the
|
||||||
/// address with "TCP://" or "CURVE://" for the protocol string, and uses upper-case base32z
|
/// address with "TCP://" or "CURVE://" for the protocol string, and uses upper-case base32z
|
||||||
/// encoding for the pubkey (for curve addresses). For literal IPv6 addresses we replace the
|
/// encoding for the pubkey (for curve addresses). For literal IPv6 addresses we surround the
|
||||||
/// surround the
|
/// address with $ instead of [...]
|
||||||
/// address with $ instead of $
|
|
||||||
///
|
///
|
||||||
/// \throws std::logic_error if called on a unix socket address.
|
/// \throws std::logic_error if called on a unix socket address.
|
||||||
std::string qr_address() const;
|
std::string qr_address() const;
|
||||||
|
|
|
@ -119,7 +119,6 @@ void OxenMQ::proxy_set_active_sns(pubkey_set pubkeys) {
|
||||||
}
|
}
|
||||||
|
|
||||||
void OxenMQ::update_active_sns(pubkey_set added, pubkey_set removed) {
|
void OxenMQ::update_active_sns(pubkey_set added, pubkey_set removed) {
|
||||||
LMQ_LOG(info, "uh, ", added.size());
|
|
||||||
if (proxy_thread.joinable()) {
|
if (proxy_thread.joinable()) {
|
||||||
std::array<uintptr_t, 2> data;
|
std::array<uintptr_t, 2> data;
|
||||||
data[0] = detail::serialize_object(std::move(added));
|
data[0] = detail::serialize_object(std::move(added));
|
||||||
|
@ -139,7 +138,6 @@ void OxenMQ::proxy_update_active_sns(pubkey_set added, pubkey_set removed) {
|
||||||
// values, pubkeys that already(added) or do not(removed) exist), then pass the purified lists
|
// values, pubkeys that already(added) or do not(removed) exist), then pass the purified lists
|
||||||
// to the _clean version.
|
// to the _clean version.
|
||||||
|
|
||||||
LMQ_LOG(info, "uh, ", added.size(), ", ", removed.size());
|
|
||||||
for (auto it = removed.begin(); it != removed.end(); ) {
|
for (auto it = removed.begin(); it != removed.end(); ) {
|
||||||
const auto& pk = *it;
|
const auto& pk = *it;
|
||||||
if (pk.size() != 32) {
|
if (pk.size() != 32) {
|
||||||
|
|
240
oxenmq/base32z.h
240
oxenmq/base32z.h
|
@ -74,40 +74,81 @@ static_assert(b32z_lut.from_b32z('w') == 20 && b32z_lut.from_b32z('T') == 17 &&
|
||||||
|
|
||||||
} // namespace detail
|
} // namespace detail
|
||||||
|
|
||||||
/// Converts bytes into a base32z encoded character sequence.
|
/// Returns the number of characters required to encode a base32z string from the given number of bytes.
|
||||||
template <typename InputIt, typename OutputIt>
|
inline constexpr size_t to_base32z_size(size_t byte_size) { return (byte_size*8 + 4) / 5; } // ⌈bits/5⌉ because 5 bits per byte
|
||||||
void to_base32z(InputIt begin, InputIt end, OutputIt out) {
|
/// Returns the (maximum) number of bytes required to decode a base32z string of the given size.
|
||||||
static_assert(sizeof(decltype(*begin)) == 1, "to_base32z requires chars/bytes");
|
inline constexpr size_t from_base32z_size(size_t b32z_size) { return b32z_size*5 / 8; } // ⌊bits/8⌋
|
||||||
int bits = 0; // Tracks the number of unconsumed bits held in r, will always be in [0, 4]
|
|
||||||
std::uint_fast16_t r = 0;
|
|
||||||
while (begin != end) {
|
|
||||||
r = r << 8 | static_cast<unsigned char>(*begin++);
|
|
||||||
|
|
||||||
// we just added 8 bits, so we can *always* consume 5 to produce one character, so (net) we
|
/// Iterable object for on-the-fly base32z encoding. Used internally, but also particularly useful
|
||||||
// are adding 3 bits.
|
/// when converting from one encoding to another.
|
||||||
bits += 3;
|
template <typename InputIt>
|
||||||
*out++ = detail::b32z_lut.to_b32z(r >> bits); // Right-shift off the bits we aren't consuming right now
|
struct base32z_encoder final {
|
||||||
|
private:
|
||||||
|
InputIt _it, _end;
|
||||||
|
static_assert(sizeof(decltype(*_it)) == 1, "base32z_encoder requires chars/bytes input iterator");
|
||||||
|
// Number of bits held in r; will always be >= 5 until we are at the end.
|
||||||
|
int bits{_it != _end ? 8 : 0};
|
||||||
|
// Holds bits of data we've already read, which might belong to current or next chars
|
||||||
|
uint_fast16_t r{bits ? static_cast<unsigned char>(*_it) : (unsigned char)0};
|
||||||
|
public:
|
||||||
|
using iterator_category = std::input_iterator_tag;
|
||||||
|
using difference_type = std::ptrdiff_t;
|
||||||
|
using value_type = char;
|
||||||
|
using reference = value_type;
|
||||||
|
using pointer = void;
|
||||||
|
base32z_encoder(InputIt begin, InputIt end) : _it{std::move(begin)}, _end{std::move(end)} {}
|
||||||
|
|
||||||
// Drop the bits we don't want to keep (because we just consumed them)
|
base32z_encoder end() { return {_end, _end}; }
|
||||||
|
|
||||||
|
bool operator==(const base32z_encoder& i) { return _it == i._it && bits == i.bits; }
|
||||||
|
bool operator!=(const base32z_encoder& i) { return !(*this == i); }
|
||||||
|
|
||||||
|
base32z_encoder& operator++() {
|
||||||
|
assert(bits >= 5);
|
||||||
|
// Discard the most significant 5 bits
|
||||||
|
bits -= 5;
|
||||||
r &= (1 << bits) - 1;
|
r &= (1 << bits) - 1;
|
||||||
|
// If we end up with less than 5 significant bits then try to pull another 8 bits:
|
||||||
if (bits >= 5) { // We have enough bits to produce a second character; essentially the same as above
|
if (bits < 5 && _it != _end) {
|
||||||
bits -= 5; // Except now we are just consuming 5 without having added any more
|
if (++_it != _end) {
|
||||||
*out++ = detail::b32z_lut.to_b32z(r >> bits);
|
r = (r << 8) | static_cast<unsigned char>(*_it);
|
||||||
r &= (1 << bits) - 1;
|
bits += 8;
|
||||||
|
} else if (bits > 0) {
|
||||||
|
// No more input bytes, so shift `r` to put the bits we have into the most
|
||||||
|
// significant bit position for the final character. E.g. if we have "11" we want
|
||||||
|
// the last character to be encoded "11000".
|
||||||
|
r <<= (5 - bits);
|
||||||
|
bits = 5;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
return *this;
|
||||||
}
|
}
|
||||||
|
base32z_encoder operator++(int) { base32z_encoder copy{*this}; ++*this; return copy; }
|
||||||
|
|
||||||
if (bits > 0) // We hit the end, but still have some unconsumed bits so need one final character to append
|
char operator*() {
|
||||||
*out++ = detail::b32z_lut.to_b32z(r << (5 - bits));
|
// Right-shift off the excess bits we aren't accessing yet
|
||||||
|
return detail::b32z_lut.to_b32z(r >> (bits - 5));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Converts bytes into a base32z encoded character sequence, writing them starting at `out`.
|
||||||
|
/// Returns the final value of out (i.e. the iterator positioned just after the last written base32z
|
||||||
|
/// character).
|
||||||
|
template <typename InputIt, typename OutputIt>
|
||||||
|
OutputIt to_base32z(InputIt begin, InputIt end, OutputIt out) {
|
||||||
|
static_assert(sizeof(decltype(*begin)) == 1, "to_base32z requires chars/bytes");
|
||||||
|
base32z_encoder it{begin, end};
|
||||||
|
return std::copy(it, it.end(), out);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Creates a base32z string from an iterator pair of a byte sequence.
|
/// Creates a base32z string from an iterator pair of a byte sequence.
|
||||||
template <typename It>
|
template <typename It>
|
||||||
std::string to_base32z(It begin, It end) {
|
std::string to_base32z(It begin, It end) {
|
||||||
std::string base32z;
|
std::string base32z;
|
||||||
if constexpr (std::is_base_of_v<std::random_access_iterator_tag, typename std::iterator_traits<It>::iterator_category>)
|
if constexpr (std::is_base_of_v<std::random_access_iterator_tag, typename std::iterator_traits<It>::iterator_category>) {
|
||||||
base32z.reserve((std::distance(begin, end)*8 + 4) / 5); // == bytes*8/5, rounded up.
|
using std::distance;
|
||||||
|
base32z.reserve(to_base32z_size(distance(begin, end)));
|
||||||
|
}
|
||||||
to_base32z(begin, end, std::back_inserter(base32z));
|
to_base32z(begin, end, std::back_inserter(base32z));
|
||||||
return base32z;
|
return base32z;
|
||||||
}
|
}
|
||||||
|
@ -117,15 +158,36 @@ template <typename CharT>
|
||||||
std::string to_base32z(std::basic_string_view<CharT> s) { return to_base32z(s.begin(), s.end()); }
|
std::string to_base32z(std::basic_string_view<CharT> s) { return to_base32z(s.begin(), s.end()); }
|
||||||
inline std::string to_base32z(std::string_view s) { return to_base32z<>(s); }
|
inline std::string to_base32z(std::string_view s) { return to_base32z<>(s); }
|
||||||
|
|
||||||
/// Returns true if all elements in the range are base32z characters
|
/// Returns true if the given [begin, end) range is an acceptable base32z string: specifically every
|
||||||
|
/// character must be in the base32z alphabet, and the string must be a valid encoding length that
|
||||||
|
/// could have been produced by to_base32z (i.e. some lengths are impossible).
|
||||||
template <typename It>
|
template <typename It>
|
||||||
constexpr bool is_base32z(It begin, It end) {
|
constexpr bool is_base32z(It begin, It end) {
|
||||||
static_assert(sizeof(decltype(*begin)) == 1, "is_base32z requires chars/bytes");
|
static_assert(sizeof(decltype(*begin)) == 1, "is_base32z requires chars/bytes");
|
||||||
|
size_t count = 0;
|
||||||
|
constexpr bool random = std::is_base_of_v<std::random_access_iterator_tag, typename std::iterator_traits<It>::iterator_category>;
|
||||||
|
if constexpr (random) {
|
||||||
|
using std::distance;
|
||||||
|
count = distance(begin, end) % 8;
|
||||||
|
if (count == 1 || count == 3 || count == 6) // see below
|
||||||
|
return false;
|
||||||
|
}
|
||||||
for (; begin != end; ++begin) {
|
for (; begin != end; ++begin) {
|
||||||
auto c = static_cast<unsigned char>(*begin);
|
auto c = static_cast<unsigned char>(*begin);
|
||||||
if (detail::b32z_lut.from_b32z(c) == 0 && !(c == 'y' || c == 'Y'))
|
if (detail::b32z_lut.from_b32z(c) == 0 && !(c == 'y' || c == 'Y'))
|
||||||
return false;
|
return false;
|
||||||
|
if constexpr (!random)
|
||||||
|
count++;
|
||||||
}
|
}
|
||||||
|
// Check for a valid length.
|
||||||
|
// - 5n + 0 bytes encodes to 8n chars (no padding bits)
|
||||||
|
// - 5n + 1 bytes encodes to 8n+2 chars (last 2 bits are padding)
|
||||||
|
// - 5n + 2 bytes encodes to 8n+4 chars (last 4 bits are padding)
|
||||||
|
// - 5n + 3 bytes encodes to 8n+5 chars (last 1 bit is padding)
|
||||||
|
// - 5n + 4 bytes encodes to 8n+7 chars (last 3 bits are padding)
|
||||||
|
if constexpr (!random)
|
||||||
|
if (count %= 8; count == 1 || count == 3 || count == 6)
|
||||||
|
return false;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -134,55 +196,89 @@ template <typename CharT>
|
||||||
constexpr bool is_base32z(std::basic_string_view<CharT> s) { return is_base32z(s.begin(), s.end()); }
|
constexpr bool is_base32z(std::basic_string_view<CharT> s) { return is_base32z(s.begin(), s.end()); }
|
||||||
constexpr bool is_base32z(std::string_view s) { return is_base32z<>(s); }
|
constexpr bool is_base32z(std::string_view s) { return is_base32z<>(s); }
|
||||||
|
|
||||||
/// Converts a sequence of base32z digits to bytes. Undefined behaviour if any characters are not
|
/// Iterable object for on-the-fly base32z decoding. Used internally, but also particularly useful
|
||||||
/// valid base32z alphabet characters. It is permitted for the input and output ranges to overlap
|
/// when converting from one encoding to another. The input range must be a valid base32z
|
||||||
/// as long as `out` is no earlier than `begin`. Note that if you pass in a sequence that could not
|
/// encoded string.
|
||||||
/// have been created by a base32z encoding of a byte sequence, we treat the excess bits as if they
|
|
||||||
/// were not provided.
|
|
||||||
///
|
///
|
||||||
/// For example, "yyy" represents a 15-bit value, but a byte sequence is either 8-bit (requiring 2
|
/// Note that we ignore "padding" bits without requiring that they actually be 0. For instance, the
|
||||||
/// characters) or 16-bit (requiring 4). Similarly, "yb" is an impossible encoding because it has
|
/// bytes "\ff\ff" are ideally encoded as "999o" (16 bits of 1s + 4 padding 0 bits), but we don't
|
||||||
/// its 10th bit set (b = 00001), but a base32z encoded value should have all 0's beyond the 8th (or
|
/// require that the padding bits be 0. That is, "9999", "9993", etc. will all decode to the same
|
||||||
/// 16th or 24th or ... bit). We treat any such bits as if they were not specified (even if they
|
/// \ff\ff output string.
|
||||||
/// are): which means "yy", "yb", "yyy", "yy9", "yd", etc. all decode to the same 1-byte value "\0".
|
template <typename InputIt>
|
||||||
template <typename InputIt, typename OutputIt>
|
struct base32z_decoder final {
|
||||||
void from_base32z(InputIt begin, InputIt end, OutputIt out) {
|
private:
|
||||||
static_assert(sizeof(decltype(*begin)) == 1, "from_base32z requires chars/bytes");
|
InputIt _it, _end;
|
||||||
uint_fast16_t curr = 0;
|
static_assert(sizeof(decltype(*_it)) == 1, "base32z_decoder requires chars/bytes input iterator");
|
||||||
int bits = 0; // number of bits we've loaded into val; we always keep this < 8.
|
uint_fast16_t in = 0;
|
||||||
while (begin != end) {
|
int bits = 0; // number of bits loaded into `in`; will be in [8, 12] until we hit the end
|
||||||
curr = curr << 5 | detail::b32z_lut.from_b32z(static_cast<unsigned char>(*begin++));
|
public:
|
||||||
if (bits >= 3) {
|
using iterator_category = std::input_iterator_tag;
|
||||||
bits -= 3; // Added 5, removing 8
|
using difference_type = std::ptrdiff_t;
|
||||||
*out++ = static_cast<detail::byte_type_t<OutputIt>>(
|
using value_type = char;
|
||||||
static_cast<uint8_t>(curr >> bits));
|
using reference = value_type;
|
||||||
curr &= (1 << bits) - 1;
|
using pointer = void;
|
||||||
} else {
|
base32z_decoder(InputIt begin, InputIt end) : _it{std::move(begin)}, _end{std::move(end)} {
|
||||||
bits += 5;
|
if (_it != _end)
|
||||||
}
|
load_byte();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ignore any trailing bits. base32z encoding always has at least as many bits as the source
|
base32z_decoder end() { return {_end, _end}; }
|
||||||
// bytes, which means we should not be able to get here from a properly encoded b32z value with
|
|
||||||
// anything other than 0s: if we have no extra bits (e.g. 5 bytes == 8 b32z chars) then we have
|
bool operator==(const base32z_decoder& i) { return _it == i._it; }
|
||||||
// a 0-bit value; if we have some extra bits (e.g. 6 bytes requires 10 b32z chars, but that
|
bool operator!=(const base32z_decoder& i) { return _it != i._it; }
|
||||||
// contains 50 bits > 48 bits) then those extra bits will be 0s (and this covers the bits -= 3
|
|
||||||
// case above: it'll leave us with 0-4 extra bits, but those extra bits would be 0 if produced
|
base32z_decoder& operator++() {
|
||||||
// from an actual byte sequence).
|
// Discard 8 most significant bits
|
||||||
//
|
bits -= 8;
|
||||||
// The "bits += 5" case, then, means that we could end with 5-7 bits. This, however, cannot be
|
in &= (1 << bits) - 1;
|
||||||
// produced by a valid encoding:
|
if (++_it != _end)
|
||||||
// - 0 bytes gives us 0 chars with 0 leftover bits
|
load_byte();
|
||||||
// - 1 byte gives us 2 chars with 2 leftover bits
|
return *this;
|
||||||
// - 2 bytes gives us 4 chars with 4 leftover bits
|
}
|
||||||
// - 3 bytes gives us 5 chars with 1 leftover bit
|
base32z_decoder operator++(int) { base32z_decoder copy{*this}; ++*this; return copy; }
|
||||||
// - 4 bytes gives us 7 chars with 3 leftover bits
|
|
||||||
// - 5 bytes gives us 8 chars with 0 leftover bits (this is where the cycle repeats)
|
char operator*() {
|
||||||
//
|
return in >> (bits - 8);
|
||||||
// So really the only way we can get 5-7 leftover bits is if you took a 0, 2 or 5 char output (or
|
}
|
||||||
// any 8n + {0,2,5} char output) and added a base32z character to the end. If you do that,
|
|
||||||
// well, too bad: you're giving invalid output and so we're just going to pretend that extra
|
private:
|
||||||
// character you added isn't there by not doing anything here.
|
void load_in() {
|
||||||
|
in = in << 5
|
||||||
|
| detail::b32z_lut.from_b32z(static_cast<unsigned char>(*_it));
|
||||||
|
bits += 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
void load_byte() {
|
||||||
|
load_in();
|
||||||
|
if (bits < 8 && ++_it != _end)
|
||||||
|
load_in();
|
||||||
|
|
||||||
|
// If we hit the _end iterator above then we hit the end of the input with fewer than 8 bits
|
||||||
|
// accumulated to make a full byte. For a properly encoded base32z string this should only
|
||||||
|
// be possible with 0-4 bits of all 0s; these are essentially "padding" bits (e.g. encoding
|
||||||
|
// 2 byte (16 bits) requires 4 b32z chars (20 bits), where only the first 16 bits are
|
||||||
|
// significant). Ideally any padding bits should be 0, but we don't check that and rather
|
||||||
|
// just ignore them.
|
||||||
|
//
|
||||||
|
// It also isn't possible to get here with 5-7 bits if the string passes `is_base32z`
|
||||||
|
// because the length checks we do there disallow such a length as valid. (If you were to
|
||||||
|
// pass such a string to us anyway then we are technically UB, but the current
|
||||||
|
// implementation just ignore the extra bits as if they are extra padding).
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Converts a sequence of base32z digits to bytes. Undefined behaviour if any characters are not
|
||||||
|
/// valid base32z alphabet characters. It is permitted for the input and output ranges to overlap
|
||||||
|
/// as long as `out` is no later than `begin`.
|
||||||
|
///
|
||||||
|
template <typename InputIt, typename OutputIt>
|
||||||
|
OutputIt from_base32z(InputIt begin, InputIt end, OutputIt out) {
|
||||||
|
static_assert(sizeof(decltype(*begin)) == 1, "from_base32z requires chars/bytes");
|
||||||
|
base32z_decoder it{begin, end};
|
||||||
|
auto bend = it.end();
|
||||||
|
while (it != bend)
|
||||||
|
*out++ = static_cast<detail::byte_type_t<OutputIt>>(*it++);
|
||||||
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Convert a base32z sequence into a std::string of bytes. Undefined behaviour if any characters
|
/// Convert a base32z sequence into a std::string of bytes. Undefined behaviour if any characters
|
||||||
|
@ -190,8 +286,10 @@ void from_base32z(InputIt begin, InputIt end, OutputIt out) {
|
||||||
template <typename It>
|
template <typename It>
|
||||||
std::string from_base32z(It begin, It end) {
|
std::string from_base32z(It begin, It end) {
|
||||||
std::string bytes;
|
std::string bytes;
|
||||||
if constexpr (std::is_base_of_v<std::random_access_iterator_tag, typename std::iterator_traits<It>::iterator_category>)
|
if constexpr (std::is_base_of_v<std::random_access_iterator_tag, typename std::iterator_traits<It>::iterator_category>) {
|
||||||
bytes.reserve((std::distance(begin, end)*5 + 7) / 8); // == chars*5/8, rounded up.
|
using std::distance;
|
||||||
|
bytes.reserve(from_base32z_size(distance(begin, end)));
|
||||||
|
}
|
||||||
from_base32z(begin, end, std::back_inserter(bytes));
|
from_base32z(begin, end, std::back_inserter(bytes));
|
||||||
return bytes;
|
return bytes;
|
||||||
}
|
}
|
||||||
|
|
294
oxenmq/base64.h
294
oxenmq/base64.h
|
@ -76,70 +76,153 @@ static_assert(b64_lut.from_b64('/') == 63 && b64_lut.from_b64('7') == 59 && b64_
|
||||||
|
|
||||||
} // namespace detail
|
} // namespace detail
|
||||||
|
|
||||||
/// Converts bytes into a base64 encoded character sequence.
|
/// Returns the number of characters required to encode a base64 string from the given number of bytes.
|
||||||
template <typename InputIt, typename OutputIt>
|
inline constexpr size_t to_base64_size(size_t byte_size, bool padded = true) {
|
||||||
void to_base64(InputIt begin, InputIt end, OutputIt out) {
|
return padded
|
||||||
static_assert(sizeof(decltype(*begin)) == 1, "to_base64 requires chars/bytes");
|
? (byte_size + 2) / 3 * 4 // bytes*4/3, rounded up to the next multiple of 4
|
||||||
int bits = 0; // Tracks the number of unconsumed bits held in r, will always be in {0, 2, 4}
|
: (byte_size * 4 + 2) / 3; // ⌈bytes*4/3⌉
|
||||||
std::uint_fast16_t r = 0;
|
}
|
||||||
while (begin != end) {
|
/// Returns the (maximum) number of bytes required to decode a base64 string of the given size.
|
||||||
r = r << 8 | static_cast<unsigned char>(*begin++);
|
/// Note that this may overallocate by 1-2 bytes if the size includes 1-2 padding chars.
|
||||||
|
inline constexpr size_t from_base64_size(size_t b64_size) {
|
||||||
// we just added 8 bits, so we can *always* consume 6 to produce one character, so (net) we
|
return b64_size * 3 / 4; // == ⌊bits/8⌋; floor because we ignore trailing "impossible" bits (see below)
|
||||||
// are adding 2 bits.
|
|
||||||
bits += 2;
|
|
||||||
*out++ = detail::b64_lut.to_b64(r >> bits); // Right-shift off the bits we aren't consuming right now
|
|
||||||
|
|
||||||
// Drop the bits we don't want to keep (because we just consumed them)
|
|
||||||
r &= (1 << bits) - 1;
|
|
||||||
|
|
||||||
if (bits == 6) { // We have enough bits to produce a second character (which means we had 4 before and added 8)
|
|
||||||
bits = 0;
|
|
||||||
*out++ = detail::b64_lut.to_b64(r);
|
|
||||||
r = 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// If bits == 0 then we ended our 6-bit outputs coinciding with 8-bit values, i.e. at a multiple
|
|
||||||
// of 24 bits: this means we don't have anything else to output and don't need any padding.
|
|
||||||
if (bits == 2) {
|
|
||||||
// We finished with 2 unconsumed bits, which means we ended 1 byte past a 24-bit group (e.g.
|
|
||||||
// 1 byte, 4 bytes, 301 bytes, etc.); since we need to always be a multiple of 4 output
|
|
||||||
// characters that means we've produced 1: so we right-fill 0s to get the next char, then
|
|
||||||
// add two padding ='s.
|
|
||||||
*out++ = detail::b64_lut.to_b64(r << 4);
|
|
||||||
*out++ = '=';
|
|
||||||
*out++ = '=';
|
|
||||||
} else if (bits == 4) {
|
|
||||||
// 4 bits left means we produced 2 6-bit values from the first 2 bytes of a 3-byte group.
|
|
||||||
// Fill 0s to get the last one, plus one padding output.
|
|
||||||
*out++ = detail::b64_lut.to_b64(r << 2);
|
|
||||||
*out++ = '=';
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Creates and returns a base64 string from an iterator pair of a character sequence
|
/// Iterable object for on-the-fly base64 encoding. Used internally, but also particularly useful
|
||||||
|
/// when converting from one encoding to another.
|
||||||
|
template <typename InputIt>
|
||||||
|
struct base64_encoder final {
|
||||||
|
private:
|
||||||
|
InputIt _it, _end;
|
||||||
|
static_assert(sizeof(decltype(*_it)) == 1, "base64_encoder requires chars/bytes input iterator");
|
||||||
|
// How much padding (at most) we can add at the end
|
||||||
|
int padding;
|
||||||
|
// Number of bits held in r; will always be >= 6 until we are at the end.
|
||||||
|
int bits{_it != _end ? 8 : 0};
|
||||||
|
// Holds bits of data we've already read, which might belong to current or next chars
|
||||||
|
uint_fast16_t r{bits ? static_cast<unsigned char>(*_it) : (unsigned char)0};
|
||||||
|
public:
|
||||||
|
using iterator_category = std::input_iterator_tag;
|
||||||
|
using difference_type = std::ptrdiff_t;
|
||||||
|
using value_type = char;
|
||||||
|
using reference = value_type;
|
||||||
|
using pointer = void;
|
||||||
|
base64_encoder(InputIt begin, InputIt end, bool padded = true)
|
||||||
|
: _it{std::move(begin)}, _end{std::move(end)}, padding{padded} {}
|
||||||
|
|
||||||
|
base64_encoder end() { return {_end, _end, false}; }
|
||||||
|
|
||||||
|
bool operator==(const base64_encoder& i) { return _it == i._it && bits == i.bits && padding == i.padding; }
|
||||||
|
bool operator!=(const base64_encoder& i) { return !(*this == i); }
|
||||||
|
|
||||||
|
base64_encoder& operator++() {
|
||||||
|
if (bits == 0) {
|
||||||
|
padding--;
|
||||||
|
return *this;
|
||||||
|
}
|
||||||
|
assert(bits >= 6);
|
||||||
|
// Discard the most significant 6 bits
|
||||||
|
bits -= 6;
|
||||||
|
r &= (1 << bits) - 1;
|
||||||
|
// If we end up with less than 6 significant bits then try to pull another 8 bits:
|
||||||
|
if (bits < 6 && _it != _end) {
|
||||||
|
if (++_it != _end) {
|
||||||
|
r = (r << 8) | static_cast<unsigned char>(*_it);
|
||||||
|
bits += 8;
|
||||||
|
} else if (bits > 0) {
|
||||||
|
// No more input bytes, so shift `r` to put the bits we have into the most
|
||||||
|
// significant bit position for the final character, and figure out how many padding
|
||||||
|
// bytes we want to append. E.g. if we have "11" we want
|
||||||
|
// the last character to be encoded "110000".
|
||||||
|
if (padding) {
|
||||||
|
// padding should be:
|
||||||
|
// 3n+0 input => 4n output, no padding, handled below
|
||||||
|
// 3n+1 input => 4n+2 output + 2 padding; we'll land here with 2 trailing bits
|
||||||
|
// 3n+2 input => 4n+3 output + 1 padding; we'll land here with 4 trailing bits
|
||||||
|
padding = 3 - bits / 2;
|
||||||
|
}
|
||||||
|
r <<= (6 - bits);
|
||||||
|
bits = 6;
|
||||||
|
} else {
|
||||||
|
padding = 0; // No excess bits, so input was a multiple of 3 and thus no padding
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return *this;
|
||||||
|
}
|
||||||
|
base64_encoder operator++(int) { base64_encoder copy{*this}; ++*this; return copy; }
|
||||||
|
|
||||||
|
char operator*() {
|
||||||
|
if (bits == 0 && padding)
|
||||||
|
return '=';
|
||||||
|
// Right-shift off the excess bits we aren't accessing yet
|
||||||
|
return detail::b64_lut.to_b64(r >> (bits - 6));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Converts bytes into a base64 encoded character sequence, writing them starting at `out`.
|
||||||
|
/// Returns the final value of out (i.e. the iterator positioned just after the last written base64
|
||||||
|
/// character).
|
||||||
|
template <typename InputIt, typename OutputIt>
|
||||||
|
OutputIt to_base64(InputIt begin, InputIt end, OutputIt out, bool padded = true) {
|
||||||
|
static_assert(sizeof(decltype(*begin)) == 1, "to_base64 requires chars/bytes");
|
||||||
|
auto it = base64_encoder{begin, end, padded};
|
||||||
|
return std::copy(it, it.end(), out);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates and returns a base64 string from an iterator pair of a character sequence. The
|
||||||
|
/// resulting string will have '=' padding, if appropriate.
|
||||||
template <typename It>
|
template <typename It>
|
||||||
std::string to_base64(It begin, It end) {
|
std::string to_base64(It begin, It end) {
|
||||||
std::string base64;
|
std::string base64;
|
||||||
if constexpr (std::is_base_of_v<std::random_access_iterator_tag, typename std::iterator_traits<It>::iterator_category>)
|
if constexpr (std::is_base_of_v<std::random_access_iterator_tag, typename std::iterator_traits<It>::iterator_category>) {
|
||||||
base64.reserve((std::distance(begin, end) + 2) / 3 * 4); // bytes*4/3, rounded up to the next multiple of 4
|
using std::distance;
|
||||||
|
base64.reserve(to_base64_size(distance(begin, end)));
|
||||||
|
}
|
||||||
to_base64(begin, end, std::back_inserter(base64));
|
to_base64(begin, end, std::back_inserter(base64));
|
||||||
return base64;
|
return base64;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Creates a base64 string from an iterable, std::string-like object
|
/// Creates and returns a base64 string from an iterator pair of a character sequence. The
|
||||||
|
/// resulting string will not be padded.
|
||||||
|
template <typename It>
|
||||||
|
std::string to_base64_unpadded(It begin, It end) {
|
||||||
|
std::string base64;
|
||||||
|
if constexpr (std::is_base_of_v<std::random_access_iterator_tag, typename std::iterator_traits<It>::iterator_category>) {
|
||||||
|
using std::distance;
|
||||||
|
base64.reserve(to_base64_size(distance(begin, end), false));
|
||||||
|
}
|
||||||
|
to_base64(begin, end, std::back_inserter(base64), false);
|
||||||
|
return base64;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates a base64 string from an iterable, std::string-like object. The string will have '='
|
||||||
|
/// padding, if appropriate.
|
||||||
template <typename CharT>
|
template <typename CharT>
|
||||||
std::string to_base64(std::basic_string_view<CharT> s) { return to_base64(s.begin(), s.end()); }
|
std::string to_base64(std::basic_string_view<CharT> s) { return to_base64(s.begin(), s.end()); }
|
||||||
inline std::string to_base64(std::string_view s) { return to_base64<>(s); }
|
inline std::string to_base64(std::string_view s) { return to_base64<>(s); }
|
||||||
|
|
||||||
|
/// Creates a base64 string from an iterable, std::string-like object. The string will not be
|
||||||
|
/// padded.
|
||||||
|
template <typename CharT>
|
||||||
|
std::string to_base64_unpadded(std::basic_string_view<CharT> s) { return to_base64_unpadded(s.begin(), s.end()); }
|
||||||
|
inline std::string to_base64_unpadded(std::string_view s) { return to_base64_unpadded<>(s); }
|
||||||
|
|
||||||
/// Returns true if the range is a base64 encoded value; we allow (but do not require) '=' padding,
|
/// Returns true if the range is a base64 encoded value; we allow (but do not require) '=' padding,
|
||||||
/// but only at the end, only 1 or 2, and only if it pads out the total to a multiple of 4.
|
/// but only at the end, only 1 or 2, and only if it pads out the total to a multiple of 4.
|
||||||
|
/// Otherwise the string must contain only valid base64 characters, and must not have a length of
|
||||||
|
/// 4n+1 (because that cannot be produced by base64 encoding).
|
||||||
template <typename It>
|
template <typename It>
|
||||||
constexpr bool is_base64(It begin, It end) {
|
constexpr bool is_base64(It begin, It end) {
|
||||||
static_assert(sizeof(decltype(*begin)) == 1, "is_base64 requires chars/bytes");
|
static_assert(sizeof(decltype(*begin)) == 1, "is_base64 requires chars/bytes");
|
||||||
using std::distance;
|
using std::distance;
|
||||||
using std::prev;
|
using std::prev;
|
||||||
|
size_t count = 0;
|
||||||
|
constexpr bool random = std::is_base_of_v<std::random_access_iterator_tag, typename std::iterator_traits<It>::iterator_category>;
|
||||||
|
if constexpr (random) {
|
||||||
|
count = distance(begin, end) % 4;
|
||||||
|
if (count == 1)
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
// Allow 1 or 2 padding chars *if* they pad it to a multiple of 4.
|
// Allow 1 or 2 padding chars *if* they pad it to a multiple of 4.
|
||||||
if (begin != end && distance(begin, end) % 4 == 0) {
|
if (begin != end && distance(begin, end) % 4 == 0) {
|
||||||
|
@ -154,7 +237,14 @@ constexpr bool is_base64(It begin, It end) {
|
||||||
auto c = static_cast<unsigned char>(*begin);
|
auto c = static_cast<unsigned char>(*begin);
|
||||||
if (detail::b64_lut.from_b64(c) == 0 && c != 'A')
|
if (detail::b64_lut.from_b64(c) == 0 && c != 'A')
|
||||||
return false;
|
return false;
|
||||||
|
if constexpr (!random)
|
||||||
|
count++;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if constexpr (!random)
|
||||||
|
if (count % 4 == 1) // base64 encoding will produce 4n, 4n+2, 4n+3, but never 4n+1
|
||||||
|
return false;
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -163,10 +253,87 @@ template <typename CharT>
|
||||||
constexpr bool is_base64(std::basic_string_view<CharT> s) { return is_base64(s.begin(), s.end()); }
|
constexpr bool is_base64(std::basic_string_view<CharT> s) { return is_base64(s.begin(), s.end()); }
|
||||||
constexpr bool is_base64(std::string_view s) { return is_base64(s.begin(), s.end()); }
|
constexpr bool is_base64(std::string_view s) { return is_base64(s.begin(), s.end()); }
|
||||||
|
|
||||||
|
/// Iterable object for on-the-fly base64 decoding. Used internally, but also particularly useful
|
||||||
|
/// when converting from one encoding to another. The input range must be a valid base64 encoded
|
||||||
|
/// string (with or without padding).
|
||||||
|
///
|
||||||
|
/// Note that we ignore "padding" bits without requiring that they actually be 0. For instance, the
|
||||||
|
/// bytes "\ff\ff" are ideally encoded as "//8=" (16 bits of 1s + 2 padding 0 bits, then a full
|
||||||
|
/// 6-bit padding char). We don't, however, require that the padding bits be 0. That is, "///=",
|
||||||
|
/// "//9=", "//+=", etc. will all decode to the same \ff\ff output string.
|
||||||
|
template <typename InputIt>
|
||||||
|
struct base64_decoder final {
|
||||||
|
private:
|
||||||
|
InputIt _it, _end;
|
||||||
|
static_assert(sizeof(decltype(*_it)) == 1, "base64_decoder requires chars/bytes input iterator");
|
||||||
|
uint_fast16_t in = 0;
|
||||||
|
int bits = 0; // number of bits loaded into `in`; will be in [8, 12] until we hit the end
|
||||||
|
public:
|
||||||
|
using iterator_category = std::input_iterator_tag;
|
||||||
|
using difference_type = std::ptrdiff_t;
|
||||||
|
using value_type = char;
|
||||||
|
using reference = value_type;
|
||||||
|
using pointer = void;
|
||||||
|
base64_decoder(InputIt begin, InputIt end) : _it{std::move(begin)}, _end{std::move(end)} {
|
||||||
|
if (_it != _end)
|
||||||
|
load_byte();
|
||||||
|
}
|
||||||
|
|
||||||
|
base64_decoder end() { return {_end, _end}; }
|
||||||
|
|
||||||
|
bool operator==(const base64_decoder& i) { return _it == i._it; }
|
||||||
|
bool operator!=(const base64_decoder& i) { return _it != i._it; }
|
||||||
|
|
||||||
|
base64_decoder& operator++() {
|
||||||
|
// Discard 8 most significant bits
|
||||||
|
bits -= 8;
|
||||||
|
in &= (1 << bits) - 1;
|
||||||
|
if (++_it != _end)
|
||||||
|
load_byte();
|
||||||
|
return *this;
|
||||||
|
}
|
||||||
|
base64_decoder operator++(int) { base64_decoder copy{*this}; ++*this; return copy; }
|
||||||
|
|
||||||
|
char operator*() {
|
||||||
|
return in >> (bits - 8);
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
void load_in() {
|
||||||
|
// We hit padding trying to read enough for a full byte, so we're done. (And since you were
|
||||||
|
// already supposed to have checked validity with is_base64, the padding can only be at the
|
||||||
|
// end).
|
||||||
|
auto c = static_cast<unsigned char>(*_it);
|
||||||
|
if (c == '=') {
|
||||||
|
_it = _end;
|
||||||
|
bits = 0;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
in = in << 6
|
||||||
|
| detail::b64_lut.from_b64(c);
|
||||||
|
bits += 6;
|
||||||
|
}
|
||||||
|
|
||||||
|
void load_byte() {
|
||||||
|
load_in();
|
||||||
|
if (bits && bits < 8 && ++_it != _end)
|
||||||
|
load_in();
|
||||||
|
|
||||||
|
// If we hit the _end iterator above then we hit the end of the input (or hit padding) with
|
||||||
|
// fewer than 8 bits accumulated to make a full byte. For a properly encoded base64 string
|
||||||
|
// this should only be possible with 0, 2, or 4 bits of all 0s; these are essentially
|
||||||
|
// "padding" bits (e.g. encoding 2 byte (16 bits) requires 3 b64 chars (18 bits), where
|
||||||
|
// only the first 16 bits are significant). Ideally any padding bits should be 0, but we
|
||||||
|
// don't check that and rather just ignore them.
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
/// Converts a sequence of base64 digits to bytes. Undefined behaviour if any characters are not
|
/// Converts a sequence of base64 digits to bytes. Undefined behaviour if any characters are not
|
||||||
/// valid base64 alphabet characters. It is permitted for the input and output ranges to overlap as
|
/// valid base64 alphabet characters. It is permitted for the input and output ranges to overlap as
|
||||||
/// long as `out` is no earlier than `begin`. Trailing padding characters are permitted but not
|
/// long as `out` is no later than `begin`. Trailing padding characters are permitted but not
|
||||||
/// required.
|
/// required. Returns the final value of out (that is, the iterator positioned just after the
|
||||||
|
/// last written character).
|
||||||
///
|
///
|
||||||
/// It is possible to provide "impossible" base64 encoded values; for example "YWJja" which has 30
|
/// It is possible to provide "impossible" base64 encoded values; for example "YWJja" which has 30
|
||||||
/// bits of data even though a base64 encoded byte string should have 24 (4 chars) or 36 (6 chars)
|
/// bits of data even though a base64 encoded byte string should have 24 (4 chars) or 36 (6 chars)
|
||||||
|
@ -175,30 +342,13 @@ constexpr bool is_base64(std::string_view s) { return is_base64(s.begin(), s.end
|
||||||
/// encoding of "abcd") and "YWJjZB", "YWJjZC", ..., "YWJjZP" all decode to the same "abcd" value:
|
/// encoding of "abcd") and "YWJjZB", "YWJjZC", ..., "YWJjZP" all decode to the same "abcd" value:
|
||||||
/// the last 4 bits of the last character are essentially considered padding.
|
/// the last 4 bits of the last character are essentially considered padding.
|
||||||
template <typename InputIt, typename OutputIt>
|
template <typename InputIt, typename OutputIt>
|
||||||
void from_base64(InputIt begin, InputIt end, OutputIt out) {
|
OutputIt from_base64(InputIt begin, InputIt end, OutputIt out) {
|
||||||
static_assert(sizeof(decltype(*begin)) == 1, "from_base64 requires chars/bytes");
|
static_assert(sizeof(decltype(*begin)) == 1, "from_base64 requires chars/bytes");
|
||||||
uint_fast16_t curr = 0;
|
base64_decoder it{begin, end};
|
||||||
int bits = 0; // number of bits we've loaded into val; we always keep this < 8.
|
auto bend = it.end();
|
||||||
while (begin != end) {
|
while (it != bend)
|
||||||
auto c = static_cast<unsigned char>(*begin++);
|
*out++ = static_cast<detail::byte_type_t<OutputIt>>(*it++);
|
||||||
|
return out;
|
||||||
// padding; don't bother checking if we're at the end because is_base64 is a precondition
|
|
||||||
// and we're allowed UB if it isn't satisfied.
|
|
||||||
if (c == '=') continue;
|
|
||||||
|
|
||||||
curr = curr << 6 | detail::b64_lut.from_b64(c);
|
|
||||||
if (bits == 0)
|
|
||||||
bits = 6;
|
|
||||||
else {
|
|
||||||
bits -= 2; // Added 6, removing 8
|
|
||||||
*out++ = static_cast<detail::byte_type_t<OutputIt>>(
|
|
||||||
static_cast<uint8_t>(curr >> bits));
|
|
||||||
curr &= (1 << bits) - 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Don't worry about leftover bits because either they have to be 0, or they can't happen at
|
|
||||||
// all. See base32z.h for why: the reasoning is exactly the same (except using 6 bits per
|
|
||||||
// character here instead of 5).
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Converts base64 digits from a iterator pair of characters into a std::string of bytes.
|
/// Converts base64 digits from a iterator pair of characters into a std::string of bytes.
|
||||||
|
@ -206,8 +356,10 @@ void from_base64(InputIt begin, InputIt end, OutputIt out) {
|
||||||
template <typename It>
|
template <typename It>
|
||||||
std::string from_base64(It begin, It end) {
|
std::string from_base64(It begin, It end) {
|
||||||
std::string bytes;
|
std::string bytes;
|
||||||
if constexpr (std::is_base_of_v<std::random_access_iterator_tag, typename std::iterator_traits<It>::iterator_category>)
|
if constexpr (std::is_base_of_v<std::random_access_iterator_tag, typename std::iterator_traits<It>::iterator_category>) {
|
||||||
bytes.reserve(std::distance(begin, end)*6 / 8); // each digit carries 6 bits; this may overallocate by 1-2 bytes due to padding
|
using std::distance;
|
||||||
|
bytes.reserve(from_base64_size(distance(begin, end)));
|
||||||
|
}
|
||||||
from_base64(begin, end, std::back_inserter(bytes));
|
from_base64(begin, end, std::back_inserter(bytes));
|
||||||
return bytes;
|
return bytes;
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,306 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <cassert>
|
||||||
|
#include <charconv>
|
||||||
|
#include <stdexcept>
|
||||||
|
#include <string_view>
|
||||||
|
#include <unordered_map>
|
||||||
|
#include <utility>
|
||||||
|
#include <variant>
|
||||||
|
|
||||||
|
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 <typename IntType>
|
||||||
|
char* apple_to_chars10(char* buf, IntType val) {
|
||||||
|
static_assert(std::is_integral_v<IntType> && sizeof(IntType) <= 64);
|
||||||
|
if constexpr (std::is_signed_v<IntType>) {
|
||||||
|
if (val < 0) {
|
||||||
|
buf[0] = '-';
|
||||||
|
return apple_to_chars10(buf+1, static_cast<std::make_unsigned_t<IntType>>(-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<char>(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<char*, char*>;
|
||||||
|
// Our data is a begin/end pointer pair for the root list, or a pointer to our parent if a
|
||||||
|
// sublist.
|
||||||
|
std::variant<buf_span, bt_list_producer*, bt_dict_producer*> 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 <typename IntType>
|
||||||
|
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 <typename IntType, std::enable_if_t<std::is_integral_v<IntType>, 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<size_t>(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<size_t>(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 <typename IntType, std::enable_if_t<std::is_integral_v<IntType>, 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 <typename IntType, std::enable_if_t<std::is_integral_v<IntType>, 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 <typename ForwardIt>
|
||||||
|
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 <typename T, std::enable_if_t<std::is_convertible_v<T, std::string_view> || std::is_integral_v<T>, 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 <typename ForwardIt, std::enable_if_t<!std::is_convertible_v<ForwardIt, std::string_view>, 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_t<std::decay_t<decltype(from->first)>>;
|
||||||
|
using ValType = std::decay_t<decltype(from->second)>;
|
||||||
|
static_assert(std::is_convertible_v<decltype(from->first), std::string_view>);
|
||||||
|
static_assert(std::is_convertible_v<ValType, std::string_view> || std::is_integral_v<ValType>);
|
||||||
|
using BadUnorderedMap = std::unordered_map<KeyType, ValType>;
|
||||||
|
static_assert(!( // Disallow unordered_map iterators because they are not going to be ordered.
|
||||||
|
std::is_same_v<typename BadUnorderedMap::iterator, ForwardIt> ||
|
||||||
|
std::is_same_v<typename BadUnorderedMap::const_iterator, ForwardIt>));
|
||||||
|
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
|
|
@ -27,6 +27,10 @@
|
||||||
// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
|
||||||
#include "bt_serialize.h"
|
#include "bt_serialize.h"
|
||||||
|
#include "bt_producer.h"
|
||||||
|
#include "variant.h"
|
||||||
|
|
||||||
|
#include <cassert>
|
||||||
#include <iterator>
|
#include <iterator>
|
||||||
|
|
||||||
namespace oxenmq {
|
namespace oxenmq {
|
||||||
|
@ -228,4 +232,138 @@ std::pair<std::string_view, std::string_view> bt_dict_consumer::next_string() {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
bt_list_producer::bt_list_producer(bt_list_producer* parent, std::string_view prefix)
|
||||||
|
: data{parent}, buffer{parent->buffer}, from{buffer.first} {
|
||||||
|
parent->has_child = true;
|
||||||
|
buffer_append(prefix);
|
||||||
|
append_intermediate_ends();
|
||||||
|
}
|
||||||
|
|
||||||
|
bt_list_producer::bt_list_producer(bt_dict_producer* parent, std::string_view prefix)
|
||||||
|
: data{parent}, buffer{parent->buffer}, from{buffer.first} {
|
||||||
|
parent->has_child = true;
|
||||||
|
buffer_append(prefix);
|
||||||
|
append_intermediate_ends();
|
||||||
|
}
|
||||||
|
|
||||||
|
bt_list_producer::bt_list_producer(bt_list_producer&& other)
|
||||||
|
: data{std::move(other.data)}, buffer{other.buffer}, from{other.from}, to{other.to} {
|
||||||
|
if (other.has_child) throw std::logic_error{"Cannot move bt_list/dict_producer with active sublists/subdicts"};
|
||||||
|
var::visit([](auto& x) {
|
||||||
|
if constexpr (!std::is_same_v<buf_span&, decltype(x)>)
|
||||||
|
x = nullptr;
|
||||||
|
}, other.data);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
bt_list_producer::bt_list_producer(char* begin, char* end)
|
||||||
|
: data{buf_span{begin, end}}, buffer{*std::get_if<buf_span>(&data)}, from{buffer.first} {
|
||||||
|
buffer_append("l"sv);
|
||||||
|
append_intermediate_ends();
|
||||||
|
}
|
||||||
|
|
||||||
|
bt_list_producer::~bt_list_producer() {
|
||||||
|
var::visit([this](auto& x) {
|
||||||
|
if constexpr (!std::is_same_v<buf_span&, decltype(x)>) {
|
||||||
|
if (!x)
|
||||||
|
return;
|
||||||
|
|
||||||
|
assert(!has_child);
|
||||||
|
assert(x->has_child);
|
||||||
|
x->has_child = false;
|
||||||
|
// We've already written the intermediate 'e', so just increment the buffer to
|
||||||
|
// finalize it.
|
||||||
|
buffer.first++;
|
||||||
|
}
|
||||||
|
}, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
void bt_list_producer::append(std::string_view data) {
|
||||||
|
if (has_child) throw std::logic_error{"Cannot append to list when a sublist is active"};
|
||||||
|
append_impl(data);
|
||||||
|
append_intermediate_ends();
|
||||||
|
}
|
||||||
|
|
||||||
|
bt_list_producer bt_list_producer::append_list() {
|
||||||
|
if (has_child) throw std::logic_error{"Cannot call append_list while another nested list/dict is active"};
|
||||||
|
return bt_list_producer{this};
|
||||||
|
}
|
||||||
|
|
||||||
|
bt_dict_producer bt_list_producer::append_dict() {
|
||||||
|
if (has_child) throw std::logic_error{"Cannot call append_dict while another nested list/dict is active"};
|
||||||
|
return bt_dict_producer{this};
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
void bt_list_producer::buffer_append(std::string_view d, bool advance) {
|
||||||
|
var::visit([d, advance, this](auto& x) {
|
||||||
|
if constexpr (std::is_same_v<buf_span&, decltype(x)>) {
|
||||||
|
size_t avail = std::distance(x.first, x.second);
|
||||||
|
if (d.size() > avail)
|
||||||
|
throw std::length_error{"Cannot write bt_producer: buffer size exceeded"};
|
||||||
|
std::copy(d.begin(), d.end(), x.first);
|
||||||
|
to = x.first + d.size();
|
||||||
|
if (advance)
|
||||||
|
x.first += d.size();
|
||||||
|
} else {
|
||||||
|
x->buffer_append(d, advance);
|
||||||
|
}
|
||||||
|
}, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
static constexpr std::string_view eee = "eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee"sv;
|
||||||
|
|
||||||
|
void bt_list_producer::append_intermediate_ends(size_t count) {
|
||||||
|
return var::visit([this, count](auto& x) mutable {
|
||||||
|
if constexpr (std::is_same_v<buf_span&, decltype(x)>) {
|
||||||
|
for (; count > eee.size(); count -= eee.size())
|
||||||
|
buffer_append(eee, false);
|
||||||
|
buffer_append(eee.substr(0, count), false);
|
||||||
|
} else {
|
||||||
|
// x is a parent pointer
|
||||||
|
x->append_intermediate_ends(count + 1);
|
||||||
|
to = x->to - 1; // Our `to` should be one 'e' before our parent's `to`.
|
||||||
|
}
|
||||||
|
}, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
void bt_list_producer::append_impl(std::string_view s) {
|
||||||
|
char buf[21]; // length + ':'
|
||||||
|
auto* ptr = write_integer(s.size(), buf);
|
||||||
|
*ptr++ = ':';
|
||||||
|
buffer_append({buf, static_cast<size_t>(ptr-buf)});
|
||||||
|
buffer_append(s);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Subdict constructors
|
||||||
|
bt_dict_producer::bt_dict_producer(bt_list_producer* parent) : bt_list_producer{parent, "d"sv} {}
|
||||||
|
bt_dict_producer::bt_dict_producer(bt_dict_producer* parent) : bt_list_producer{parent, "d"sv} {}
|
||||||
|
|
||||||
|
#ifndef NDEBUG
|
||||||
|
|
||||||
|
void bt_dict_producer::check_incrementing_key(size_t size) {
|
||||||
|
std::string_view this_key{buffer.first - size, size};
|
||||||
|
assert(!last_key.data() || this_key > last_key);
|
||||||
|
last_key = this_key;
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif
|
||||||
|
|
||||||
|
bt_dict_producer bt_dict_producer::append_dict(std::string_view key) {
|
||||||
|
if (has_child) throw std::logic_error{"Cannot call append_dict while another nested list/dict is active"};
|
||||||
|
append_impl(key);
|
||||||
|
check_incrementing_key(key.size());
|
||||||
|
return bt_dict_producer{this};
|
||||||
|
}
|
||||||
|
|
||||||
|
bt_list_producer bt_dict_producer::append_list(std::string_view key) {
|
||||||
|
if (has_child) throw std::logic_error{"Cannot call append_list while another nested list/dict is active"};
|
||||||
|
append_impl(key);
|
||||||
|
check_incrementing_key(key.size());
|
||||||
|
return bt_list_producer{this};
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
} // namespace oxenmq
|
} // namespace oxenmq
|
||||||
|
|
135
oxenmq/hex.h
135
oxenmq/hex.h
|
@ -62,23 +62,65 @@ static_assert(hex_lut.from_hex('a') == 10 && hex_lut.from_hex('F') == 15 && hex_
|
||||||
|
|
||||||
} // namespace detail
|
} // namespace detail
|
||||||
|
|
||||||
/// Creates hex digits from a character sequence.
|
/// Returns the number of characters required to encode a hex string from the given number of bytes.
|
||||||
template <typename InputIt, typename OutputIt>
|
inline constexpr size_t to_hex_size(size_t byte_size) { return byte_size * 2; }
|
||||||
void to_hex(InputIt begin, InputIt end, OutputIt out) {
|
/// Returns the number of bytes required to decode a hex string of the given size.
|
||||||
static_assert(sizeof(decltype(*begin)) == 1, "to_hex requires chars/bytes");
|
inline constexpr size_t from_hex_size(size_t hex_size) { return hex_size / 2; }
|
||||||
for (; begin != end; ++begin) {
|
|
||||||
uint8_t c = static_cast<uint8_t>(*begin);
|
/// Iterable object for on-the-fly hex encoding. Used internally, but also particularly useful when
|
||||||
*out++ = detail::hex_lut.to_hex(c >> 4);
|
/// converting from one encoding to another.
|
||||||
*out++ = detail::hex_lut.to_hex(c & 0x0f);
|
template <typename InputIt>
|
||||||
|
struct hex_encoder final {
|
||||||
|
private:
|
||||||
|
InputIt _it, _end;
|
||||||
|
static_assert(sizeof(decltype(*_it)) == 1, "hex_encoder requires chars/bytes input iterator");
|
||||||
|
uint8_t c = 0;
|
||||||
|
bool second_half = false;
|
||||||
|
public:
|
||||||
|
using iterator_category = std::input_iterator_tag;
|
||||||
|
using difference_type = std::ptrdiff_t;
|
||||||
|
using value_type = char;
|
||||||
|
using reference = value_type;
|
||||||
|
using pointer = void;
|
||||||
|
hex_encoder(InputIt begin, InputIt end) : _it{std::move(begin)}, _end{std::move(end)} {}
|
||||||
|
|
||||||
|
hex_encoder end() { return {_end, _end}; }
|
||||||
|
|
||||||
|
bool operator==(const hex_encoder& i) { return _it == i._it && second_half == i.second_half; }
|
||||||
|
bool operator!=(const hex_encoder& i) { return !(*this == i); }
|
||||||
|
|
||||||
|
hex_encoder& operator++() {
|
||||||
|
second_half = !second_half;
|
||||||
|
if (!second_half)
|
||||||
|
++_it;
|
||||||
|
return *this;
|
||||||
}
|
}
|
||||||
|
hex_encoder operator++(int) { hex_encoder copy{*this}; ++*this; return copy; }
|
||||||
|
char operator*() {
|
||||||
|
return detail::hex_lut.to_hex(second_half
|
||||||
|
? c & 0x0f
|
||||||
|
: (c = static_cast<uint8_t>(*_it)) >> 4);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Creates hex digits from a character sequence given by iterators, writes them starting at `out`.
|
||||||
|
/// Returns the final value of out (i.e. the iterator positioned just after the last written
|
||||||
|
/// hex character).
|
||||||
|
template <typename InputIt, typename OutputIt>
|
||||||
|
OutputIt to_hex(InputIt begin, InputIt end, OutputIt out) {
|
||||||
|
static_assert(sizeof(decltype(*begin)) == 1, "to_hex requires chars/bytes");
|
||||||
|
auto it = hex_encoder{begin, end};
|
||||||
|
return std::copy(it, it.end(), out);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Creates a string of hex digits from a character sequence iterator pair
|
/// Creates a string of hex digits from a character sequence iterator pair
|
||||||
template <typename It>
|
template <typename It>
|
||||||
std::string to_hex(It begin, It end) {
|
std::string to_hex(It begin, It end) {
|
||||||
std::string hex;
|
std::string hex;
|
||||||
if constexpr (std::is_base_of_v<std::random_access_iterator_tag, typename std::iterator_traits<It>::iterator_category>)
|
if constexpr (std::is_base_of_v<std::random_access_iterator_tag, typename std::iterator_traits<It>::iterator_category>) {
|
||||||
hex.reserve(2 * std::distance(begin, end));
|
using std::distance;
|
||||||
|
hex.reserve(to_hex_size(distance(begin, end)));
|
||||||
|
}
|
||||||
to_hex(begin, end, std::back_inserter(hex));
|
to_hex(begin, end, std::back_inserter(hex));
|
||||||
return hex;
|
return hex;
|
||||||
}
|
}
|
||||||
|
@ -101,9 +143,11 @@ template <typename It>
|
||||||
constexpr bool is_hex(It begin, It end) {
|
constexpr bool is_hex(It begin, It end) {
|
||||||
static_assert(sizeof(decltype(*begin)) == 1, "is_hex requires chars/bytes");
|
static_assert(sizeof(decltype(*begin)) == 1, "is_hex requires chars/bytes");
|
||||||
constexpr bool ra = std::is_base_of_v<std::random_access_iterator_tag, typename std::iterator_traits<It>::iterator_category>;
|
constexpr bool ra = std::is_base_of_v<std::random_access_iterator_tag, typename std::iterator_traits<It>::iterator_category>;
|
||||||
if constexpr (ra)
|
if constexpr (ra) {
|
||||||
if (std::distance(begin, end) % 2 != 0)
|
using std::distance;
|
||||||
|
if (distance(begin, end) % 2 != 0)
|
||||||
return false;
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
size_t count = 0;
|
size_t count = 0;
|
||||||
for (; begin != end; ++begin) {
|
for (; begin != end; ++begin) {
|
||||||
|
@ -129,20 +173,61 @@ constexpr char from_hex_digit(unsigned char x) noexcept {
|
||||||
/// Constructs a byte value from a pair of hex digits
|
/// Constructs a byte value from a pair of hex digits
|
||||||
constexpr char from_hex_pair(unsigned char a, unsigned char b) noexcept { return (from_hex_digit(a) << 4) | from_hex_digit(b); }
|
constexpr char from_hex_pair(unsigned char a, unsigned char b) noexcept { return (from_hex_digit(a) << 4) | from_hex_digit(b); }
|
||||||
|
|
||||||
|
/// Iterable object for on-the-fly hex decoding. Used internally but also particularly useful when
|
||||||
|
/// converting from one encoding to another. Undefined behaviour if the given iterator range is not
|
||||||
|
/// a valid hex string with even length (i.e. is_hex() should return true).
|
||||||
|
template <typename InputIt>
|
||||||
|
struct hex_decoder final {
|
||||||
|
private:
|
||||||
|
InputIt _it, _end;
|
||||||
|
static_assert(sizeof(decltype(*_it)) == 1, "hex_encoder requires chars/bytes input iterator");
|
||||||
|
char byte;
|
||||||
|
public:
|
||||||
|
using iterator_category = std::input_iterator_tag;
|
||||||
|
using difference_type = std::ptrdiff_t;
|
||||||
|
using value_type = char;
|
||||||
|
using reference = value_type;
|
||||||
|
using pointer = void;
|
||||||
|
hex_decoder(InputIt begin, InputIt end) : _it{std::move(begin)}, _end{std::move(end)} {
|
||||||
|
if (_it != _end)
|
||||||
|
load_byte();
|
||||||
|
}
|
||||||
|
|
||||||
|
hex_decoder end() { return {_end, _end}; }
|
||||||
|
|
||||||
|
bool operator==(const hex_decoder& i) { return _it == i._it; }
|
||||||
|
bool operator!=(const hex_decoder& i) { return _it != i._it; }
|
||||||
|
|
||||||
|
hex_decoder& operator++() {
|
||||||
|
if (++_it != _end)
|
||||||
|
load_byte();
|
||||||
|
return *this;
|
||||||
|
}
|
||||||
|
hex_decoder operator++(int) { hex_decoder copy{*this}; ++*this; return copy; }
|
||||||
|
char operator*() const { return byte; }
|
||||||
|
|
||||||
|
private:
|
||||||
|
void load_byte() {
|
||||||
|
auto a = *_it;
|
||||||
|
auto b = *++_it;
|
||||||
|
byte = from_hex_pair(static_cast<unsigned char>(a), static_cast<unsigned char>(b));
|
||||||
|
}
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
/// Converts a sequence of hex digits to bytes. Undefined behaviour if any characters are not in
|
/// Converts a sequence of hex digits to bytes. Undefined behaviour if any characters are not in
|
||||||
/// [0-9a-fA-F] or if the input sequence length is not even: call `is_hex` first if you need to
|
/// [0-9a-fA-F] or if the input sequence length is not even: call `is_hex` first if you need to
|
||||||
/// check. It is permitted for the input and output ranges to overlap as long as out is no earlier
|
/// check. It is permitted for the input and output ranges to overlap as long as out is no later
|
||||||
/// than begin.
|
/// than begin. Returns the final value of out (that is, the iterator positioned just after the
|
||||||
|
/// last written character).
|
||||||
template <typename InputIt, typename OutputIt>
|
template <typename InputIt, typename OutputIt>
|
||||||
void from_hex(InputIt begin, InputIt end, OutputIt out) {
|
OutputIt from_hex(InputIt begin, InputIt end, OutputIt out) {
|
||||||
using std::distance;
|
|
||||||
assert(is_hex(begin, end));
|
assert(is_hex(begin, end));
|
||||||
while (begin != end) {
|
auto it = hex_decoder(begin, end);
|
||||||
auto a = *begin++;
|
const auto hend = it.end();
|
||||||
auto b = *begin++;
|
while (it != hend)
|
||||||
*out++ = static_cast<detail::byte_type_t<OutputIt>>(
|
*out++ = static_cast<detail::byte_type_t<OutputIt>>(*it++);
|
||||||
from_hex_pair(static_cast<unsigned char>(a), static_cast<unsigned char>(b)));
|
return out;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Converts a sequence of hex digits to a string of bytes and returns it. Undefined behaviour if
|
/// Converts a sequence of hex digits to a string of bytes and returns it. Undefined behaviour if
|
||||||
|
@ -150,8 +235,10 @@ void from_hex(InputIt begin, InputIt end, OutputIt out) {
|
||||||
template <typename It>
|
template <typename It>
|
||||||
std::string from_hex(It begin, It end) {
|
std::string from_hex(It begin, It end) {
|
||||||
std::string bytes;
|
std::string bytes;
|
||||||
if constexpr (std::is_base_of_v<std::random_access_iterator_tag, typename std::iterator_traits<It>::iterator_category>)
|
if constexpr (std::is_base_of_v<std::random_access_iterator_tag, typename std::iterator_traits<It>::iterator_category>) {
|
||||||
bytes.reserve(std::distance(begin, end) / 2);
|
using std::distance;
|
||||||
|
bytes.reserve(from_hex_size(distance(begin, end)));
|
||||||
|
}
|
||||||
from_hex(begin, end, std::back_inserter(bytes));
|
from_hex(begin, end, std::back_inserter(bytes));
|
||||||
return bytes;
|
return bytes;
|
||||||
}
|
}
|
||||||
|
|
|
@ -75,6 +75,15 @@ public:
|
||||||
|
|
||||||
explicit DeferredSend(Message& m) : oxenmq{m.oxenmq}, conn{m.conn}, reply_tag{m.reply_tag} {}
|
explicit DeferredSend(Message& m) : oxenmq{m.oxenmq}, conn{m.conn}, reply_tag{m.reply_tag} {}
|
||||||
|
|
||||||
|
template <typename... Args>
|
||||||
|
void operator()(Args &&...args) const {
|
||||||
|
if (reply_tag.empty())
|
||||||
|
back(std::forward<Args>(args)...);
|
||||||
|
else
|
||||||
|
reply(std::forward<Args>(args)...);
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
/// Equivalent to msg.send_back(...), but can be invoked later.
|
/// Equivalent to msg.send_back(...), but can be invoked later.
|
||||||
template <typename... Args>
|
template <typename... Args>
|
||||||
void back(std::string_view command, Args&&... args) const;
|
void back(std::string_view command, Args&&... args) const;
|
||||||
|
|
|
@ -262,7 +262,9 @@ void OxenMQ::start() {
|
||||||
}
|
}
|
||||||
|
|
||||||
void OxenMQ::listen_curve(std::string bind_addr, AllowFunc allow_connection, std::function<void(bool)> on_bind) {
|
void OxenMQ::listen_curve(std::string bind_addr, AllowFunc allow_connection, std::function<void(bool)> on_bind) {
|
||||||
if (!allow_connection) allow_connection = [](auto, auto, auto) { return AuthLevel::none; };
|
if (std::string_view{bind_addr}.substr(0, 9) == "inproc://")
|
||||||
|
throw std::logic_error{"inproc:// cannot be used with listen_curve"};
|
||||||
|
if (!allow_connection) allow_connection = [](auto&&...) { return AuthLevel::none; };
|
||||||
bind_data d{std::move(bind_addr), true, std::move(allow_connection), std::move(on_bind)};
|
bind_data d{std::move(bind_addr), true, std::move(allow_connection), std::move(on_bind)};
|
||||||
if (proxy_thread.joinable())
|
if (proxy_thread.joinable())
|
||||||
detail::send_control(get_control_socket(), "BIND", bt_serialize(detail::serialize_object(std::move(d))));
|
detail::send_control(get_control_socket(), "BIND", bt_serialize(detail::serialize_object(std::move(d))));
|
||||||
|
@ -271,7 +273,9 @@ void OxenMQ::listen_curve(std::string bind_addr, AllowFunc allow_connection, std
|
||||||
}
|
}
|
||||||
|
|
||||||
void OxenMQ::listen_plain(std::string bind_addr, AllowFunc allow_connection, std::function<void(bool)> on_bind) {
|
void OxenMQ::listen_plain(std::string bind_addr, AllowFunc allow_connection, std::function<void(bool)> on_bind) {
|
||||||
if (!allow_connection) allow_connection = [](auto, auto, auto) { return AuthLevel::none; };
|
if (std::string_view{bind_addr}.substr(0, 9) == "inproc://")
|
||||||
|
throw std::logic_error{"inproc:// cannot be used with listen_plain"};
|
||||||
|
if (!allow_connection) allow_connection = [](auto&&...) { return AuthLevel::none; };
|
||||||
bind_data d{std::move(bind_addr), false, std::move(allow_connection), std::move(on_bind)};
|
bind_data d{std::move(bind_addr), false, std::move(allow_connection), std::move(on_bind)};
|
||||||
if (proxy_thread.joinable())
|
if (proxy_thread.joinable())
|
||||||
detail::send_control(get_control_socket(), "BIND", bt_serialize(detail::serialize_object(std::move(d))));
|
detail::send_control(get_control_socket(), "BIND", bt_serialize(detail::serialize_object(std::move(d))));
|
||||||
|
|
|
@ -82,6 +82,9 @@ inline constexpr auto DEFAULT_CONNECT_SN_KEEP_ALIVE = 5min;
|
||||||
// The default timeout for connect_remote()
|
// The default timeout for connect_remote()
|
||||||
inline constexpr auto REMOTE_CONNECT_TIMEOUT = 10s;
|
inline constexpr auto REMOTE_CONNECT_TIMEOUT = 10s;
|
||||||
|
|
||||||
|
// Default timeout for connect_inproc()
|
||||||
|
inline constexpr auto INPROC_CONNECT_TIMEOUT = 50ms;
|
||||||
|
|
||||||
// The amount of time we wait for a reply to a REQUEST before calling the callback with
|
// The amount of time we wait for a reply to a REQUEST before calling the callback with
|
||||||
// `false` to signal a timeout.
|
// `false` to signal a timeout.
|
||||||
inline constexpr auto DEFAULT_REQUEST_TIMEOUT = 15s;
|
inline constexpr auto DEFAULT_REQUEST_TIMEOUT = 15s;
|
||||||
|
@ -410,6 +413,9 @@ private:
|
||||||
/// The connections to/from remotes we currently have open, both listening and outgoing.
|
/// The connections to/from remotes we currently have open, both listening and outgoing.
|
||||||
std::map<int64_t, zmq::socket_t> connections;
|
std::map<int64_t, zmq::socket_t> connections;
|
||||||
|
|
||||||
|
/// The connection ID of the built-in inproc listener for making requests to self
|
||||||
|
int64_t inproc_listener_connid;
|
||||||
|
|
||||||
/// If set then it indicates a change in `connections` which means we need to rebuild pollitems
|
/// If set then it indicates a change in `connections` which means we need to rebuild pollitems
|
||||||
/// and stop using existing connections iterators.
|
/// and stop using existing connections iterators.
|
||||||
bool connections_updated = true;
|
bool connections_updated = true;
|
||||||
|
@ -442,7 +448,7 @@ private:
|
||||||
/// indices of idle, active workers
|
/// indices of idle, active workers
|
||||||
std::vector<unsigned int> idle_workers;
|
std::vector<unsigned int> idle_workers;
|
||||||
|
|
||||||
/// Maximum number of general task workers, specified by g`/during construction
|
/// Maximum number of general task workers, specified by set_general_threads()
|
||||||
int general_workers = std::max<int>(1, std::thread::hardware_concurrency());
|
int general_workers = std::max<int>(1, std::thread::hardware_concurrency());
|
||||||
|
|
||||||
/// Maximum number of possible worker threads we can have. This is calculated when starting,
|
/// Maximum number of possible worker threads we can have. This is calculated when starting,
|
||||||
|
@ -780,13 +786,6 @@ public:
|
||||||
* listening in curve25519 mode (otherwise we couldn't verify its authenticity). Should return
|
* listening in curve25519 mode (otherwise we couldn't verify its authenticity). Should return
|
||||||
* empty for not found or if SN lookups are not supported.
|
* empty for not found or if SN lookups are not supported.
|
||||||
*
|
*
|
||||||
* @param allow_incoming is a callback that OxenMQ can use to determine whether an incoming
|
|
||||||
* connection should be allowed at all and, if so, whether the connection is from a known
|
|
||||||
* service node. Called with the connecting IP, the remote's verified x25519 pubkey, and the
|
|
||||||
* called on incoming connections with the (verified) incoming connection
|
|
||||||
* pubkey (32-byte binary string) to determine whether the given SN should be allowed to
|
|
||||||
* connect.
|
|
||||||
*
|
|
||||||
* @param log a function or callable object that writes a log message. If omitted then all log
|
* @param log a function or callable object that writes a log message. If omitted then all log
|
||||||
* messages are suppressed.
|
* messages are suppressed.
|
||||||
*
|
*
|
||||||
|
@ -1000,7 +999,7 @@ public:
|
||||||
* connections on this address to determine the incoming remote's access and authentication
|
* connections on this address to determine the incoming remote's access and authentication
|
||||||
* level. Note that `allow_connection` here will be called with an empty pubkey.
|
* level. Note that `allow_connection` here will be called with an empty pubkey.
|
||||||
*
|
*
|
||||||
* @param bind address - can be any string zmq supports; typically a tcp IP/port combination
|
* @param bind address - can be any string zmq supports, for example a tcp IP/port combination
|
||||||
* such as: "tcp://\*:4567" or "tcp://1.2.3.4:5678".
|
* such as: "tcp://\*:4567" or "tcp://1.2.3.4:5678".
|
||||||
*
|
*
|
||||||
* @param allow_connection function to call to determine whether to allow the connection and, if
|
* @param allow_connection function to call to determine whether to allow the connection and, if
|
||||||
|
@ -1027,7 +1026,7 @@ public:
|
||||||
* @param pubkey - the public key (32-byte binary string) of the service node to connect to
|
* @param pubkey - the public key (32-byte binary string) of the service node to connect to
|
||||||
* @param options - connection options; see the structs in `connect_option`, in particular:
|
* @param options - connection options; see the structs in `connect_option`, in particular:
|
||||||
* - keep_alive -- how long the SN connection will be kept alive after valid activity
|
* - keep_alive -- how long the SN connection will be kept alive after valid activity
|
||||||
* - remote_hint -- a remote address hint that may be used instead of doing a lookup
|
* - hint -- a remote address hint that may be used instead of doing a lookup
|
||||||
* - ephemeral_routing_id -- allows you to override the EPHEMERAL_ROUTING_ID option for
|
* - ephemeral_routing_id -- allows you to override the EPHEMERAL_ROUTING_ID option for
|
||||||
* this connection.
|
* this connection.
|
||||||
*
|
*
|
||||||
|
@ -1095,6 +1094,16 @@ public:
|
||||||
AuthLevel auth_level = AuthLevel::none,
|
AuthLevel auth_level = AuthLevel::none,
|
||||||
std::chrono::milliseconds timeout = REMOTE_CONNECT_TIMEOUT);
|
std::chrono::milliseconds timeout = REMOTE_CONNECT_TIMEOUT);
|
||||||
|
|
||||||
|
/// Connects to the built-in in-process listening socket of this OxenMQ server for local
|
||||||
|
/// communication. Note that auth_level defaults to admin (unlike connect_remote), and the
|
||||||
|
/// default timeout is much shorter.
|
||||||
|
///
|
||||||
|
/// Also note that incoming inproc requests are unauthenticated: that is, they will always have
|
||||||
|
/// admin-level access.
|
||||||
|
template <typename... Option>
|
||||||
|
ConnectionID connect_inproc(ConnectSuccess on_connect, ConnectFailure on_failure,
|
||||||
|
const Option&... options);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Disconnects an established outgoing connection established with `connect_remote()` (or, less
|
* Disconnects an established outgoing connection established with `connect_remote()` (or, less
|
||||||
* commonly, `connect_sn()`).
|
* commonly, `connect_sn()`).
|
||||||
|
@ -1103,8 +1112,7 @@ public:
|
||||||
*
|
*
|
||||||
* @param linger how long to allow the connection to linger while there are still pending
|
* @param linger how long to allow the connection to linger while there are still pending
|
||||||
* outbound messages to it before disconnecting and dropping any pending messages. (Note that
|
* outbound messages to it before disconnecting and dropping any pending messages. (Note that
|
||||||
* this lingering is internal; the disconnect_remote() call does not block). The default is 1
|
* this lingering is internal; the disconnect() call does not block). The default is 1 second.
|
||||||
* second.
|
|
||||||
*
|
*
|
||||||
* If given a pubkey, we try to close an outgoing connection to the given SN if one exists; note
|
* If given a pubkey, we try to close an outgoing connection to the given SN if one exists; note
|
||||||
* however that this is often not particularly useful as messages to that SN can immediately
|
* however that this is often not particularly useful as messages to that SN can immediately
|
||||||
|
@ -1118,8 +1126,8 @@ public:
|
||||||
* if not already connected).
|
* if not already connected).
|
||||||
*
|
*
|
||||||
* If a new connection is established it will have a relatively short (30s) idle timeout. If
|
* If a new connection is established it will have a relatively short (30s) idle timeout. If
|
||||||
* the connection should stay open longer you should either call `connect(pubkey, IDLETIME)` or
|
* the connection should stay open longer you should either call `connect_sn(pubkey, IDLETIME)`
|
||||||
* pass a a `send_option::keep_alive{IDLETIME}` in `opts`.
|
* or pass a a `send_option::keep_alive{IDLETIME}` in `opts`.
|
||||||
*
|
*
|
||||||
* Note that this method (along with connect) doesn't block waiting for a connection or for the
|
* Note that this method (along with connect) doesn't block waiting for a connection or for the
|
||||||
* message to send; it merely instructs the proxy thread that it should send. ZMQ will
|
* message to send; it merely instructs the proxy thread that it should send. ZMQ will
|
||||||
|
@ -1273,9 +1281,9 @@ public:
|
||||||
/**
|
/**
|
||||||
* Adds a timer that gets scheduled periodically in the job queue. Normally jobs are not
|
* Adds a timer that gets scheduled periodically in the job queue. Normally jobs are not
|
||||||
* double-booked: that is, a new timed job will not be scheduled if the timer fires before a
|
* double-booked: that is, a new timed job will not be scheduled if the timer fires before a
|
||||||
* previously scheduled callback of the job has not yet completed. If you want to override this
|
* previously scheduled callback of the job has completed. If you want to override this (so
|
||||||
* (so that, under heavy load or long jobs, there can be more than one of the same job scheduled
|
* that, under heavy load or long jobs, there can be more than one of the same job scheduled or
|
||||||
* or running at a time) then specify `squelch` as `false`.
|
* running at a time) then specify `squelch` as `false`.
|
||||||
*
|
*
|
||||||
* The returned value can be kept and later passed into `cancel_timer()` if you want to be able
|
* The returned value can be kept and later passed into `cancel_timer()` if you want to be able
|
||||||
* to cancel a timer.
|
* to cancel a timer.
|
||||||
|
@ -1411,6 +1419,8 @@ struct outgoing {
|
||||||
|
|
||||||
/// Specifies the idle timeout for the connection - if a new or existing outgoing connection is used
|
/// Specifies the idle timeout for the connection - if a new or existing outgoing connection is used
|
||||||
/// for the send and its current idle timeout setting is less than this value then it is updated.
|
/// for the send and its current idle timeout setting is less than this value then it is updated.
|
||||||
|
///
|
||||||
|
/// A negative value is treated as if the option were not supplied at all.
|
||||||
struct keep_alive {
|
struct keep_alive {
|
||||||
std::chrono::milliseconds time;
|
std::chrono::milliseconds time;
|
||||||
explicit keep_alive(std::chrono::milliseconds time) : time{std::move(time)} {}
|
explicit keep_alive(std::chrono::milliseconds time) : time{std::move(time)} {}
|
||||||
|
@ -1421,6 +1431,8 @@ struct keep_alive {
|
||||||
/// (This has no effect if specified on a non-request() call). Note that requests failures are only
|
/// (This has no effect if specified on a non-request() call). Note that requests failures are only
|
||||||
/// processed in the CONN_CHECK_INTERVAL timer, so it can be up to that much longer than the time
|
/// processed in the CONN_CHECK_INTERVAL timer, so it can be up to that much longer than the time
|
||||||
/// specified here before a failure callback is invoked.
|
/// specified here before a failure callback is invoked.
|
||||||
|
///
|
||||||
|
/// Specifying a negative timeout is equivalent to not specifying the option at all.
|
||||||
struct request_timeout {
|
struct request_timeout {
|
||||||
std::chrono::milliseconds time;
|
std::chrono::milliseconds time;
|
||||||
explicit request_timeout(std::chrono::milliseconds time) : time{std::move(time)} {}
|
explicit request_timeout(std::chrono::milliseconds time) : time{std::move(time)} {}
|
||||||
|
@ -1466,7 +1478,7 @@ namespace connect_option {
|
||||||
/// the default (OxenMQ::EPHEMERAL_ROUTING_ID). See OxenMQ::EPHEMERAL_ROUTING_ID for a description
|
/// the default (OxenMQ::EPHEMERAL_ROUTING_ID). See OxenMQ::EPHEMERAL_ROUTING_ID for a description
|
||||||
/// of this.
|
/// of this.
|
||||||
///
|
///
|
||||||
/// Typically use: `connect_options::ephemeral_routing_id{}` or `connect_options::ephemeral_routing_id{false}`.
|
/// Typically use: `connect_option::ephemeral_routing_id{}` or `connect_option::ephemeral_routing_id{false}`.
|
||||||
struct ephemeral_routing_id {
|
struct ephemeral_routing_id {
|
||||||
bool use_ephemeral_routing_id = true;
|
bool use_ephemeral_routing_id = true;
|
||||||
// Constructor; default construction gives you ephemeral routing id, but the bool parameter can
|
// Constructor; default construction gives you ephemeral routing id, but the bool parameter can
|
||||||
|
@ -1486,6 +1498,8 @@ struct timeout {
|
||||||
/// milliseconds. If an outgoing connection already exists, the longer of the existing and the
|
/// milliseconds. If an outgoing connection already exists, the longer of the existing and the
|
||||||
/// given keep alive is used.
|
/// given keep alive is used.
|
||||||
///
|
///
|
||||||
|
/// A negative value is treated as if the keep_alive option had not been specified.
|
||||||
|
///
|
||||||
/// Note that, if not specified, the default keep-alive for a connection established via
|
/// Note that, if not specified, the default keep-alive for a connection established via
|
||||||
/// connect_sn() is 5 minutes (which is much longer than the default for an implicit connect() by
|
/// connect_sn() is 5 minutes (which is much longer than the default for an implicit connect() by
|
||||||
/// calling send() directly with a pubkey.)
|
/// calling send() directly with a pubkey.)
|
||||||
|
@ -1501,6 +1515,7 @@ struct keep_alive {
|
||||||
/// potentially expensive lookup call).
|
/// potentially expensive lookup call).
|
||||||
struct hint {
|
struct hint {
|
||||||
std::string address;
|
std::string address;
|
||||||
|
// Constructor taking a hint. If the hint is an empty string then no hint will be used.
|
||||||
explicit hint(std::string_view address) : address{address} {}
|
explicit hint(std::string_view address) : address{address} {}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -1554,6 +1569,7 @@ void apply_send_option(bt_list& parts, bt_dict&, const send_option::data_parts_i
|
||||||
|
|
||||||
/// `hint` specialization: sets the hint in the control data
|
/// `hint` specialization: sets the hint in the control data
|
||||||
inline void apply_send_option(bt_list&, bt_dict& control_data, const send_option::hint& hint) {
|
inline void apply_send_option(bt_list&, bt_dict& control_data, const send_option::hint& hint) {
|
||||||
|
if (hint.connect_hint.empty()) return;
|
||||||
control_data["hint"] = hint.connect_hint;
|
control_data["hint"] = hint.connect_hint;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1574,12 +1590,14 @@ inline void apply_send_option(bt_list&, bt_dict& control_data, const send_option
|
||||||
|
|
||||||
/// `keep_alive` specialization: increases the outgoing socket idle timeout (if shorter)
|
/// `keep_alive` specialization: increases the outgoing socket idle timeout (if shorter)
|
||||||
inline void apply_send_option(bt_list&, bt_dict& control_data, const send_option::keep_alive& timeout) {
|
inline void apply_send_option(bt_list&, bt_dict& control_data, const send_option::keep_alive& timeout) {
|
||||||
control_data["keep_alive"] = timeout.time.count();
|
if (timeout.time >= 0ms)
|
||||||
|
control_data["keep_alive"] = timeout.time.count();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// `request_timeout` specialization: set the timeout time for a request
|
/// `request_timeout` specialization: set the timeout time for a request
|
||||||
inline void apply_send_option(bt_list&, bt_dict& control_data, const send_option::request_timeout& timeout) {
|
inline void apply_send_option(bt_list&, bt_dict& control_data, const send_option::request_timeout& timeout) {
|
||||||
control_data["request_timeout"] = timeout.time.count();
|
if (timeout.time >= 0ms)
|
||||||
|
control_data["request_timeout"] = timeout.time.count();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// `queue_failure` specialization
|
/// `queue_failure` specialization
|
||||||
|
@ -1624,10 +1642,12 @@ inline void apply_connect_option(OxenMQ& omq, bool remote, bt_dict& opts, const
|
||||||
else omq.log(LogLevel::warn, __FILE__, __LINE__, "connect_option::timeout ignored for connect_sn(...)");
|
else omq.log(LogLevel::warn, __FILE__, __LINE__, "connect_option::timeout ignored for connect_sn(...)");
|
||||||
}
|
}
|
||||||
inline void apply_connect_option(OxenMQ& omq, bool remote, bt_dict& opts, const connect_option::keep_alive& ka) {
|
inline void apply_connect_option(OxenMQ& omq, bool remote, bt_dict& opts, const connect_option::keep_alive& ka) {
|
||||||
if (!remote) opts["keep_alive"] = ka.time.count();
|
if (ka.time < 0ms) return;
|
||||||
|
else if (!remote) opts["keep_alive"] = ka.time.count();
|
||||||
else omq.log(LogLevel::warn, __FILE__, __LINE__, "connect_option::keep_alive ignored for connect_remote(...)");
|
else omq.log(LogLevel::warn, __FILE__, __LINE__, "connect_option::keep_alive ignored for connect_remote(...)");
|
||||||
}
|
}
|
||||||
inline void apply_connect_option(OxenMQ& omq, bool remote, bt_dict& opts, const connect_option::hint& hint) {
|
inline void apply_connect_option(OxenMQ& omq, bool remote, bt_dict& opts, const connect_option::hint& hint) {
|
||||||
|
if (hint.address.empty()) return;
|
||||||
if (!remote) opts["hint"] = hint.address;
|
if (!remote) opts["hint"] = hint.address;
|
||||||
else omq.log(LogLevel::warn, __FILE__, __LINE__, "connect_option::hint ignored for connect_remote(...)");
|
else omq.log(LogLevel::warn, __FILE__, __LINE__, "connect_option::hint ignored for connect_remote(...)");
|
||||||
}
|
}
|
||||||
|
@ -1678,6 +1698,27 @@ ConnectionID OxenMQ::connect_sn(std::string_view pubkey, const Option&... option
|
||||||
return pubkey;
|
return pubkey;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
template <typename... Option>
|
||||||
|
ConnectionID OxenMQ::connect_inproc(ConnectSuccess on_connect, ConnectFailure on_failure,
|
||||||
|
const Option&... options) {
|
||||||
|
bt_dict opts{
|
||||||
|
{"timeout", INPROC_CONNECT_TIMEOUT.count()},
|
||||||
|
{"auth_level", static_cast<std::underlying_type_t<AuthLevel>>(AuthLevel::admin)}
|
||||||
|
};
|
||||||
|
|
||||||
|
(detail::apply_connect_option(*this, true, opts, options), ...);
|
||||||
|
|
||||||
|
auto id = next_conn_id++;
|
||||||
|
opts["conn_id"] = id;
|
||||||
|
opts["connect"] = detail::serialize_object(std::move(on_connect));
|
||||||
|
opts["failure"] = detail::serialize_object(std::move(on_failure));
|
||||||
|
opts["remote"] = "inproc://sn-self";
|
||||||
|
|
||||||
|
detail::send_control(get_control_socket(), "CONNECT_REMOTE", bt_serialize(opts));
|
||||||
|
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
template <typename... T>
|
template <typename... T>
|
||||||
void OxenMQ::send(ConnectionID to, std::string_view cmd, const T&... opts) {
|
void OxenMQ::send(ConnectionID to, std::string_view cmd, const T&... opts) {
|
||||||
detail::send_control(get_control_socket(), "SEND",
|
detail::send_control(get_control_socket(), "SEND",
|
||||||
|
|
|
@ -411,6 +411,13 @@ void OxenMQ::proxy_loop() {
|
||||||
saved_umask = umask(STARTUP_UMASK);
|
saved_umask = umask(STARTUP_UMASK);
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
{
|
||||||
|
zmq::socket_t inproc_listener{context, zmq::socket_type::router};
|
||||||
|
inproc_listener.bind(SN_ADDR_SELF);
|
||||||
|
inproc_listener_connid = next_conn_id++;
|
||||||
|
connections.emplace_hint(connections.end(), inproc_listener_connid, std::move(inproc_listener));
|
||||||
|
}
|
||||||
|
|
||||||
for (size_t i = 0; i < bind.size(); i++) {
|
for (size_t i = 0; i < bind.size(); i++) {
|
||||||
if (!proxy_bind(bind[i], i)) {
|
if (!proxy_bind(bind[i], i)) {
|
||||||
LMQ_LOG(warn, "OxenMQ failed to listen on ", bind[i].address);
|
LMQ_LOG(warn, "OxenMQ failed to listen on ", bind[i].address);
|
||||||
|
|
|
@ -19,7 +19,7 @@ namespace {
|
||||||
[[gnu::always_inline]] inline
|
[[gnu::always_inline]] inline
|
||||||
bool worker_wait_for(OxenMQ& lmq, zmq::socket_t& sock, std::vector<zmq::message_t>& parts, const std::string_view worker_id, const std::string_view expect) {
|
bool worker_wait_for(OxenMQ& lmq, zmq::socket_t& sock, std::vector<zmq::message_t>& parts, const std::string_view worker_id, const std::string_view expect) {
|
||||||
while (true) {
|
while (true) {
|
||||||
lmq.log(LogLevel::debug, __FILE__, __LINE__, "worker ", worker_id, " waiting for ", expect);
|
lmq.log(LogLevel::trace, __FILE__, __LINE__, "worker ", worker_id, " waiting for ", expect);
|
||||||
parts.clear();
|
parts.clear();
|
||||||
recv_message_parts(sock, parts);
|
recv_message_parts(sock, parts);
|
||||||
if (parts.size() != 1) {
|
if (parts.size() != 1) {
|
||||||
|
@ -293,6 +293,11 @@ void OxenMQ::proxy_to_worker(int64_t conn_id, zmq::socket_t& sock, std::vector<z
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
peer = &it->second;
|
peer = &it->second;
|
||||||
|
} else if (conn_id == inproc_listener_connid) {
|
||||||
|
tmp_peer.auth_level = AuthLevel::admin;
|
||||||
|
tmp_peer.pubkey = pubkey;
|
||||||
|
tmp_peer.service_node = active_service_nodes.count(pubkey);
|
||||||
|
peer = &tmp_peer;
|
||||||
} else {
|
} else {
|
||||||
std::tie(tmp_peer.pubkey, tmp_peer.auth_level) = detail::extract_metadata(parts.back());
|
std::tie(tmp_peer.pubkey, tmp_peer.auth_level) = detail::extract_metadata(parts.back());
|
||||||
tmp_peer.service_node = tmp_peer.pubkey.size() == 32 && active_service_nodes.count(tmp_peer.pubkey);
|
tmp_peer.service_node = tmp_peer.pubkey.size() == 32 && active_service_nodes.count(tmp_peer.pubkey);
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
Subproject commit b3b07215d1ca2224aea6ff3e21d87ad0f7750df2
|
Subproject commit dba29b60d639bf8d206a9a12c223e6ed4284fb13
|
|
@ -5,12 +5,22 @@
|
||||||
|
|
||||||
using namespace oxenmq;
|
using namespace oxenmq;
|
||||||
|
|
||||||
|
// Apple's mutexes, thread scheduling, and IO handling are garbage and it shows up with lots of
|
||||||
|
// spurious failures in this test suite (because it expects a system to not suck that badly), so we
|
||||||
|
// multiply the time-sensitive bits by this factor as a hack to make the test suite work.
|
||||||
|
constexpr int TIME_DILATION =
|
||||||
|
#ifdef __APPLE__
|
||||||
|
5;
|
||||||
|
#else
|
||||||
|
1;
|
||||||
|
#endif
|
||||||
|
|
||||||
static auto startup = std::chrono::steady_clock::now();
|
static auto startup = std::chrono::steady_clock::now();
|
||||||
|
|
||||||
/// Returns a localhost connection string to listen on. It can be considered random, though in
|
/// Returns a localhost connection string to listen on. It can be considered random, though in
|
||||||
/// practice in the current implementation is sequential starting at 4500.
|
/// practice in the current implementation is sequential starting at 25432.
|
||||||
inline std::string random_localhost() {
|
inline std::string random_localhost() {
|
||||||
static uint16_t last = 4499;
|
static std::atomic<uint16_t> last = 25432;
|
||||||
last++;
|
last++;
|
||||||
assert(last); // We should never call this enough to overflow
|
assert(last); // We should never call this enough to overflow
|
||||||
return "tcp://127.0.0.1:" + std::to_string(last);
|
return "tcp://127.0.0.1:" + std::to_string(last);
|
||||||
|
@ -30,7 +40,7 @@ inline void wait_for(Func f) {
|
||||||
for (int i = 0; i < 20; i++) {
|
for (int i = 0; i < 20; i++) {
|
||||||
if (f())
|
if (f())
|
||||||
break;
|
break;
|
||||||
std::this_thread::sleep_for(10ms);
|
std::this_thread::sleep_for(10ms * TIME_DILATION);
|
||||||
}
|
}
|
||||||
auto lock = catch_lock();
|
auto lock = catch_lock();
|
||||||
UNSCOPED_INFO("done waiting after " << (std::chrono::steady_clock::now() - start).count() << "ns");
|
UNSCOPED_INFO("done waiting after " << (std::chrono::steady_clock::now() - start).count() << "ns");
|
||||||
|
@ -43,7 +53,7 @@ inline void wait_for_conn(std::atomic<bool> &c) {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Waits enough time for us to receive a reply from a localhost remote.
|
/// Waits enough time for us to receive a reply from a localhost remote.
|
||||||
inline void reply_sleep() { std::this_thread::sleep_for(10ms); }
|
inline void reply_sleep() { std::this_thread::sleep_for(10ms * TIME_DILATION); }
|
||||||
|
|
||||||
inline OxenMQ::Logger get_logger(std::string prefix = "") {
|
inline OxenMQ::Logger get_logger(std::string prefix = "") {
|
||||||
std::string me = "tests/common.h";
|
std::string me = "tests/common.h";
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
#include "oxenmq/bt_serialize.h"
|
#include "oxenmq/bt_serialize.h"
|
||||||
|
#include "oxenmq/bt_producer.h"
|
||||||
#include "common.h"
|
#include "common.h"
|
||||||
#include <map>
|
#include <map>
|
||||||
#include <set>
|
#include <set>
|
||||||
|
@ -210,6 +211,10 @@ TEST_CASE("bt tuple serialization", "[bt][tuple][serialization]") {
|
||||||
bt_list m{{1, 2, std::make_tuple(3, 4, "hi"sv), std::make_pair("foo"s, "bar"sv), -4}};
|
bt_list m{{1, 2, std::make_tuple(3, 4, "hi"sv), std::make_pair("foo"s, "bar"sv), -4}};
|
||||||
REQUIRE( bt_serialize(m) == "li1ei2eli3ei4e2:hiel3:foo3:barei-4ee" );
|
REQUIRE( bt_serialize(m) == "li1ei2eli3ei4e2:hiel3:foo3:barei-4ee" );
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("bt allocation-free consumer", "[bt][dict][list][consumer]") {
|
||||||
|
|
||||||
// Consumer deserialization:
|
// Consumer deserialization:
|
||||||
bt_list_consumer lc{"li1ei2eli3ei4e2:hiel3:foo3:barei-4ee"};
|
bt_list_consumer lc{"li1ei2eli3ei4e2:hiel3:foo3:barei-4ee"};
|
||||||
REQUIRE( lc.consume_integer<int>() == 1 );
|
REQUIRE( lc.consume_integer<int>() == 1 );
|
||||||
|
@ -227,31 +232,91 @@ TEST_CASE("bt tuple serialization", "[bt][tuple][serialization]") {
|
||||||
std::make_pair("b"sv, std::make_tuple(1, 2, 3)) );
|
std::make_pair("b"sv, std::make_tuple(1, 2, 3)) );
|
||||||
}
|
}
|
||||||
|
|
||||||
#if 0
|
TEST_CASE("bt allocation-free producer", "[bt][dict][list][producer]") {
|
||||||
|
|
||||||
|
char smallbuf[16];
|
||||||
|
bt_list_producer toosmall{smallbuf, 16}; // le, total = 2
|
||||||
|
toosmall += 42; // i42e, total = 6
|
||||||
|
toosmall += "abcdefgh"; // 8:abcdefgh, total=16
|
||||||
|
CHECK( toosmall.view() == "li42e8:abcdefghe" );
|
||||||
|
|
||||||
|
CHECK_THROWS_AS( toosmall += "", std::length_error );
|
||||||
|
|
||||||
|
char buf[1024];
|
||||||
|
bt_list_producer lp{buf, sizeof(buf)};
|
||||||
|
CHECK( lp.view() == "le" );
|
||||||
|
CHECK( (void*) lp.end() == (void*) (buf + 2) );
|
||||||
|
|
||||||
|
lp.append("abc");
|
||||||
|
CHECK( lp.view() == "l3:abce" );
|
||||||
|
lp += 42;
|
||||||
|
CHECK( lp.view() == "l3:abci42ee" );
|
||||||
|
std::vector<int> randos = {{1, 17, -999}};
|
||||||
|
lp.append(randos.begin(), randos.end());
|
||||||
|
CHECK( lp.view() == "l3:abci42ei1ei17ei-999ee" );
|
||||||
|
|
||||||
{
|
{
|
||||||
std::cout << "zomg consumption\n";
|
auto sublist = lp.append_list();
|
||||||
bt_dict_consumer dc{zomg_};
|
CHECK_THROWS_AS( lp.append(1), std::logic_error );
|
||||||
for (int i = 0; i < 5; i++)
|
CHECK( sublist.view() == "le" );
|
||||||
if (!dc.skip_until("b"))
|
CHECK( lp.view() == "l3:abci42ei1ei17ei-999elee" );
|
||||||
throw std::runtime_error("Couldn't find b, but I know it's there!");
|
sublist.append(0);
|
||||||
|
|
||||||
auto dc1 = dc;
|
auto sublist2{std::move(sublist)};
|
||||||
if (dc.skip_until("z")) {
|
sublist2 += "";
|
||||||
auto v = dc.consume_integer<int>();
|
CHECK( sublist2.view() == "li0e0:e" );
|
||||||
std::cout << " - " << v.first << ": " << v.second << "\n";
|
CHECK( lp.view() == "l3:abci42ei1ei17ei-999eli0e0:ee" );
|
||||||
} else {
|
|
||||||
std::cout << " - no z (bad!)\n";
|
|
||||||
}
|
|
||||||
|
|
||||||
std::cout << "zomg (second pass)\n";
|
|
||||||
for (auto &p : dc1.consume_dict().second) {
|
|
||||||
std::cout << " - " << p.first << " = (whatever)\n";
|
|
||||||
}
|
|
||||||
while (dc1) {
|
|
||||||
auto v = dc1.consume_integer<int>();
|
|
||||||
std::cout << " - " << v.first << ": " << v.second << "\n";
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
lp.append_list().append_list().append_list() += "omg"s;
|
||||||
|
CHECK( lp.view() == "l3:abci42ei1ei17ei-999eli0e0:elll3:omgeeee" );
|
||||||
|
|
||||||
|
{
|
||||||
|
auto dict = lp.append_dict();
|
||||||
|
CHECK( dict.view() == "de" );
|
||||||
|
CHECK( lp.view() == "l3:abci42ei1ei17ei-999eli0e0:elll3:omgeeedee" );
|
||||||
|
|
||||||
|
CHECK_THROWS_AS( lp.append(1), std::logic_error );
|
||||||
|
|
||||||
|
dict.append("foo", "bar");
|
||||||
|
dict.append("g", 42);
|
||||||
|
|
||||||
|
CHECK( dict.view() == "d3:foo3:bar1:gi42ee" );
|
||||||
|
CHECK( lp.view() == "l3:abci42ei1ei17ei-999eli0e0:elll3:omgeeed3:foo3:bar1:gi42eee" );
|
||||||
|
|
||||||
|
dict.append_list("h").append_dict().append_dict("a").append_list("A") += 999;
|
||||||
|
CHECK( dict.view() == "d3:foo3:bar1:gi42e1:hld1:ad1:Ali999eeeeee" );
|
||||||
|
CHECK( lp.view() == "l3:abci42ei1ei17ei-999eli0e0:elll3:omgeeed3:foo3:bar1:gi42e1:hld1:ad1:Ali999eeeeeee" );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#ifdef OXENMQ_APPLE_TO_CHARS_WORKAROUND
|
||||||
|
TEST_CASE("apple to_chars workaround test", "[bt][apple][sucks]") {
|
||||||
|
char buf[20];
|
||||||
|
auto buf_view = [&](char* end) { return std::string_view{buf, static_cast<size_t>(end - buf)}; };
|
||||||
|
CHECK( buf_view(oxenmq::apple_to_chars10(buf, 0)) == "0" );
|
||||||
|
CHECK( buf_view(oxenmq::apple_to_chars10(buf, 1)) == "1" );
|
||||||
|
CHECK( buf_view(oxenmq::apple_to_chars10(buf, 2)) == "2" );
|
||||||
|
CHECK( buf_view(oxenmq::apple_to_chars10(buf, 10)) == "10" );
|
||||||
|
CHECK( buf_view(oxenmq::apple_to_chars10(buf, 42)) == "42" );
|
||||||
|
CHECK( buf_view(oxenmq::apple_to_chars10(buf, 99)) == "99" );
|
||||||
|
CHECK( buf_view(oxenmq::apple_to_chars10(buf, 1234567890)) == "1234567890" );
|
||||||
|
CHECK( buf_view(oxenmq::apple_to_chars10(buf, -1)) == "-1" );
|
||||||
|
CHECK( buf_view(oxenmq::apple_to_chars10(buf, -2)) == "-2" );
|
||||||
|
CHECK( buf_view(oxenmq::apple_to_chars10(buf, -10)) == "-10" );
|
||||||
|
CHECK( buf_view(oxenmq::apple_to_chars10(buf, -99)) == "-99" );
|
||||||
|
CHECK( buf_view(oxenmq::apple_to_chars10(buf, -1234567890)) == "-1234567890" );
|
||||||
|
CHECK( buf_view(oxenmq::apple_to_chars10(buf, char{42})) == "42" );
|
||||||
|
CHECK( buf_view(oxenmq::apple_to_chars10(buf, (unsigned char){42})) == "42" );
|
||||||
|
CHECK( buf_view(oxenmq::apple_to_chars10(buf, short{42})) == "42" );
|
||||||
|
CHECK( buf_view(oxenmq::apple_to_chars10(buf, std::numeric_limits<char>::min())) == "-128" );
|
||||||
|
CHECK( buf_view(oxenmq::apple_to_chars10(buf, std::numeric_limits<char>::max())) == "127" );
|
||||||
|
CHECK( buf_view(oxenmq::apple_to_chars10(buf, (unsigned char){42})) == "42" );
|
||||||
|
CHECK( buf_view(oxenmq::apple_to_chars10(buf, std::numeric_limits<uint64_t>::max())) == "18446744073709551615" );
|
||||||
|
CHECK( buf_view(oxenmq::apple_to_chars10(buf, int64_t{-1})) == "-1" );
|
||||||
|
CHECK( buf_view(oxenmq::apple_to_chars10(buf, std::numeric_limits<int64_t>::min())) == "-9223372036854775808" );
|
||||||
|
CHECK( buf_view(oxenmq::apple_to_chars10(buf, int64_t{-9223372036854775807})) == "-9223372036854775807" );
|
||||||
|
CHECK( buf_view(oxenmq::apple_to_chars10(buf, int64_t{9223372036854775807})) == "9223372036854775807" );
|
||||||
|
CHECK( buf_view(oxenmq::apple_to_chars10(buf, int64_t{9223372036854775806})) == "9223372036854775806" );
|
||||||
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -470,9 +470,9 @@ TEST_CASE("deferred replies", "[commands][send][deferred]") {
|
||||||
std::string msg = m.data.empty() ? ""s : std::string{m.data.front()};
|
std::string msg = m.data.empty() ? ""s : std::string{m.data.front()};
|
||||||
std::thread t{[send=m.send_later(), msg=std::move(msg)] {
|
std::thread t{[send=m.send_later(), msg=std::move(msg)] {
|
||||||
{ auto lock = catch_lock(); UNSCOPED_INFO("sleeping"); }
|
{ auto lock = catch_lock(); UNSCOPED_INFO("sleeping"); }
|
||||||
std::this_thread::sleep_for(50ms);
|
std::this_thread::sleep_for(50ms * TIME_DILATION);
|
||||||
{ auto lock = catch_lock(); UNSCOPED_INFO("sending"); }
|
{ auto lock = catch_lock(); UNSCOPED_INFO("sending"); }
|
||||||
send.reply(msg);
|
send(msg);
|
||||||
}};
|
}};
|
||||||
t.detach();
|
t.detach();
|
||||||
});
|
});
|
||||||
|
@ -516,7 +516,7 @@ TEST_CASE("deferred replies", "[commands][send][deferred]") {
|
||||||
auto lock = catch_lock();
|
auto lock = catch_lock();
|
||||||
REQUIRE( replies.size() == 0 ); // The server waits 50ms before sending, so we shouldn't have any reply yet
|
REQUIRE( replies.size() == 0 ); // The server waits 50ms before sending, so we shouldn't have any reply yet
|
||||||
}
|
}
|
||||||
std::this_thread::sleep_for(60ms); // We're at least 70ms in now so the 50ms-delayed server responses should have arrived
|
std::this_thread::sleep_for(60ms * TIME_DILATION); // We're at least 70ms in now so the 50ms-delayed server responses should have arrived
|
||||||
{
|
{
|
||||||
std::lock_guard lq{reply_mut};
|
std::lock_guard lq{reply_mut};
|
||||||
auto lock = catch_lock();
|
auto lock = catch_lock();
|
||||||
|
|
|
@ -279,7 +279,7 @@ TEST_CASE("SN disconnections", "[connect][disconnect]") {
|
||||||
lmq[2]->send(pubkey[1], "sn.hi");
|
lmq[2]->send(pubkey[1], "sn.hi");
|
||||||
lmq[1]->send(pubkey[0], "BYE");
|
lmq[1]->send(pubkey[0], "BYE");
|
||||||
lmq[0]->send(pubkey[2], "sn.hi");
|
lmq[0]->send(pubkey[2], "sn.hi");
|
||||||
std::this_thread::sleep_for(50ms);
|
std::this_thread::sleep_for(50ms * TIME_DILATION);
|
||||||
|
|
||||||
auto lock = catch_lock();
|
auto lock = catch_lock();
|
||||||
REQUIRE(his == 5);
|
REQUIRE(his == 5);
|
||||||
|
@ -504,3 +504,107 @@ TEST_CASE("SN backchatter", "[connect][sn]") {
|
||||||
auto lock = catch_lock();
|
auto lock = catch_lock();
|
||||||
REQUIRE(f == "abc");
|
REQUIRE(f == "abc");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TEST_CASE("inproc connections", "[connect][inproc]") {
|
||||||
|
std::string inproc_name = "foo";
|
||||||
|
OxenMQ omq{get_logger("OMQ» "), LogLevel::trace};
|
||||||
|
|
||||||
|
omq.add_category("public", Access{AuthLevel::none});
|
||||||
|
omq.add_request_command("public", "hello", [&](Message& m) { m.send_reply("hi"); });
|
||||||
|
|
||||||
|
omq.start();
|
||||||
|
|
||||||
|
std::atomic<int> got{0};
|
||||||
|
bool success = false;
|
||||||
|
auto c_inproc = omq.connect_inproc(
|
||||||
|
[&](auto conn) { success = true; got++; },
|
||||||
|
[&](auto conn, std::string_view reason) { auto lock = catch_lock(); INFO("inproc connection failed: " << reason); got++; }
|
||||||
|
);
|
||||||
|
|
||||||
|
wait_for([&got] { return got.load() > 0; });
|
||||||
|
{
|
||||||
|
auto lock = catch_lock();
|
||||||
|
REQUIRE( success );
|
||||||
|
REQUIRE( got == 1 );
|
||||||
|
}
|
||||||
|
|
||||||
|
got = 0;
|
||||||
|
success = false;
|
||||||
|
omq.request(c_inproc, "public.hello", [&](auto success_, auto parts_) {
|
||||||
|
success = success_ && parts_.size() == 1 && parts_.front() == "hi"; got++;
|
||||||
|
});
|
||||||
|
reply_sleep();
|
||||||
|
{
|
||||||
|
auto lock = catch_lock();
|
||||||
|
REQUIRE( got == 1 );
|
||||||
|
REQUIRE( success );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("no explicit inproc listening", "[connect][inproc]") {
|
||||||
|
OxenMQ omq{get_logger("OMQ» "), LogLevel::trace};
|
||||||
|
REQUIRE_THROWS_AS(omq.listen_plain("inproc://foo"), std::logic_error);
|
||||||
|
REQUIRE_THROWS_AS(omq.listen_curve("inproc://foo"), std::logic_error);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("inproc connection permissions", "[connect][inproc]") {
|
||||||
|
std::string listen = random_localhost();
|
||||||
|
OxenMQ omq{get_logger("OMQ» "), LogLevel::trace};
|
||||||
|
|
||||||
|
omq.add_category("public", Access{AuthLevel::none});
|
||||||
|
omq.add_request_command("public", "hello", [&](Message& m) { m.send_reply("hi"); });
|
||||||
|
omq.add_category("private", Access{AuthLevel::admin});
|
||||||
|
omq.add_request_command("private", "handshake", [&](Message& m) { m.send_reply("yo dude"); });
|
||||||
|
|
||||||
|
omq.listen_plain(listen);
|
||||||
|
|
||||||
|
omq.start();
|
||||||
|
|
||||||
|
std::atomic<int> got{0};
|
||||||
|
bool success = false;
|
||||||
|
auto c_inproc = omq.connect_inproc(
|
||||||
|
[&](auto conn) { success = true; got++; },
|
||||||
|
[&](auto conn, std::string_view reason) { auto lock = catch_lock(); INFO("inproc connection failed: " << reason); got++; }
|
||||||
|
);
|
||||||
|
|
||||||
|
bool pub_success = false;
|
||||||
|
auto c_pub = omq.connect_remote(address{listen},
|
||||||
|
[&](auto conn) { pub_success = true; got++; },
|
||||||
|
[&](auto conn, std::string_view reason) { auto lock = catch_lock(); INFO("tcp connection failed: " << reason); got++; }
|
||||||
|
);
|
||||||
|
|
||||||
|
wait_for([&got] { return got.load() == 2; });
|
||||||
|
{
|
||||||
|
auto lock = catch_lock();
|
||||||
|
REQUIRE( got == 2 );
|
||||||
|
REQUIRE( success );
|
||||||
|
REQUIRE( pub_success );
|
||||||
|
}
|
||||||
|
|
||||||
|
got = 0;
|
||||||
|
success = false;
|
||||||
|
pub_success = false;
|
||||||
|
bool success_private = false;
|
||||||
|
bool pub_success_private = false;
|
||||||
|
omq.request(c_inproc, "public.hello", [&](auto success_, auto parts_) {
|
||||||
|
success = success_ && parts_.size() == 1 && parts_.front() == "hi"; got++;
|
||||||
|
});
|
||||||
|
omq.request(c_pub, "public.hello", [&](auto success_, auto parts_) {
|
||||||
|
pub_success = success_ && parts_.size() == 1 && parts_.front() == "hi"; got++;
|
||||||
|
});
|
||||||
|
omq.request(c_inproc, "private.handshake", [&](auto success_, auto parts_) {
|
||||||
|
success_private = success_ && parts_.size() == 1 && parts_.front() == "yo dude"; got++;
|
||||||
|
});
|
||||||
|
omq.request(c_pub, "private.handshake", [&](auto success_, auto parts_) {
|
||||||
|
pub_success_private = success_; got++;
|
||||||
|
});
|
||||||
|
wait_for([&got] { return got.load() == 4; });
|
||||||
|
{
|
||||||
|
auto lock = catch_lock();
|
||||||
|
REQUIRE( got == 4 );
|
||||||
|
REQUIRE( success );
|
||||||
|
REQUIRE( pub_success );
|
||||||
|
REQUIRE( success_private );
|
||||||
|
REQUIRE_FALSE( pub_success_private );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -44,12 +44,32 @@ TEST_CASE("hex encoding/decoding", "[encoding][decoding][hex]") {
|
||||||
std::basic_string_view<std::byte> b{bytes.data(), bytes.size()};
|
std::basic_string_view<std::byte> b{bytes.data(), bytes.size()};
|
||||||
REQUIRE( oxenmq::to_hex(b) == "ff421234"s );
|
REQUIRE( oxenmq::to_hex(b) == "ff421234"s );
|
||||||
|
|
||||||
|
// In-place decoding and truncation via to_hex's returned iterator:
|
||||||
|
std::string some_hex = "48656c6c6f";
|
||||||
|
some_hex.erase(oxenmq::from_hex(some_hex.begin(), some_hex.end(), some_hex.begin()), some_hex.end());
|
||||||
|
REQUIRE( some_hex == "Hello" );
|
||||||
|
|
||||||
|
// Test the returned iterator from encoding
|
||||||
|
std::string hellohex;
|
||||||
|
*oxenmq::to_hex(some_hex.begin(), some_hex.end(), std::back_inserter(hellohex))++ = '!';
|
||||||
|
REQUIRE( hellohex == "48656c6c6f!" );
|
||||||
|
|
||||||
bytes.resize(8);
|
bytes.resize(8);
|
||||||
bytes[0] = std::byte{'f'}; bytes[1] = std::byte{'f'}; bytes[2] = std::byte{'4'}; bytes[3] = std::byte{'2'};
|
bytes[0] = std::byte{'f'}; bytes[1] = std::byte{'f'}; bytes[2] = std::byte{'4'}; bytes[3] = std::byte{'2'};
|
||||||
bytes[4] = std::byte{'1'}; bytes[5] = std::byte{'2'}; bytes[6] = std::byte{'3'}; bytes[7] = std::byte{'4'};
|
bytes[4] = std::byte{'1'}; bytes[5] = std::byte{'2'}; bytes[6] = std::byte{'3'}; bytes[7] = std::byte{'4'};
|
||||||
std::basic_string_view<std::byte> hex_bytes{bytes.data(), bytes.size()};
|
std::basic_string_view<std::byte> hex_bytes{bytes.data(), bytes.size()};
|
||||||
REQUIRE( oxenmq::is_hex(hex_bytes) );
|
REQUIRE( oxenmq::is_hex(hex_bytes) );
|
||||||
REQUIRE( oxenmq::from_hex(hex_bytes) == "\xff\x42\x12\x34" );
|
REQUIRE( oxenmq::from_hex(hex_bytes) == "\xff\x42\x12\x34" );
|
||||||
|
|
||||||
|
REQUIRE( oxenmq::to_hex_size(1) == 2 );
|
||||||
|
REQUIRE( oxenmq::to_hex_size(2) == 4 );
|
||||||
|
REQUIRE( oxenmq::to_hex_size(3) == 6 );
|
||||||
|
REQUIRE( oxenmq::to_hex_size(4) == 8 );
|
||||||
|
REQUIRE( oxenmq::to_hex_size(100) == 200 );
|
||||||
|
REQUIRE( oxenmq::from_hex_size(2) == 1 );
|
||||||
|
REQUIRE( oxenmq::from_hex_size(4) == 2 );
|
||||||
|
REQUIRE( oxenmq::from_hex_size(6) == 3 );
|
||||||
|
REQUIRE( oxenmq::from_hex_size(98) == 49 );
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST_CASE("base32z encoding/decoding", "[encoding][decoding][base32z]") {
|
TEST_CASE("base32z encoding/decoding", "[encoding][decoding][base32z]") {
|
||||||
|
@ -99,6 +119,16 @@ TEST_CASE("base32z encoding/decoding", "[encoding][decoding][base32z]") {
|
||||||
REQUIRE( pk_b32z_again == pk_b32z );
|
REQUIRE( pk_b32z_again == pk_b32z );
|
||||||
REQUIRE( pk_again == pk );
|
REQUIRE( pk_again == pk );
|
||||||
|
|
||||||
|
// In-place decoding and truncation via returned iterator:
|
||||||
|
std::string some_b32z = "jb1sa5dx";
|
||||||
|
some_b32z.erase(oxenmq::from_base32z(some_b32z.begin(), some_b32z.end(), some_b32z.begin()), some_b32z.end());
|
||||||
|
REQUIRE( some_b32z == "Hello" );
|
||||||
|
|
||||||
|
// Test the returned iterator from encoding
|
||||||
|
std::string hellob32z;
|
||||||
|
*oxenmq::to_base32z(some_b32z.begin(), some_b32z.end(), std::back_inserter(hellob32z))++ = '!';
|
||||||
|
REQUIRE( hellob32z == "jb1sa5dx!" );
|
||||||
|
|
||||||
std::vector<std::byte> bytes{{std::byte{0}, std::byte{255}}};
|
std::vector<std::byte> bytes{{std::byte{0}, std::byte{255}}};
|
||||||
std::basic_string_view<std::byte> b{bytes.data(), bytes.size()};
|
std::basic_string_view<std::byte> b{bytes.data(), bytes.size()};
|
||||||
REQUIRE( oxenmq::to_base32z(b) == "yd9o" );
|
REQUIRE( oxenmq::to_base32z(b) == "yd9o" );
|
||||||
|
@ -108,6 +138,37 @@ TEST_CASE("base32z encoding/decoding", "[encoding][decoding][base32z]") {
|
||||||
std::basic_string_view<std::byte> b32_bytes{bytes.data(), bytes.size()};
|
std::basic_string_view<std::byte> b32_bytes{bytes.data(), bytes.size()};
|
||||||
REQUIRE( oxenmq::is_base32z(b32_bytes) );
|
REQUIRE( oxenmq::is_base32z(b32_bytes) );
|
||||||
REQUIRE( oxenmq::from_base32z(b32_bytes) == "\x00\xff"sv );
|
REQUIRE( oxenmq::from_base32z(b32_bytes) == "\x00\xff"sv );
|
||||||
|
|
||||||
|
REQUIRE( oxenmq::is_base32z("") );
|
||||||
|
REQUIRE_FALSE( oxenmq::is_base32z("y") );
|
||||||
|
REQUIRE( oxenmq::is_base32z("yy") );
|
||||||
|
REQUIRE_FALSE( oxenmq::is_base32z("yyy") );
|
||||||
|
REQUIRE( oxenmq::is_base32z("yyyy") );
|
||||||
|
REQUIRE( oxenmq::is_base32z("yyyyy") );
|
||||||
|
REQUIRE_FALSE( oxenmq::is_base32z("yyyyyy") );
|
||||||
|
REQUIRE( oxenmq::is_base32z("yyyyyyy") );
|
||||||
|
REQUIRE( oxenmq::is_base32z("yyyyyyyy") );
|
||||||
|
|
||||||
|
REQUIRE( oxenmq::to_base32z_size(1) == 2 );
|
||||||
|
REQUIRE( oxenmq::to_base32z_size(2) == 4 );
|
||||||
|
REQUIRE( oxenmq::to_base32z_size(3) == 5 );
|
||||||
|
REQUIRE( oxenmq::to_base32z_size(4) == 7 );
|
||||||
|
REQUIRE( oxenmq::to_base32z_size(5) == 8 );
|
||||||
|
REQUIRE( oxenmq::to_base32z_size(30) == 48 );
|
||||||
|
REQUIRE( oxenmq::to_base32z_size(31) == 50 );
|
||||||
|
REQUIRE( oxenmq::to_base32z_size(32) == 52 );
|
||||||
|
REQUIRE( oxenmq::to_base32z_size(33) == 53 );
|
||||||
|
REQUIRE( oxenmq::to_base32z_size(100) == 160 );
|
||||||
|
REQUIRE( oxenmq::from_base32z_size(160) == 100 );
|
||||||
|
REQUIRE( oxenmq::from_base32z_size(53) == 33 );
|
||||||
|
REQUIRE( oxenmq::from_base32z_size(52) == 32 );
|
||||||
|
REQUIRE( oxenmq::from_base32z_size(50) == 31 );
|
||||||
|
REQUIRE( oxenmq::from_base32z_size(48) == 30 );
|
||||||
|
REQUIRE( oxenmq::from_base32z_size(8) == 5 );
|
||||||
|
REQUIRE( oxenmq::from_base32z_size(7) == 4 );
|
||||||
|
REQUIRE( oxenmq::from_base32z_size(5) == 3 );
|
||||||
|
REQUIRE( oxenmq::from_base32z_size(4) == 2 );
|
||||||
|
REQUIRE( oxenmq::from_base32z_size(2) == 1 );
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST_CASE("base64 encoding/decoding", "[encoding][decoding][base64]") {
|
TEST_CASE("base64 encoding/decoding", "[encoding][decoding][base64]") {
|
||||||
|
@ -125,6 +186,13 @@ TEST_CASE("base64 encoding/decoding", "[encoding][decoding][base64]") {
|
||||||
REQUIRE( oxenmq::to_base64("abcde") == "YWJjZGU=" );
|
REQUIRE( oxenmq::to_base64("abcde") == "YWJjZGU=" );
|
||||||
REQUIRE( oxenmq::to_base64("abcdef") == "YWJjZGVm" );
|
REQUIRE( oxenmq::to_base64("abcdef") == "YWJjZGVm" );
|
||||||
|
|
||||||
|
REQUIRE( oxenmq::to_base64_unpadded("a") == "YQ" );
|
||||||
|
REQUIRE( oxenmq::to_base64_unpadded("ab") == "YWI" );
|
||||||
|
REQUIRE( oxenmq::to_base64_unpadded("abc") == "YWJj" );
|
||||||
|
REQUIRE( oxenmq::to_base64_unpadded("abcd") == "YWJjZA" );
|
||||||
|
REQUIRE( oxenmq::to_base64_unpadded("abcde") == "YWJjZGU" );
|
||||||
|
REQUIRE( oxenmq::to_base64_unpadded("abcdef") == "YWJjZGVm" );
|
||||||
|
|
||||||
REQUIRE( oxenmq::to_base64("\0\0\0\xff"s) == "AAAA/w==" );
|
REQUIRE( oxenmq::to_base64("\0\0\0\xff"s) == "AAAA/w==" );
|
||||||
REQUIRE( oxenmq::to_base64("\0\0\0\xff\xff"s) == "AAAA//8=" );
|
REQUIRE( oxenmq::to_base64("\0\0\0\xff\xff"s) == "AAAA//8=" );
|
||||||
REQUIRE( oxenmq::to_base64("\0\0\0\xff\xff\xff"s) == "AAAA////" );
|
REQUIRE( oxenmq::to_base64("\0\0\0\xff\xff\xff"s) == "AAAA////" );
|
||||||
|
@ -159,6 +227,7 @@ TEST_CASE("base64 encoding/decoding", "[encoding][decoding][base64]") {
|
||||||
REQUIRE( oxenmq::is_base64("YWJjZB") ); // not really valid, but we explicitly accept it
|
REQUIRE( oxenmq::is_base64("YWJjZB") ); // not really valid, but we explicitly accept it
|
||||||
|
|
||||||
REQUIRE_FALSE( oxenmq::is_base64("YWJjZ=") ); // invalid padding (padding can only be 4th or 3rd+4th of a 4-char block)
|
REQUIRE_FALSE( oxenmq::is_base64("YWJjZ=") ); // invalid padding (padding can only be 4th or 3rd+4th of a 4-char block)
|
||||||
|
REQUIRE_FALSE( oxenmq::is_base64("YYYYA") ); // invalid: base64 can never be length 4n+1
|
||||||
REQUIRE_FALSE( oxenmq::is_base64("YWJj=") );
|
REQUIRE_FALSE( oxenmq::is_base64("YWJj=") );
|
||||||
REQUIRE_FALSE( oxenmq::is_base64("YWJj=A") );
|
REQUIRE_FALSE( oxenmq::is_base64("YWJj=A") );
|
||||||
REQUIRE_FALSE( oxenmq::is_base64("YWJjA===") );
|
REQUIRE_FALSE( oxenmq::is_base64("YWJjA===") );
|
||||||
|
@ -189,6 +258,16 @@ TEST_CASE("base64 encoding/decoding", "[encoding][decoding][base64]") {
|
||||||
REQUIRE( pk_b64_again == pk_b64 );
|
REQUIRE( pk_b64_again == pk_b64 );
|
||||||
REQUIRE( pk_again == pk );
|
REQUIRE( pk_again == pk );
|
||||||
|
|
||||||
|
// In-place decoding and truncation via returned iterator:
|
||||||
|
std::string some_b64 = "SGVsbG8=";
|
||||||
|
some_b64.erase(oxenmq::from_base64(some_b64.begin(), some_b64.end(), some_b64.begin()), some_b64.end());
|
||||||
|
REQUIRE( some_b64 == "Hello" );
|
||||||
|
|
||||||
|
// Test the returned iterator from encoding
|
||||||
|
std::string hellob64;
|
||||||
|
*oxenmq::to_base64(some_b64.begin(), some_b64.end(), std::back_inserter(hellob64))++ = '!';
|
||||||
|
REQUIRE( hellob64 == "SGVsbG8=!" );
|
||||||
|
|
||||||
std::vector<std::byte> bytes{{std::byte{0}, std::byte{255}}};
|
std::vector<std::byte> bytes{{std::byte{0}, std::byte{255}}};
|
||||||
std::basic_string_view<std::byte> b{bytes.data(), bytes.size()};
|
std::basic_string_view<std::byte> b{bytes.data(), bytes.size()};
|
||||||
REQUIRE( oxenmq::to_base64(b) == "AP8=" );
|
REQUIRE( oxenmq::to_base64(b) == "AP8=" );
|
||||||
|
@ -198,6 +277,114 @@ TEST_CASE("base64 encoding/decoding", "[encoding][decoding][base64]") {
|
||||||
std::basic_string_view<std::byte> b64_bytes{bytes.data(), bytes.size()};
|
std::basic_string_view<std::byte> b64_bytes{bytes.data(), bytes.size()};
|
||||||
REQUIRE( oxenmq::is_base64(b64_bytes) );
|
REQUIRE( oxenmq::is_base64(b64_bytes) );
|
||||||
REQUIRE( oxenmq::from_base64(b64_bytes) == "\xff\x00"sv );
|
REQUIRE( oxenmq::from_base64(b64_bytes) == "\xff\x00"sv );
|
||||||
|
|
||||||
|
REQUIRE( oxenmq::to_base64_size(1) == 4 );
|
||||||
|
REQUIRE( oxenmq::to_base64_size(2) == 4 );
|
||||||
|
REQUIRE( oxenmq::to_base64_size(3) == 4 );
|
||||||
|
REQUIRE( oxenmq::to_base64_size(4) == 8 );
|
||||||
|
REQUIRE( oxenmq::to_base64_size(5) == 8 );
|
||||||
|
REQUIRE( oxenmq::to_base64_size(6) == 8 );
|
||||||
|
REQUIRE( oxenmq::to_base64_size(30) == 40 );
|
||||||
|
REQUIRE( oxenmq::to_base64_size(31) == 44 );
|
||||||
|
REQUIRE( oxenmq::to_base64_size(32) == 44 );
|
||||||
|
REQUIRE( oxenmq::to_base64_size(33) == 44 );
|
||||||
|
REQUIRE( oxenmq::to_base64_size(100) == 136 );
|
||||||
|
REQUIRE( oxenmq::from_base64_size(136) == 102 ); // Not symmetric because we don't know the last two are padding
|
||||||
|
REQUIRE( oxenmq::from_base64_size(134) == 100 ); // Unpadded
|
||||||
|
REQUIRE( oxenmq::from_base64_size(44) == 33 );
|
||||||
|
REQUIRE( oxenmq::from_base64_size(43) == 32 );
|
||||||
|
REQUIRE( oxenmq::from_base64_size(42) == 31 );
|
||||||
|
REQUIRE( oxenmq::from_base64_size(40) == 30 );
|
||||||
|
REQUIRE( oxenmq::from_base64_size(8) == 6 );
|
||||||
|
REQUIRE( oxenmq::from_base64_size(7) == 5 );
|
||||||
|
REQUIRE( oxenmq::from_base64_size(6) == 4 );
|
||||||
|
REQUIRE( oxenmq::from_base64_size(4) == 3 );
|
||||||
|
REQUIRE( oxenmq::from_base64_size(3) == 2 );
|
||||||
|
REQUIRE( oxenmq::from_base64_size(2) == 1 );
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("transcoding", "[decoding][encoding][base32z][hex][base64]") {
|
||||||
|
// Decoders:
|
||||||
|
oxenmq::base64_decoder in64{pk_b64.begin(), pk_b64.end()};
|
||||||
|
oxenmq::base32z_decoder in32z{pk_b32z.begin(), pk_b32z.end()};
|
||||||
|
oxenmq::hex_decoder in16{pk_hex.begin(), pk_hex.end()};
|
||||||
|
|
||||||
|
// Transcoders:
|
||||||
|
oxenmq::base32z_encoder b64_to_b32z{in64, in64.end()};
|
||||||
|
oxenmq::base32z_encoder hex_to_b32z{in16, in16.end()};
|
||||||
|
oxenmq::hex_encoder b64_to_hex{in64, in64.end()};
|
||||||
|
oxenmq::hex_encoder b32z_to_hex{in32z, in32z.end()};
|
||||||
|
oxenmq::base64_encoder hex_to_b64{in16, in16.end()};
|
||||||
|
oxenmq::base64_encoder b32z_to_b64{in32z, in32z.end()};
|
||||||
|
// These ones are stupid, but should work anyway:
|
||||||
|
oxenmq::base64_encoder b64_to_b64{in64, in64.end()};
|
||||||
|
oxenmq::base32z_encoder b32z_to_b32z{in32z, in32z.end()};
|
||||||
|
oxenmq::hex_encoder hex_to_hex{in16, in16.end()};
|
||||||
|
|
||||||
|
// Decoding to bytes:
|
||||||
|
std::string x;
|
||||||
|
auto xx = std::back_inserter(x);
|
||||||
|
std::copy(in64, in64.end(), xx);
|
||||||
|
REQUIRE( x == pk );
|
||||||
|
x.clear();
|
||||||
|
std::copy(in32z, in32z.end(), xx);
|
||||||
|
REQUIRE( x == pk );
|
||||||
|
x.clear();
|
||||||
|
std::copy(in16, in16.end(), xx);
|
||||||
|
REQUIRE( x == pk );
|
||||||
|
|
||||||
|
// Transcoding
|
||||||
|
x.clear();
|
||||||
|
std::copy(b64_to_hex, b64_to_hex.end(), xx);
|
||||||
|
CHECK( x == pk_hex );
|
||||||
|
|
||||||
|
x.clear();
|
||||||
|
std::copy(b64_to_b32z, b64_to_b32z.end(), xx);
|
||||||
|
CHECK( x == pk_b32z );
|
||||||
|
|
||||||
|
x.clear();
|
||||||
|
std::copy(b64_to_b64, b64_to_b64.end(), xx);
|
||||||
|
CHECK( x == pk_b64 );
|
||||||
|
|
||||||
|
x.clear();
|
||||||
|
std::copy(b32z_to_hex, b32z_to_hex.end(), xx);
|
||||||
|
CHECK( x == pk_hex );
|
||||||
|
|
||||||
|
x.clear();
|
||||||
|
std::copy(b32z_to_b32z, b32z_to_b32z.end(), xx);
|
||||||
|
CHECK( x == pk_b32z );
|
||||||
|
|
||||||
|
x.clear();
|
||||||
|
std::copy(b32z_to_b64, b32z_to_b64.end(), xx);
|
||||||
|
CHECK( x == pk_b64 );
|
||||||
|
|
||||||
|
x.clear();
|
||||||
|
std::copy(hex_to_hex, hex_to_hex.end(), xx);
|
||||||
|
CHECK( x == pk_hex );
|
||||||
|
|
||||||
|
x.clear();
|
||||||
|
std::copy(hex_to_b32z, hex_to_b32z.end(), xx);
|
||||||
|
CHECK( x == pk_b32z );
|
||||||
|
|
||||||
|
x.clear();
|
||||||
|
std::copy(hex_to_b64, hex_to_b64.end(), xx);
|
||||||
|
CHECK( x == pk_b64 );
|
||||||
|
|
||||||
|
// Make a big chain of conversions
|
||||||
|
oxenmq::base32z_encoder it1{in64, in64.end()};
|
||||||
|
oxenmq::base32z_decoder it2{it1, it1.end()};
|
||||||
|
oxenmq::base64_encoder it3{it2, it2.end()};
|
||||||
|
oxenmq::base64_decoder it4{it3, it3.end()};
|
||||||
|
oxenmq::hex_encoder it5{it4, it4.end()};
|
||||||
|
x.clear();
|
||||||
|
std::copy(it5, it5.end(), xx);
|
||||||
|
CHECK( x == pk_hex );
|
||||||
|
|
||||||
|
// No-padding b64 encoding:
|
||||||
|
oxenmq::base64_encoder b64_nopad{pk.begin(), pk.end(), false};
|
||||||
|
x.clear();
|
||||||
|
std::copy(b64_nopad, b64_nopad.end(), xx);
|
||||||
|
CHECK( x == pk_b64.substr(0, pk_b64.size()-1) );
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST_CASE("std::byte decoding", "[decoding][hex][base32z][base64]") {
|
TEST_CASE("std::byte decoding", "[decoding][hex][base32z][base64]") {
|
||||||
|
|
|
@ -15,9 +15,10 @@ TEST_CASE("timer test", "[timer][basic]") {
|
||||||
auto start = std::chrono::steady_clock::now();
|
auto start = std::chrono::steady_clock::now();
|
||||||
wait_for([&] { return ticks.load() > 3; });
|
wait_for([&] { return ticks.load() > 3; });
|
||||||
{
|
{
|
||||||
|
auto elapsed_ms = std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::steady_clock::now() - start).count();
|
||||||
auto lock = catch_lock();
|
auto lock = catch_lock();
|
||||||
REQUIRE( ticks.load() > 3 );
|
REQUIRE( ticks.load() > 3 );
|
||||||
REQUIRE( std::chrono::steady_clock::now() - start < 40ms );
|
REQUIRE( elapsed_ms < 50 * TIME_DILATION );
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -35,13 +36,13 @@ TEST_CASE("timer squelch", "[timer][squelch]") {
|
||||||
// finishes, by which point we set `done` and so should get exactly 1 tick.
|
// finishes, by which point we set `done` and so should get exactly 1 tick.
|
||||||
auto timer = omq.add_timer([&] {
|
auto timer = omq.add_timer([&] {
|
||||||
if (first.exchange(false)) {
|
if (first.exchange(false)) {
|
||||||
std::this_thread::sleep_for(30ms);
|
std::this_thread::sleep_for(30ms * TIME_DILATION);
|
||||||
ticks++;
|
ticks++;
|
||||||
done = true;
|
done = true;
|
||||||
} else if (!done) {
|
} else if (!done) {
|
||||||
ticks++;
|
ticks++;
|
||||||
}
|
}
|
||||||
}, 5ms, true /* squelch */);
|
}, 5ms * TIME_DILATION, true /* squelch */);
|
||||||
omq.start();
|
omq.start();
|
||||||
|
|
||||||
wait_for([&] { return done.load(); });
|
wait_for([&] { return done.load(); });
|
||||||
|
@ -58,7 +59,7 @@ TEST_CASE("timer squelch", "[timer][squelch]") {
|
||||||
std::atomic<int> ticks2 = 0;
|
std::atomic<int> ticks2 = 0;
|
||||||
auto timer2 = omq.add_timer([&] {
|
auto timer2 = omq.add_timer([&] {
|
||||||
if (first2.exchange(false)) {
|
if (first2.exchange(false)) {
|
||||||
std::this_thread::sleep_for(30ms);
|
std::this_thread::sleep_for(40ms * TIME_DILATION);
|
||||||
done2 = true;
|
done2 = true;
|
||||||
} else if (!done2) {
|
} else if (!done2) {
|
||||||
ticks2++;
|
ticks2++;
|
||||||
|
@ -82,13 +83,13 @@ TEST_CASE("timer cancel", "[timer][cancel]") {
|
||||||
std::atomic<int> ticks = 0;
|
std::atomic<int> ticks = 0;
|
||||||
|
|
||||||
// We set up *and cancel* this timer before omq starts, so it should never fire
|
// We set up *and cancel* this timer before omq starts, so it should never fire
|
||||||
auto notimer = omq.add_timer([&] { ticks += 1000; }, 5ms);
|
auto notimer = omq.add_timer([&] { ticks += 1000; }, 5ms * TIME_DILATION);
|
||||||
omq.cancel_timer(notimer);
|
omq.cancel_timer(notimer);
|
||||||
|
|
||||||
TimerID timer = omq.add_timer([&] {
|
TimerID timer = omq.add_timer([&] {
|
||||||
if (++ticks == 3)
|
if (++ticks == 3)
|
||||||
omq.cancel_timer(timer);
|
omq.cancel_timer(timer);
|
||||||
}, 5ms);
|
}, 5ms * TIME_DILATION);
|
||||||
|
|
||||||
omq.start();
|
omq.start();
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue