Merge pull request #59 from necro-nemesis/fedora/34

prep v1.2.8 build
This commit is contained in:
Jason Rhinelander 2021-10-27 16:43:59 -03:00 committed by GitHub
commit 2dcc3f72a2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 1468 additions and 248 deletions

View File

@ -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)")
project(liboxenmq
VERSION 1.2.6
VERSION 1.2.8
LANGUAGES CXX C)
include(GNUInstallDirs)
@ -113,13 +113,15 @@ target_include_directories(oxenmq
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/cppzmq>
)
target_compile_options(oxenmq PRIVATE -Wall -Wextra -Werror)
set_target_properties(oxenmq PROPERTIES
CXX_STANDARD 17
CXX_STANDARD_REQUIRED ON
CXX_EXTENSIONS OFF
POSITION_INDEPENDENT_CODE ON
)
target_compile_options(oxenmq PRIVATE -Wall -Wextra)
option(WARNINGS_AS_ERRORS "treat all warnings as errors" ON)
if(WARNINGS_AS_ERRORS)
target_compile_options(oxenmq PRIVATE -Werror)
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)
foreach(lib ${ARGN})

View File

@ -1,6 +1,6 @@
Name: oxenmq
Version: 1.2.6
Release: 2%{?dist}
Version: 1.2.8
Release: 1%{?dist}
Summary: zeromq-based Oxen message passing library
License: BSD
@ -87,11 +87,18 @@ build software using oxenmq.
%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
- Split oxenmq into lib and devel package
- Versioned the library, as we do for debs
- 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
-Second build update to v1.2.6

View File

@ -1,7 +1,7 @@
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_HASH SHA512=4c18d784085179c5b1fcb753a93813095a12c8d34970f2e1bfca6499be6c9d67769c71c68b7ca54ff181b20390043170e89733c22f76ff1ea46494814f7095b1)
set(LIBZMQ_HASH SHA512=e198ef9f82d392754caadd547537666d4fba0afd7d027749b3adae450516bcf284d241d4616cad3cb4ad9af8c10373d456de92dc6d115b037941659f141e7c0e)
message(${LIBZMQ_URL})

2
cppzmq

@ -1 +1 @@
Subproject commit 76bf169fd67b8e99c1b0e6490029d9cd5ef97666
Subproject commit 33ed54228e98a82db88da5ac9aca001147521c20

View File

@ -163,9 +163,8 @@ struct address {
/// 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
/// encoding for the pubkey (for curve addresses). For literal IPv6 addresses we replace the
/// surround the
/// address with $ instead of $
/// encoding for the pubkey (for curve addresses). For literal IPv6 addresses we surround the
/// address with $ instead of [...]
///
/// \throws std::logic_error if called on a unix socket address.
std::string qr_address() const;

View File

@ -119,7 +119,6 @@ void OxenMQ::proxy_set_active_sns(pubkey_set pubkeys) {
}
void OxenMQ::update_active_sns(pubkey_set added, pubkey_set removed) {
LMQ_LOG(info, "uh, ", added.size());
if (proxy_thread.joinable()) {
std::array<uintptr_t, 2> data;
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
// to the _clean version.
LMQ_LOG(info, "uh, ", added.size(), ", ", removed.size());
for (auto it = removed.begin(); it != removed.end(); ) {
const auto& pk = *it;
if (pk.size() != 32) {

View File

@ -74,40 +74,81 @@ static_assert(b32z_lut.from_b32z('w') == 20 && b32z_lut.from_b32z('T') == 17 &&
} // namespace detail
/// Converts bytes into a base32z encoded character sequence.
template <typename InputIt, typename OutputIt>
void to_base32z(InputIt begin, InputIt end, OutputIt out) {
static_assert(sizeof(decltype(*begin)) == 1, "to_base32z requires chars/bytes");
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++);
/// Returns the number of characters required to encode a base32z string from the given number of bytes.
inline constexpr size_t to_base32z_size(size_t byte_size) { return (byte_size*8 + 4) / 5; } // ⌈bits/5⌉ because 5 bits per byte
/// Returns the (maximum) number of bytes required to decode a base32z string of the given size.
inline constexpr size_t from_base32z_size(size_t b32z_size) { return b32z_size*5 / 8; } // ⌊bits/8⌋
// we just added 8 bits, so we can *always* consume 5 to produce one character, so (net) we
// are adding 3 bits.
bits += 3;
*out++ = detail::b32z_lut.to_b32z(r >> bits); // Right-shift off the bits we aren't consuming right now
/// Iterable object for on-the-fly base32z encoding. Used internally, but also particularly useful
/// when converting from one encoding to another.
template <typename InputIt>
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;
if (bits >= 5) { // We have enough bits to produce a second character; essentially the same as above
bits -= 5; // Except now we are just consuming 5 without having added any more
*out++ = detail::b32z_lut.to_b32z(r >> bits);
r &= (1 << bits) - 1;
// If we end up with less than 5 significant bits then try to pull another 8 bits:
if (bits < 5 && _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. 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
*out++ = detail::b32z_lut.to_b32z(r << (5 - bits));
char operator*() {
// 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.
template <typename It>
std::string to_base32z(It begin, It end) {
std::string base32z;
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.
if constexpr (std::is_base_of_v<std::random_access_iterator_tag, typename std::iterator_traits<It>::iterator_category>) {
using std::distance;
base32z.reserve(to_base32z_size(distance(begin, end)));
}
to_base32z(begin, end, std::back_inserter(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()); }
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>
constexpr bool is_base32z(It begin, It end) {
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) {
auto c = static_cast<unsigned char>(*begin);
if (detail::b32z_lut.from_b32z(c) == 0 && !(c == 'y' || c == 'Y'))
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;
}
@ -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::string_view s) { return is_base32z<>(s); }
/// 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 earlier than `begin`. Note that if you pass in a sequence that could not
/// have been created by a base32z encoding of a byte sequence, we treat the excess bits as if they
/// were not provided.
/// Iterable object for on-the-fly base32z decoding. Used internally, but also particularly useful
/// when converting from one encoding to another. The input range must be a valid base32z
/// encoded string.
///
/// For example, "yyy" represents a 15-bit value, but a byte sequence is either 8-bit (requiring 2
/// characters) or 16-bit (requiring 4). Similarly, "yb" is an impossible encoding because it has
/// its 10th bit set (b = 00001), but a base32z encoded value should have all 0's beyond the 8th (or
/// 16th or 24th or ... bit). We treat any such bits as if they were not specified (even if they
/// are): which means "yy", "yb", "yyy", "yy9", "yd", etc. all decode to the same 1-byte value "\0".
template <typename InputIt, typename OutputIt>
void from_base32z(InputIt begin, InputIt end, OutputIt out) {
static_assert(sizeof(decltype(*begin)) == 1, "from_base32z requires chars/bytes");
uint_fast16_t curr = 0;
int bits = 0; // number of bits we've loaded into val; we always keep this < 8.
while (begin != end) {
curr = curr << 5 | detail::b32z_lut.from_b32z(static_cast<unsigned char>(*begin++));
if (bits >= 3) {
bits -= 3; // Added 5, removing 8
*out++ = static_cast<detail::byte_type_t<OutputIt>>(
static_cast<uint8_t>(curr >> bits));
curr &= (1 << bits) - 1;
} else {
bits += 5;
}
/// Note that we ignore "padding" bits without requiring that they actually be 0. For instance, the
/// bytes "\ff\ff" are ideally encoded as "999o" (16 bits of 1s + 4 padding 0 bits), but we don't
/// require that the padding bits be 0. That is, "9999", "9993", etc. will all decode to the same
/// \ff\ff output string.
template <typename InputIt>
struct base32z_decoder final {
private:
InputIt _it, _end;
static_assert(sizeof(decltype(*_it)) == 1, "base32z_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;
base32z_decoder(InputIt begin, InputIt end) : _it{std::move(begin)}, _end{std::move(end)} {
if (_it != _end)
load_byte();
}
// Ignore any trailing bits. base32z encoding always has at least as many bits as the source
// 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
// a 0-bit value; if we have some extra bits (e.g. 6 bytes requires 10 b32z chars, but that
// 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
// from an actual byte sequence).
//
// The "bits += 5" case, then, means that we could end with 5-7 bits. This, however, cannot be
// produced by a valid encoding:
// - 0 bytes gives us 0 chars with 0 leftover bits
// - 1 byte gives us 2 chars with 2 leftover bits
// - 2 bytes gives us 4 chars with 4 leftover bits
// - 3 bytes gives us 5 chars with 1 leftover bit
// - 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)
//
// 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
// character you added isn't there by not doing anything here.
base32z_decoder end() { return {_end, _end}; }
bool operator==(const base32z_decoder& i) { return _it == i._it; }
bool operator!=(const base32z_decoder& i) { return _it != i._it; }
base32z_decoder& operator++() {
// Discard 8 most significant bits
bits -= 8;
in &= (1 << bits) - 1;
if (++_it != _end)
load_byte();
return *this;
}
base32z_decoder operator++(int) { base32z_decoder copy{*this}; ++*this; return copy; }
char operator*() {
return in >> (bits - 8);
}
private:
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
@ -190,8 +286,10 @@ void from_base32z(InputIt begin, InputIt end, OutputIt out) {
template <typename It>
std::string from_base32z(It begin, It end) {
std::string bytes;
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.
if constexpr (std::is_base_of_v<std::random_access_iterator_tag, typename std::iterator_traits<It>::iterator_category>) {
using std::distance;
bytes.reserve(from_base32z_size(distance(begin, end)));
}
from_base32z(begin, end, std::back_inserter(bytes));
return bytes;
}

View File

@ -76,70 +76,153 @@ static_assert(b64_lut.from_b64('/') == 63 && b64_lut.from_b64('7') == 59 && b64_
} // namespace detail
/// Converts bytes into a base64 encoded character sequence.
template <typename InputIt, typename OutputIt>
void to_base64(InputIt begin, InputIt end, OutputIt out) {
static_assert(sizeof(decltype(*begin)) == 1, "to_base64 requires chars/bytes");
int bits = 0; // Tracks the number of unconsumed bits held in r, will always be in {0, 2, 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 6 to produce one character, so (net) we
// 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++ = '=';
}
/// Returns the number of characters required to encode a base64 string from the given number of bytes.
inline constexpr size_t to_base64_size(size_t byte_size, bool padded = true) {
return padded
? (byte_size + 2) / 3 * 4 // bytes*4/3, rounded up to the next multiple of 4
: (byte_size * 4 + 2) / 3; // ⌈bytes*4/3⌉
}
/// Returns the (maximum) number of bytes required to decode a base64 string of the given size.
/// 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) {
return b64_size * 3 / 4; // == ⌊bits/8⌋; floor because we ignore trailing "impossible" bits (see below)
}
/// 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>
std::string to_base64(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>)
base64.reserve((std::distance(begin, end) + 2) / 3 * 4); // bytes*4/3, rounded up to the next multiple of 4
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)));
}
to_base64(begin, end, std::back_inserter(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>
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); }
/// 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,
/// 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>
constexpr bool is_base64(It begin, It end) {
static_assert(sizeof(decltype(*begin)) == 1, "is_base64 requires chars/bytes");
using std::distance;
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.
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);
if (detail::b64_lut.from_b64(c) == 0 && c != 'A')
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;
}
@ -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::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
/// 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
/// required.
/// long as `out` is no later than `begin`. Trailing padding characters are permitted but not
/// 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
/// 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:
/// the last 4 bits of the last character are essentially considered padding.
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");
uint_fast16_t curr = 0;
int bits = 0; // number of bits we've loaded into val; we always keep this < 8.
while (begin != end) {
auto c = static_cast<unsigned char>(*begin++);
// 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).
base64_decoder it{begin, end};
auto bend = it.end();
while (it != bend)
*out++ = static_cast<detail::byte_type_t<OutputIt>>(*it++);
return out;
}
/// 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>
std::string from_base64(It begin, It end) {
std::string bytes;
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
if constexpr (std::is_base_of_v<std::random_access_iterator_tag, typename std::iterator_traits<It>::iterator_category>) {
using std::distance;
bytes.reserve(from_base64_size(distance(begin, end)));
}
from_base64(begin, end, std::back_inserter(bytes));
return bytes;
}

306
oxenmq/bt_producer.h Normal file
View File

@ -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

View File

@ -27,6 +27,10 @@
// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
#include "bt_serialize.h"
#include "bt_producer.h"
#include "variant.h"
#include <cassert>
#include <iterator>
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

View File

@ -62,23 +62,65 @@ static_assert(hex_lut.from_hex('a') == 10 && hex_lut.from_hex('F') == 15 && hex_
} // namespace detail
/// Creates hex digits from a character sequence.
template <typename InputIt, typename OutputIt>
void to_hex(InputIt begin, InputIt end, OutputIt out) {
static_assert(sizeof(decltype(*begin)) == 1, "to_hex requires chars/bytes");
for (; begin != end; ++begin) {
uint8_t c = static_cast<uint8_t>(*begin);
*out++ = detail::hex_lut.to_hex(c >> 4);
*out++ = detail::hex_lut.to_hex(c & 0x0f);
/// Returns the number of characters required to encode a hex string from the given number of bytes.
inline constexpr size_t to_hex_size(size_t byte_size) { return byte_size * 2; }
/// Returns the number of bytes required to decode a hex string of the given size.
inline constexpr size_t from_hex_size(size_t hex_size) { return hex_size / 2; }
/// Iterable object for on-the-fly hex encoding. Used internally, but also particularly useful when
/// converting from one encoding to another.
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
template <typename It>
std::string to_hex(It begin, It end) {
std::string hex;
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));
if constexpr (std::is_base_of_v<std::random_access_iterator_tag, typename std::iterator_traits<It>::iterator_category>) {
using std::distance;
hex.reserve(to_hex_size(distance(begin, end)));
}
to_hex(begin, end, std::back_inserter(hex));
return hex;
}
@ -101,9 +143,11 @@ template <typename It>
constexpr bool is_hex(It begin, It end) {
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>;
if constexpr (ra)
if (std::distance(begin, end) % 2 != 0)
if constexpr (ra) {
using std::distance;
if (distance(begin, end) % 2 != 0)
return false;
}
size_t count = 0;
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
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
/// [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
/// than begin.
/// check. It is permitted for the input and output ranges to overlap as long as out is no later
/// than begin. Returns the final value of out (that is, the iterator positioned just after the
/// last written character).
template <typename InputIt, typename OutputIt>
void from_hex(InputIt begin, InputIt end, OutputIt out) {
using std::distance;
OutputIt from_hex(InputIt begin, InputIt end, OutputIt out) {
assert(is_hex(begin, end));
while (begin != end) {
auto a = *begin++;
auto b = *begin++;
*out++ = static_cast<detail::byte_type_t<OutputIt>>(
from_hex_pair(static_cast<unsigned char>(a), static_cast<unsigned char>(b)));
}
auto it = hex_decoder(begin, end);
const auto hend = it.end();
while (it != hend)
*out++ = static_cast<detail::byte_type_t<OutputIt>>(*it++);
return out;
}
/// 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>
std::string from_hex(It begin, It end) {
std::string bytes;
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);
if constexpr (std::is_base_of_v<std::random_access_iterator_tag, typename std::iterator_traits<It>::iterator_category>) {
using std::distance;
bytes.reserve(from_hex_size(distance(begin, end)));
}
from_hex(begin, end, std::back_inserter(bytes));
return bytes;
}

View File

@ -75,6 +75,15 @@ public:
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.
template <typename... Args>
void back(std::string_view command, Args&&... args) const;

View File

@ -262,7 +262,9 @@ void OxenMQ::start() {
}
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)};
if (proxy_thread.joinable())
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) {
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)};
if (proxy_thread.joinable())
detail::send_control(get_control_socket(), "BIND", bt_serialize(detail::serialize_object(std::move(d))));

View File

@ -82,6 +82,9 @@ inline constexpr auto DEFAULT_CONNECT_SN_KEEP_ALIVE = 5min;
// The default timeout for connect_remote()
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
// `false` to signal a timeout.
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.
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
/// and stop using existing connections iterators.
bool connections_updated = true;
@ -442,7 +448,7 @@ private:
/// indices of idle, active 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());
/// 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
* 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
* messages are suppressed.
*
@ -1000,7 +999,7 @@ public:
* 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.
*
* @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".
*
* @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 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
* - 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
* this connection.
*
@ -1095,6 +1094,16 @@ public:
AuthLevel auth_level = AuthLevel::none,
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
* commonly, `connect_sn()`).
@ -1103,8 +1112,7 @@ public:
*
* @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
* this lingering is internal; the disconnect_remote() call does not block). The default is 1
* second.
* this lingering is internal; the disconnect() call does not block). The default is 1 second.
*
* 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
@ -1118,8 +1126,8 @@ public:
* if not already connected).
*
* 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
* pass a a `send_option::keep_alive{IDLETIME}` in `opts`.
* the connection should stay open longer you should either call `connect_sn(pubkey, IDLETIME)`
* 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
* 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
* 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
* (so that, under heavy load or long jobs, there can be more than one of the same job scheduled
* or running at a time) then specify `squelch` as `false`.
* previously scheduled callback of the job has completed. If you want to override this (so
* that, under heavy load or long jobs, there can be more than one of the same job scheduled or
* 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
* 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
/// 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 {
std::chrono::milliseconds 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
/// 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.
///
/// Specifying a negative timeout is equivalent to not specifying the option at all.
struct request_timeout {
std::chrono::milliseconds 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
/// 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 {
bool use_ephemeral_routing_id = true;
// 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
/// 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
/// connect_sn() is 5 minutes (which is much longer than the default for an implicit connect() by
/// calling send() directly with a pubkey.)
@ -1501,6 +1515,7 @@ struct keep_alive {
/// potentially expensive lookup call).
struct hint {
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} {}
};
@ -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
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;
}
@ -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)
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
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
@ -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(...)");
}
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(...)");
}
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;
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;
}
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>
void OxenMQ::send(ConnectionID to, std::string_view cmd, const T&... opts) {
detail::send_control(get_control_socket(), "SEND",

View File

@ -411,6 +411,13 @@ void OxenMQ::proxy_loop() {
saved_umask = umask(STARTUP_UMASK);
#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++) {
if (!proxy_bind(bind[i], i)) {
LMQ_LOG(warn, "OxenMQ failed to listen on ", bind[i].address);

View File

@ -19,7 +19,7 @@ namespace {
[[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) {
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();
recv_message_parts(sock, parts);
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;
}
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 {
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);

@ -1 +1 @@
Subproject commit b3b07215d1ca2224aea6ff3e21d87ad0f7750df2
Subproject commit dba29b60d639bf8d206a9a12c223e6ed4284fb13

View File

@ -5,12 +5,22 @@
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();
/// 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() {
static uint16_t last = 4499;
static std::atomic<uint16_t> last = 25432;
last++;
assert(last); // We should never call this enough to overflow
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++) {
if (f())
break;
std::this_thread::sleep_for(10ms);
std::this_thread::sleep_for(10ms * TIME_DILATION);
}
auto lock = catch_lock();
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.
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 = "") {
std::string me = "tests/common.h";

View File

@ -1,4 +1,5 @@
#include "oxenmq/bt_serialize.h"
#include "oxenmq/bt_producer.h"
#include "common.h"
#include <map>
#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}};
REQUIRE( bt_serialize(m) == "li1ei2eli3ei4e2:hiel3:foo3:barei-4ee" );
}
TEST_CASE("bt allocation-free consumer", "[bt][dict][list][consumer]") {
// Consumer deserialization:
bt_list_consumer lc{"li1ei2eli3ei4e2:hiel3:foo3:barei-4ee"};
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)) );
}
#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";
bt_dict_consumer dc{zomg_};
for (int i = 0; i < 5; i++)
if (!dc.skip_until("b"))
throw std::runtime_error("Couldn't find b, but I know it's there!");
auto sublist = lp.append_list();
CHECK_THROWS_AS( lp.append(1), std::logic_error );
CHECK( sublist.view() == "le" );
CHECK( lp.view() == "l3:abci42ei1ei17ei-999elee" );
sublist.append(0);
auto dc1 = dc;
if (dc.skip_until("z")) {
auto v = dc.consume_integer<int>();
std::cout << " - " << v.first << ": " << v.second << "\n";
} 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";
}
auto sublist2{std::move(sublist)};
sublist2 += "";
CHECK( sublist2.view() == "li0e0:e" );
CHECK( lp.view() == "l3:abci42ei1ei17ei-999eli0e0:ee" );
}
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

View File

@ -470,9 +470,9 @@ TEST_CASE("deferred replies", "[commands][send][deferred]") {
std::string msg = m.data.empty() ? ""s : std::string{m.data.front()};
std::thread t{[send=m.send_later(), msg=std::move(msg)] {
{ 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"); }
send.reply(msg);
send(msg);
}};
t.detach();
});
@ -516,7 +516,7 @@ TEST_CASE("deferred replies", "[commands][send][deferred]") {
auto lock = catch_lock();
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};
auto lock = catch_lock();

View File

@ -279,7 +279,7 @@ TEST_CASE("SN disconnections", "[connect][disconnect]") {
lmq[2]->send(pubkey[1], "sn.hi");
lmq[1]->send(pubkey[0], "BYE");
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();
REQUIRE(his == 5);
@ -504,3 +504,107 @@ TEST_CASE("SN backchatter", "[connect][sn]") {
auto lock = catch_lock();
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 );
}
}

View File

@ -44,12 +44,32 @@ TEST_CASE("hex encoding/decoding", "[encoding][decoding][hex]") {
std::basic_string_view<std::byte> b{bytes.data(), bytes.size()};
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[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'};
std::basic_string_view<std::byte> hex_bytes{bytes.data(), bytes.size()};
REQUIRE( oxenmq::is_hex(hex_bytes) );
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]") {
@ -99,6 +119,16 @@ TEST_CASE("base32z encoding/decoding", "[encoding][decoding][base32z]") {
REQUIRE( pk_b32z_again == pk_b32z );
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::basic_string_view<std::byte> b{bytes.data(), bytes.size()};
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()};
REQUIRE( oxenmq::is_base32z(b32_bytes) );
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]") {
@ -125,6 +186,13 @@ TEST_CASE("base64 encoding/decoding", "[encoding][decoding][base64]") {
REQUIRE( oxenmq::to_base64("abcde") == "YWJjZGU=" );
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\xff"s) == "AAAA//8=" );
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_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=A") );
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_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::basic_string_view<std::byte> b{bytes.data(), bytes.size()};
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()};
REQUIRE( oxenmq::is_base64(b64_bytes) );
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]") {

View File

@ -15,9 +15,10 @@ TEST_CASE("timer test", "[timer][basic]") {
auto start = std::chrono::steady_clock::now();
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();
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.
auto timer = omq.add_timer([&] {
if (first.exchange(false)) {
std::this_thread::sleep_for(30ms);
std::this_thread::sleep_for(30ms * TIME_DILATION);
ticks++;
done = true;
} else if (!done) {
ticks++;
}
}, 5ms, true /* squelch */);
}, 5ms * TIME_DILATION, true /* squelch */);
omq.start();
wait_for([&] { return done.load(); });
@ -58,7 +59,7 @@ TEST_CASE("timer squelch", "[timer][squelch]") {
std::atomic<int> ticks2 = 0;
auto timer2 = omq.add_timer([&] {
if (first2.exchange(false)) {
std::this_thread::sleep_for(30ms);
std::this_thread::sleep_for(40ms * TIME_DILATION);
done2 = true;
} else if (!done2) {
ticks2++;
@ -82,13 +83,13 @@ TEST_CASE("timer cancel", "[timer][cancel]") {
std::atomic<int> ticks = 0;
// 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);
TimerID timer = omq.add_timer([&] {
if (++ticks == 3)
omq.cancel_timer(timer);
}, 5ms);
}, 5ms * TIME_DILATION);
omq.start();