feat: start libsession-util integration and experimentation

This commit is contained in:
0x330a 2022-11-28 18:08:01 +11:00
parent 1dbcffe40b
commit 4045333dbc
37 changed files with 4393 additions and 1 deletions

2
libsession-util/.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
/build
/.cxx/

View File

@ -0,0 +1,49 @@
plugins {
id 'com.android.library'
id 'org.jetbrains.kotlin.android'
}
android {
namespace 'network.loki.messenger.libsession_util'
compileSdkVersion androidCompileSdkVersion
defaultConfig {
minSdkVersion androidMinimumSdkVersion
targetSdkVersion androidCompileSdkVersion
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
externalNativeBuild {
cmake {
cppFlags "--std=c++17"
}
}
ndk {
abiFilters 'x86_64', 'arm64-v8a' // ,'x86', 'armeabi-v7a' TODO: remove after the native library works properly with targets
}
}
buildTypes {
release {
minifyEnabled false
}
}
externalNativeBuild {
cmake {
path "src/main/cpp/CMakeLists.txt"
version "3.18.1"
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = '1.8'
}
}
dependencies {
testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test.ext:junit:1.1.4'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.0'
}

View File

@ -0,0 +1,284 @@
#pragma once
#include <string>
#include <string_view>
#include <array>
#include <iterator>
#include <cassert>
#include "byte_type.h"
namespace oxenc {
namespace detail {
/// Compile-time generated lookup tables for base32z conversion. This is case insensitive (though
/// for byte -> b32z conversion we always produce lower case).
struct b32z_table {
// Store the 0-31 decoded value of every possible char; all the chars that aren't valid are set
// to 0. (If you don't trust your data, check it with is_base32z first, which uses these 0's
// to detect invalid characters -- which is why we want a full 256 element array).
char from_b32z_lut[256];
// Store the encoded character of every 0-31 (5 bit) value.
char to_b32z_lut[32];
// constexpr constructor that fills out the above (and should do it at compile time for any half
// decent compiler).
constexpr b32z_table() noexcept : from_b32z_lut{},
to_b32z_lut{
'y', 'b', 'n', 'd', 'r', 'f', 'g', '8', 'e', 'j', 'k', 'm', 'c', 'p', 'q', 'x',
'o', 't', '1', 'u', 'w', 'i', 's', 'z', 'a', '3', '4', '5', 'h', '7', '6', '9'
}
{
for (unsigned char c = 0; c < 32; c++) {
unsigned char x = to_b32z_lut[c];
from_b32z_lut[x] = c;
if (x >= 'a' && x <= 'z')
from_b32z_lut[x - 'a' + 'A'] = c;
}
}
// Convert a b32z encoded character into a 0-31 value
constexpr char from_b32z(unsigned char c) const noexcept { return from_b32z_lut[c]; }
// Convert a 0-31 value into a b32z encoded character
constexpr char to_b32z(unsigned char b) const noexcept { return to_b32z_lut[b]; }
} constexpr b32z_lut;
// This main point of this static assert is to force the compiler to compile-time build the constexpr tables.
static_assert(b32z_lut.from_b32z('w') == 20 && b32z_lut.from_b32z('T') == 17 && b32z_lut.to_b32z(5) == 'f', "");
} // namespace detail
/// 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⌋
/// 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)} {}
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 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; }
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>) {
using std::distance;
base32z.reserve(to_base32z_size(distance(begin, end)));
}
to_base32z(begin, end, std::back_inserter(base32z));
return base32z;
}
/// Creates a base32z string from an iterable, std::string-like object
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 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;
}
/// Returns true if all elements in the string-like value are base32z characters
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); }
/// 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.
///
/// 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();
}
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
/// are not valid (case-insensitive) base32z characters.
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>) {
using std::distance;
bytes.reserve(from_base32z_size(distance(begin, end)));
}
from_base32z(begin, end, std::back_inserter(bytes));
return bytes;
}
/// Converts base32z digits from a std::string-like object into a std::string of bytes. Undefined
/// behaviour if any characters are not valid (case-insensitive) base32z characters.
template <typename CharT>
std::string from_base32z(std::basic_string_view<CharT> s) { return from_base32z(s.begin(), s.end()); }
inline std::string from_base32z(std::string_view s) { return from_base32z<>(s); }
inline namespace literals {
inline std::string operator""_b32z(const char* x, size_t n) {
std::string_view in{x, n};
if (!is_base32z(in))
throw std::invalid_argument{"base32z literal is not base32z"};
return from_base32z(in);
}
}
}

View File

@ -0,0 +1,354 @@
#pragma once
#include <string>
#include <string_view>
#include <array>
#include <iterator>
#include <cassert>
#include "byte_type.h"
namespace oxenc {
namespace detail {
/// Compile-time generated lookup tables for base64 conversion.
struct b64_table {
// Store the 0-63 decoded value of every possible char; all the chars that aren't valid are set
// to 0. (If you don't trust your data, check it with is_base64 first, which uses these 0's
// to detect invalid characters -- which is why we want a full 256 element array).
char from_b64_lut[256];
// Store the encoded character of every 0-63 (6 bit) value.
char to_b64_lut[64];
// constexpr constructor that fills out the above (and should do it at compile time for any half
// decent compiler).
constexpr b64_table() noexcept : from_b64_lut{}, to_b64_lut{} {
for (unsigned char c = 0; c < 26; c++) {
from_b64_lut[(unsigned char)('A' + c)] = 0 + c;
to_b64_lut[ (unsigned char)( 0 + c)] = 'A' + c;
}
for (unsigned char c = 0; c < 26; c++) {
from_b64_lut[(unsigned char)('a' + c)] = 26 + c;
to_b64_lut[ (unsigned char)(26 + c)] = 'a' + c;
}
for (unsigned char c = 0; c < 10; c++) {
from_b64_lut[(unsigned char)('0' + c)] = 52 + c;
to_b64_lut[ (unsigned char)(52 + c)] = '0' + c;
}
to_b64_lut[62] = '+'; from_b64_lut[(unsigned char) '+'] = 62;
to_b64_lut[63] = '/'; from_b64_lut[(unsigned char) '/'] = 63;
}
// Convert a b64 encoded character into a 0-63 value
constexpr char from_b64(unsigned char c) const noexcept { return from_b64_lut[c]; }
// Convert a 0-31 value into a b64 encoded character
constexpr char to_b64(unsigned char b) const noexcept { return to_b64_lut[b]; }
} constexpr b64_lut;
// This main point of this static assert is to force the compiler to compile-time build the constexpr tables.
static_assert(b64_lut.from_b64('/') == 63 && b64_lut.from_b64('7') == 59 && b64_lut.to_b64(38) == 'm', "");
} // namespace detail
/// 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)
}
/// 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>) {
using std::distance;
base64.reserve(to_base64_size(distance(begin, end)));
}
to_base64(begin, end, std::back_inserter(base64));
return base64;
}
/// 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) {
auto last = prev(end);
if (static_cast<unsigned char>(*last) == '=')
end = last--;
if (static_cast<unsigned char>(*last) == '=')
end = last;
}
for (; begin != end; ++begin) {
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;
}
/// Returns true if the string-like value is a base64 encoded value
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 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)
/// bits for a 3- and 4-byte input, respectively. We ignore any such "impossible" bits, and
/// similarly ignore impossible bits in the bit "overhang"; that means "YWJjZA==" (the proper
/// 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>
OutputIt from_base64(InputIt begin, InputIt end, OutputIt out) {
static_assert(sizeof(decltype(*begin)) == 1, "from_base64 requires chars/bytes");
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.
/// Undefined behaviour if any characters are not valid base64 characters.
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>) {
using std::distance;
bytes.reserve(from_base64_size(distance(begin, end)));
}
from_base64(begin, end, std::back_inserter(bytes));
return bytes;
}
/// Converts base64 digits from a std::string-like object into a std::string of bytes. Undefined
/// behaviour if any characters are not valid base64 characters.
template <typename CharT>
std::string from_base64(std::basic_string_view<CharT> s) { return from_base64(s.begin(), s.end()); }
inline std::string from_base64(std::string_view s) { return from_base64<>(s); }
inline namespace literals {
inline std::string operator""_b64(const char* x, size_t n) {
std::string_view in{x, n};
if (!is_base64(in))
throw std::invalid_argument{"base64 literal is not base64"};
return from_base64(in);
}
}
}

View File

@ -0,0 +1,5 @@
#pragma once
#include "bt_value.h"
#include "bt_serialize.h"
#include "bt_producer.h"
#include "bt_value_producer.h"

View File

@ -0,0 +1,445 @@
#pragma once
#include <cassert>
#include <charconv>
#include <stdexcept>
#include <string_view>
#include <unordered_map>
#include <utility>
#include "variant.h"
namespace oxenc {
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 OXENC_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) <= 8);
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);
// Common constructor for both list and dict producer
bt_list_producer(char* begin, char* end, std::string_view prefix);
// 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 OXENC_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) {
char buf[21]{}; // length + ':'
auto *ptr = write_integer(s.size(), buf);
*ptr++ = ':';
buffer_append({buf, static_cast<size_t>(ptr - buf)});
buffer_append(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);
/// 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) : bt_list_producer{begin, end, "l"sv} {}
/// 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, "l"sv} {}
~bt_list_producer();
/// 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)
{
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& 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();
/// Appends a bt_value, bt_dict, or bt_list to this bt_list. You must include the
/// bt_value_producer.h header (either directly or via bt.h) to use this method.
template <typename T>
void append_bt(const T& bt);
};
/// 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_list_producer{parent, "d"sv} {}
bt_dict_producer(bt_dict_producer* parent) : bt_list_producer{parent, "d"sv} {}
// 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) {
std::string_view this_key{buffer.first - size, size};
assert(!last_key.data() || this_key > last_key);
last_key = this_key;
}
#endif
public:
/// Constructs a dict 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 `de` encoding of an
/// empty list.
bt_dict_producer(char* begin, char* end) : bt_list_producer{begin, end, "d"sv} {}
/// 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_dict_producer(char* begin, size_t len) : bt_list_producer{begin, begin + len, "d"sv} {}
/// 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(); }
/// Returns the end position in the buffer.
const char* end() const { return bt_list_producer::end(); }
/// 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) {
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};
}
/// 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)
{
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};
}
/// Appends a bt_value, bt_dict, or bt_list to this bt_dict. You must include the
/// bt_value_producer.h header (either directly or via bt.h) to use this method.
template <typename T>
void append_bt(std::string_view key, const T& bt);
};
inline 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();
}
inline 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();
}
inline 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);
}
inline 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);
}
inline void bt_list_producer::append_intermediate_ends(size_t count) {
static constexpr std::string_view eee = "eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee"sv;
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);
}
inline 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);
}
inline bt_list_producer::bt_list_producer(char* begin, char* end, std::string_view prefix)
: data{buf_span{begin, end}}, buffer{*std::get_if<buf_span>(&data)}, from{buffer.first} {
buffer_append(prefix);
append_intermediate_ends();
}
inline 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};
}
inline 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};
}
} // namespace oxenc

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,83 @@
#pragma once
// This header is here to provide just the basic bt_value/bt_dict/bt_list definitions without
// needing to include the full bt_serialize.h header.
#include <map>
#include <list>
#include <cstdint>
#include <variant>
#include <string>
#include <string_view>
namespace oxenc {
struct bt_value;
/// The type used to store dictionaries inside bt_value.
using bt_dict = std::map<std::string, bt_value>; // NB: unordered_map doesn't work because it can't be used with a predeclared type
/// The type used to store list items inside bt_value.
using bt_list = std::list<bt_value>;
/// The basic variant that can hold anything (recursively).
using bt_variant = std::variant<
std::string,
std::string_view,
int64_t,
uint64_t,
bt_list,
bt_dict
>;
#ifdef __cpp_lib_remove_cvref // C++20
using std::remove_cvref_t;
#else
template <typename T>
using remove_cvref_t = std::remove_cv_t<std::remove_reference_t<T>>;
#endif
template <typename T, typename Variant>
struct has_alternative;
template <typename T, typename... V>
struct has_alternative<T, std::variant<V...>> : std::bool_constant<(std::is_same_v<T, V> || ...)> {};
template <typename T, typename Variant>
constexpr bool has_alternative_v = has_alternative<T, Variant>::value;
namespace detail {
template <typename Tuple, size_t... Is>
bt_list tuple_to_list(const Tuple& tuple, std::index_sequence<Is...>) {
return {{bt_value{std::get<Is>(tuple)}...}};
}
template <typename T> constexpr bool is_tuple = false;
template <typename... T> constexpr bool is_tuple<std::tuple<T...>> = true;
template <typename S, typename T> constexpr bool is_tuple<std::pair<S, T>> = true;
}
/// Recursive generic type that can fully represent everything valid for a BT serialization.
/// This is basically just an empty wrapper around the std::variant, except we add some extra
/// converting constructors:
/// - integer constructors so that any unsigned value goes to the uint64_t and any signed value goes
/// to the int64_t.
/// - std::tuple and std::pair constructors that build a bt_list out of the tuple/pair elements.
struct bt_value : bt_variant {
using bt_variant::bt_variant;
using bt_variant::operator=;
template <typename T, typename U = std::remove_reference_t<T>, std::enable_if_t<std::is_integral_v<U> && std::is_unsigned_v<U>, int> = 0>
bt_value(T&& uint) : bt_variant{static_cast<uint64_t>(uint)} {}
template <typename T, typename U = std::remove_reference_t<T>, std::enable_if_t<std::is_integral_v<U> && std::is_signed_v<U>, int> = 0>
bt_value(T&& sint) : bt_variant{static_cast<int64_t>(sint)} {}
template <typename... T>
bt_value(const std::tuple<T...>& tuple) : bt_variant{detail::tuple_to_list(tuple, std::index_sequence_for<T...>{})} {}
template <typename S, typename T>
bt_value(const std::pair<S, T>& pair) : bt_variant{detail::tuple_to_list(pair, std::index_sequence_for<S, T>{})} {}
template <typename T, typename U = std::remove_reference_t<T>, std::enable_if_t<!std::is_integral_v<U> && !detail::is_tuple<U>, int> = 0>
bt_value(T&& v) : bt_variant{std::forward<T>(v)} {}
bt_value(const char* s) : bt_value{std::string_view{s}} {}
};
}

View File

@ -0,0 +1,99 @@
#include "bt_producer.h"
#include "bt_value.h"
#include "variant.h"
#include <type_traits>
/// This header provides the implementations of append_bt(bt_value/bt_list/bt_dict) for
/// bt_serialize. (It is optional to avoid unnecessary includes when not wanted).
namespace oxenc {
namespace detail {
void serialize_list(bt_list_producer& out, const bt_list& l);
void serialize_dict(bt_dict_producer& out, const bt_dict& l);
struct dict_appender {
bt_dict_producer& out;
std::string_view key;
dict_appender(bt_dict_producer& out, std::string_view key)
: out{out}, key{key} {}
void operator()(const bt_dict& d) {
auto subdict = out.append_dict(key);
serialize_dict(subdict, d);
}
void operator()(const bt_list& l) {
auto sublist = out.append_list(key);
serialize_list(sublist, l);
}
template <typename T>
void operator()(const T& other) {
out.append(key, other);
}
};
struct list_appender {
bt_list_producer& out;
explicit list_appender(bt_list_producer& out)
: out{out} {}
void operator()(const bt_dict& d) {
auto subdict = out.append_dict();
serialize_dict(subdict, d);
}
void operator()(const bt_list& l) {
auto sublist = out.append_list();
serialize_list(sublist, l);
}
template <typename T>
void operator()(const T& other) {
out.append(other);
}
};
inline void serialize_dict(bt_dict_producer& out, const bt_dict& d) {
for (const auto& [k, v]: d)
var::visit(dict_appender{out, k}, static_cast<const bt_variant&>(v));
}
inline void serialize_list(bt_list_producer& out, const bt_list& l) {
for (auto& val : l)
var::visit(list_appender{out}, static_cast<const bt_variant&>(val));
}
}
template <>
inline void bt_list_producer::append_bt(const bt_dict& bt) {
auto subdict = append_dict();
detail::serialize_dict(subdict, bt);
}
template <>
inline void bt_list_producer::append_bt(const bt_list& bt) {
auto sublist = append_list();
detail::serialize_list(sublist, bt);
}
template <>
inline void bt_list_producer::append_bt(const bt_value& bt) {
var::visit(detail::list_appender{*this}, static_cast<const bt_variant&>(bt));
}
template <>
inline void bt_dict_producer::append_bt(std::string_view key, const bt_dict& bt) {
auto subdict = append_dict(key);
detail::serialize_dict(subdict, bt);
}
template <>
inline void bt_dict_producer::append_bt(std::string_view key, const bt_list& bt) {
auto sublist = append_list(key);
detail::serialize_list(sublist, bt);
}
template <>
inline void bt_dict_producer::append_bt(std::string_view key, const bt_value& bt) {
var::visit(detail::dict_appender{*this, key}, static_cast<const bt_variant&>(bt));
}
}

View File

@ -0,0 +1,28 @@
#pragma once
// Specializations for assigning from a char into an output iterator, used by hex/base32z/base64
// decoding to bytes.
#include <iterator>
#include <type_traits>
namespace oxenc::detail {
// Fallback - we just try a char
template <typename OutputIt, typename = void>
struct byte_type { using type = char; };
// Support for things like std::back_inserter:
template <typename OutputIt>
struct byte_type<OutputIt, std::void_t<typename OutputIt::container_type>> {
using type = typename OutputIt::container_type::value_type; };
// iterator, raw pointers:
template <typename OutputIt>
struct byte_type<OutputIt, std::enable_if_t<std::is_reference_v<typename std::iterator_traits<OutputIt>::reference>>> {
using type = std::remove_reference_t<typename std::iterator_traits<OutputIt>::reference>; };
template <typename OutputIt>
using byte_type_t = typename byte_type<OutputIt>::type;
}

View File

@ -0,0 +1,204 @@
#pragma once
#include <cstdint>
#include <cstring>
#include <type_traits>
#if defined(_MSC_VER) && (!defined(__clang__) || defined(__c2__))
# include <cstdlib>
# define bswap_16(x) _byteswap_ushort(x)
# define bswap_32(x) _byteswap_ulong(x)
# define bswap_64(x) _byteswap_uint64(x)
#elif defined(__clang__) || defined(__GNUC__)
# define bswap_16(x) __builtin_bswap16(x)
# define bswap_32(x) __builtin_bswap32(x)
# define bswap_64(x) __builtin_bswap64(x)
#elif defined(__linux__)
extern "C" {
# include <byteswap.h>
} // extern "C"
#else
# error "Don't know how to byteswap on this platform!"
#endif
namespace oxenc {
/// True if this is a little-endian platform
inline constexpr bool little_endian =
#if defined(__LITTLE_ENDIAN__)
true
#elif defined(__BIG_ENDIAN__)
false
#elif defined(__BYTE_ORDER) && defined(__LITTLE_ENDIAN) && __BYTE_ORDER == __LITTLE_ENDIAN
true
#elif defined(__BYTE_ORDER__) && defined(__ORDER_LITTLE_ENDIAN__) && __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__
true
#elif defined(__BYTE_ORDER) && defined(__BIG_ENDIAN) && __BYTE_ORDER == __BIG_ENDIAN
false
#elif defined(__BYTE_ORDER__) && defined(__ORDER_BIG_ENDIAN__) && __BYTE_ORDER__ == __ORDER_BIG_ENDIAN__
false
#elif defined(_WIN32)
true
#else
# error "Error: don't know which endian this is"
#endif
;
/// True if this is a big-endian platform
inline constexpr bool big_endian = !little_endian;
/// True if the type is integral and of a size we support swapping. (We also allow size-1
/// values to be passed here for completeness, though nothing is ever swapped for such a value).
template <typename T> constexpr bool is_endian_swappable =
std::is_integral_v<T> && (sizeof(T) == 1 || sizeof(T) == 2 || sizeof(T) == 4 || sizeof(T) == 8);
/// Byte swaps an integer value unconditionally. You usually want to use one of the other
/// endian-aware functions rather than this.
template <typename T, typename = std::enable_if_t<is_endian_swappable<T>>>
void byteswap_inplace(T& val) {
if constexpr (sizeof(T) == 2)
val = bswap_16(val);
else if constexpr (sizeof(T) == 4)
val = bswap_32(val);
else if constexpr (sizeof(T) == 8)
val = bswap_64(val);
}
/// Converts a host-order integer value into a little-endian value, mutating it. Does nothing
/// on little-endian platforms.
template <typename T, typename = std::enable_if_t<is_endian_swappable<T>>>
void host_to_little_inplace(T& val) {
if constexpr (!little_endian)
byteswap_inplace(val);
}
/// Converts a host-order integer value into a little-endian value, returning it. Does no
/// converstion on little-endian platforms.
template <typename T, typename = std::enable_if_t<is_endian_swappable<T>>>
T host_to_little(T val) {
host_to_little_inplace(val);
return val;
}
/// Converts a little-endian integer value into a host-order (native) integer value, mutating
/// it. Does nothing on little-endian platforms.
template <typename T, typename = std::enable_if_t<is_endian_swappable<T>>>
void little_to_host_inplace(T& val) {
if constexpr (!little_endian)
byteswap_inplace(val);
}
/// Converts a little-order integer value into a host-order (native) integer value, returning
/// it. Does no conversion on little-endian platforms.
template <typename T, typename = std::enable_if_t<is_endian_swappable<T>>>
T little_to_host(T val) {
little_to_host_inplace(val);
return val;
}
/// Converts a host-order integer value into a big-endian value, mutating it. Does nothing on
/// big-endian platforms.
template <typename T, typename = std::enable_if_t<is_endian_swappable<T>>>
void host_to_big_inplace(T& val) {
if constexpr (!big_endian)
byteswap_inplace(val);
}
/// Converts a host-order integer value into a big-endian value, returning it. Does no
/// conversion on big-endian platforms.
template <typename T, typename = std::enable_if_t<is_endian_swappable<T>>>
T host_to_big(T val) {
host_to_big_inplace(val);
return val;
}
/// Converts a big-endian value into a host-order (native) integer value, mutating it. Does
/// nothing on big-endian platforms.
template <typename T, typename = std::enable_if_t<is_endian_swappable<T>>>
void big_to_host_inplace(T& val) {
if constexpr (!big_endian)
byteswap_inplace(val);
}
/// Converts a big-order integer value into a host-order (native) integer value, returning it.
/// Does no conversion on big-endian platforms.
template <typename T, typename = std::enable_if_t<is_endian_swappable<T>>>
T big_to_host(T val) {
big_to_host_inplace(val);
return val;
}
/// Loads a host-order integer value from a memory location containing little-endian bytes.
/// (There is no alignment requirement on the given pointer address).
template <typename T, typename = std::enable_if_t<is_endian_swappable<T>>>
T load_little_to_host(const void* from) {
T val;
std::memcpy(&val, from, sizeof(T));
little_to_host_inplace(val);
return val;
}
/// Loads a little-endian integer value from a memory location containing host order bytes.
/// (There is no alignment requirement on the given pointer address).
template <typename T, typename = std::enable_if_t<is_endian_swappable<T>>>
T load_host_to_little(const void* from) {
T val;
std::memcpy(&val, from, sizeof(T));
host_to_little_inplace(val);
return val;
}
/// Loads a host-order integer value from a memory location containing big-endian bytes. (There
/// is no alignment requirement on the given pointer address).
template <typename T, typename = std::enable_if_t<is_endian_swappable<T>>>
T load_big_to_host(const void* from) {
T val;
std::memcpy(&val, from, sizeof(T));
big_to_host_inplace(val);
return val;
}
/// Loads a big-endian integer value from a memory location containing host order bytes. (There
/// is no alignment requirement on the given pointer address).
template <typename T, typename = std::enable_if_t<is_endian_swappable<T>>>
T load_host_to_big(const void* from) {
T val;
std::memcpy(&val, from, sizeof(T));
host_to_big_inplace(val);
return val;
}
/// Writes a little-endian integer value into the given memory location, copying and converting
/// it (if necessary) from the given host-order integer value.
template <typename T, typename = std::enable_if_t<is_endian_swappable<T>>>
void write_host_as_little(T val, void* to) {
host_to_little_inplace(val);
std::memcpy(to, &val, sizeof(T));
}
/// Writes a big-endian integer value into the given memory location, copying and converting it
/// (if necessary) from the given host-order integer value.
template <typename T, typename = std::enable_if_t<is_endian_swappable<T>>>
void write_host_as_big(T val, void* to) {
host_to_big_inplace(val);
std::memcpy(to, &val, sizeof(T));
}
/// Writes a host-order integer value into the given memory location, copying and converting it
/// (if necessary) from the given little-endian integer value.
template <typename T, typename = std::enable_if_t<is_endian_swappable<T>>>
void write_little_as_host(T val, void* to) {
little_to_host_inplace(val);
std::memcpy(to, &val, sizeof(T));
}
/// Writes a host-order integer value into the given memory location, copying and converting it
/// (if necessary) from the given big-endian integer value.
template <typename T, typename = std::enable_if_t<is_endian_swappable<T>>>
void write_big_as_host(T val, void* to) {
big_to_host_inplace(val);
std::memcpy(to, &val, sizeof(T));
}
} // namespace oxenc

View File

@ -0,0 +1,233 @@
#pragma once
#include <string>
#include <string_view>
#include <array>
#include <iterator>
#include <cassert>
#include "byte_type.h"
namespace oxenc {
namespace detail {
/// Compile-time generated lookup tables hex conversion
struct hex_table {
char from_hex_lut[256];
char to_hex_lut[16];
constexpr hex_table() noexcept : from_hex_lut{}, to_hex_lut{} {
for (unsigned char c = 0; c < 10; c++) {
from_hex_lut[(unsigned char)('0' + c)] = 0 + c;
to_hex_lut[ (unsigned char)( 0 + c)] = '0' + c;
}
for (unsigned char c = 0; c < 6; c++) {
from_hex_lut[(unsigned char)('a' + c)] = 10 + c;
from_hex_lut[(unsigned char)('A' + c)] = 10 + c;
to_hex_lut[ (unsigned char)(10 + c)] = 'a' + c;
}
}
constexpr char from_hex(unsigned char c) const noexcept { return from_hex_lut[c]; }
constexpr char to_hex(unsigned char b) const noexcept { return to_hex_lut[b]; }
} constexpr hex_lut;
// This main point of this static assert is to force the compiler to compile-time build the constexpr tables.
static_assert(hex_lut.from_hex('a') == 10 && hex_lut.from_hex('F') == 15 && hex_lut.to_hex(13) == 'd', "");
} // namespace detail
/// 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>) {
using std::distance;
hex.reserve(to_hex_size(distance(begin, end)));
}
to_hex(begin, end, std::back_inserter(hex));
return hex;
}
/// Creates a hex string from an iterable, std::string-like object
template <typename CharT>
std::string to_hex(std::basic_string_view<CharT> s) { return to_hex(s.begin(), s.end()); }
inline std::string to_hex(std::string_view s) { return to_hex<>(s); }
/// Returns true if the given value is a valid hex digit.
template <typename CharT>
constexpr bool is_hex_digit(CharT c) {
static_assert(sizeof(CharT) == 1, "is_hex requires chars/bytes");
return detail::hex_lut.from_hex(static_cast<unsigned char>(c)) != 0 || static_cast<unsigned char>(c) == '0';
}
/// Returns true if all elements in the range are hex characters *and* the string length is a
/// multiple of 2, and thus suitable to pass to from_hex().
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) {
using std::distance;
if (distance(begin, end) % 2 != 0)
return false;
}
size_t count = 0;
for (; begin != end; ++begin) {
if constexpr (!ra) ++count;
if (!is_hex_digit(*begin))
return false;
}
if constexpr (!ra)
return count % 2 == 0;
return true;
}
/// Returns true if all elements in the string-like value are hex characters
template <typename CharT>
constexpr bool is_hex(std::basic_string_view<CharT> s) { return is_hex(s.begin(), s.end()); }
constexpr bool is_hex(std::string_view s) { return is_hex(s.begin(), s.end()); }
/// Convert a hex digit into its numeric (0-15) value
constexpr char from_hex_digit(unsigned char x) noexcept {
return detail::hex_lut.from_hex(x);
}
/// 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 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>
OutputIt from_hex(InputIt begin, InputIt end, OutputIt out) {
assert(is_hex(begin, end));
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
/// the input sequence is not an even-length sequence of [0-9a-fA-F] characters.
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>) {
using std::distance;
bytes.reserve(from_hex_size(distance(begin, end)));
}
from_hex(begin, end, std::back_inserter(bytes));
return bytes;
}
/// Converts hex digits from a std::string-like object into a std::string of bytes. Undefined
/// behaviour if any characters are not in [0-9a-fA-F] or if the input sequence length is not even.
template <typename CharT>
std::string from_hex(std::basic_string_view<CharT> s) { return from_hex(s.begin(), s.end()); }
inline std::string from_hex(std::string_view s) { return from_hex<>(s); }
inline namespace literals {
inline std::string operator""_hex(const char* x, size_t n) {
std::string_view in{x, n};
if (!is_hex(in))
throw std::invalid_argument{"hex literal is not hex"};
return from_hex(in);
}
}
}

View File

@ -0,0 +1,103 @@
#pragma once
// Workarounds for macos compatibility. On macOS we aren't allowed to touch anything in
// std::variant that could throw if compiling with a target <10.14 because Apple fails hard at
// properly updating their STL. Thus, if compiling in such a mode, we have to introduce
// workarounds.
//
// This header defines a `var` namespace with `var::get` and `var::visit` implementations. On
// everything except broken backwards macos, this is just an alias to `std`. On broken backwards
// macos, we provide implementations that throw std::runtime_error in failure cases since the
// std::bad_variant_access exception can't be touched.
//
// You also get a BROKEN_APPLE_VARIANT macro defined if targetting a problematic mac architecture.
#include <variant>
#ifdef __APPLE__
# include <AvailabilityVersions.h>
# if defined(__APPLE__) && MAC_OS_X_VERSION_MIN_REQUIRED < MAC_OS_X_VERSION_10_14
# define BROKEN_APPLE_VARIANT
# endif
#endif
#ifndef BROKEN_APPLE_VARIANT
namespace var = std; // Oh look, actual C++17 support
#else
// Oh look, apple.
namespace var {
// Apple won't let us use std::visit or std::get if targetting some version of macos earlier than
// 10.14 because Apple is awful about not updating their STL. So we have to provide our own, and
// then call these without `std::` -- on crappy macos we'll come here, on everything else we'll ADL
// to the std:: implementation.
template <typename T, typename... Types>
constexpr T& get(std::variant<Types...>& var) {
if (auto* v = std::get_if<T>(&var)) return *v;
throw std::runtime_error{"Bad variant access -- variant does not contain the requested type"};
}
template <typename T, typename... Types>
constexpr const T& get(const std::variant<Types...>& var) {
if (auto* v = std::get_if<T>(&var)) return *v;
throw std::runtime_error{"Bad variant access -- variant does not contain the requested type"};
}
template <typename T, typename... Types>
constexpr const T&& get(const std::variant<Types...>&& var) {
if (auto* v = std::get_if<T>(&var)) return std::move(*v);
throw std::runtime_error{"Bad variant access -- variant does not contain the requested type"};
}
template <typename T, typename... Types>
constexpr T&& get(std::variant<Types...>&& var) {
if (auto* v = std::get_if<T>(&var)) return std::move(*v);
throw std::runtime_error{"Bad variant access -- variant does not contain the requested type"};
}
template <size_t I, typename... Types>
constexpr auto& get(std::variant<Types...>& var) {
if (auto* v = std::get_if<I>(&var)) return *v;
throw std::runtime_error{"Bad variant access -- variant does not contain the requested type"};
}
template <size_t I, typename... Types>
constexpr const auto& get(const std::variant<Types...>& var) {
if (auto* v = std::get_if<I>(&var)) return *v;
throw std::runtime_error{"Bad variant access -- variant does not contain the requested type"};
}
template <size_t I, typename... Types>
constexpr const auto&& get(const std::variant<Types...>&& var) {
if (auto* v = std::get_if<I>(&var)) return std::move(*v);
throw std::runtime_error{"Bad variant access -- variant does not contain the requested type"};
}
template <size_t I, typename... Types>
constexpr auto&& get(std::variant<Types...>&& var) {
if (auto* v = std::get_if<I>(&var)) return std::move(*v);
throw std::runtime_error{"Bad variant access -- variant does not contain the requested type"};
}
template <size_t I, size_t... More, class Visitor, class Variant>
constexpr auto visit_helper(Visitor&& vis, Variant&& var) {
if (var.index() == I)
return vis(var::get<I>(std::forward<Variant>(var)));
else if constexpr (sizeof...(More) > 0)
return visit_helper<More...>(std::forward<Visitor>(vis), std::forward<Variant>(var));
else
throw std::runtime_error{"Bad visit -- variant is valueless"};
}
template <size_t... Is, class Visitor, class Variant>
constexpr auto visit_helper(Visitor&& vis, Variant&& var, std::index_sequence<Is...>) {
return visit_helper<Is...>(std::forward<Visitor>(vis), std::forward<Variant>(var));
}
// Only handle a single variant here because multi-variant invocation is notably harder (and we
// don't need it).
template <class Visitor, class Variant>
constexpr auto visit(Visitor&& vis, Variant&& var) {
return visit_helper(std::forward<Visitor>(vis), std::forward<Variant>(var),
std::make_index_sequence<std::variant_size_v<std::remove_reference_t<Variant>>>{});
}
} // namespace var
#endif

View File

@ -0,0 +1,5 @@
namespace oxenc {
constexpr int VERSION_MAJOR = 1;
constexpr int VERSION_MINOR = 0;
constexpr int VERSION_PATCH = 6;
}

View File

@ -0,0 +1,58 @@
#pragma once
#include <oxenc/bt_value.h>
#include <cassert>
#ifndef NDEBUG
#include <algorithm>
#endif
namespace session::bt {
using oxenc::bt_dict;
using oxenc::bt_list;
/// Merges two bt dicts together: the returned dict includes all keys in a or b. Keys in *both*
/// dicts get their value from `a`, otherwise the value is that of the dict that contains the key.
bt_dict merge(const bt_dict& a, const bt_dict& b);
/// Merges two ordered bt_lists together using a predicate to determine order. The input lists must
/// be sorted to begin with. `cmp` must be callable with a pair of `const bt_value&` arguments and
/// must return true if the first argument should be considered less than the second argument. By
/// default this skips elements from b that compare equal to a value of a, but you can include all
/// the duplicates by specifying the `duplicates` parameter as true.
template <typename Compare>
bt_list merge_sorted(const bt_list& a, const bt_list& b, Compare cmp, bool duplicates = false) {
bt_list result;
auto it_a = a.begin();
auto it_b = b.begin();
assert(std::is_sorted(it_a, a.end(), cmp));
assert(std::is_sorted(it_b, b.end(), cmp));
if (duplicates) {
while (it_a != a.end() && it_b != b.end()) {
if (!cmp(*it_a, *it_b)) // *b <= *a
result.push_back(*it_b++);
else // *a < *b
result.push_back(*it_a++);
}
} else {
while (it_a != a.end() && it_b != b.end()) {
if (cmp(*it_b, *it_a)) // *b < *a
result.push_back(*it_b++);
else if (cmp(*it_a, *it_b)) // *a < *b
result.push_back(*it_a++);
else // *a == *b
++it_b; // skip it
}
}
if (it_a != a.end())
result.insert(result.end(), it_a, a.end());
else if (it_b != b.end())
result.insert(result.end(), it_b, b.end());
return result;
}
} // namespace session::bt

View File

@ -0,0 +1,369 @@
#pragma once
#include <oxenc/bt_serialize.h>
#include <oxenc/bt_value.h>
#include <array>
#include <cassert>
#include <optional>
#include <set>
#include <stdexcept>
#include <variant>
#include <vector>
namespace session::config {
inline constexpr int MAX_MESSAGE_SIZE = 76800; // 76.8kB = Storage server's limit
// Application data data types:
using scalar = std::variant<int64_t, std::string>;
using set = std::set<scalar>;
struct dict_value;
using dict = std::map<std::string, dict_value>;
using dict_variant = std::variant<dict, set, scalar>;
struct dict_value : dict_variant {
using dict_variant::dict_variant;
using dict_variant::operator=;
};
// Helpers for gcc-10 and earlier which don't like visiting a std::variant subtype:
constexpr inline dict_variant& unwrap(dict_value& v) {
return static_cast<dict_variant&>(v);
}
constexpr inline const dict_variant& unwrap(const dict_value& v) {
return static_cast<const dict_variant&>(v);
}
using seqno_t = std::int64_t;
using hash_t = std::array<unsigned char, 32>;
using seqno_hash_t = std::pair<seqno_t, hash_t>;
using ustring = std::basic_string<unsigned char>;
using ustring_view = std::basic_string_view<unsigned char>;
class MutableConfigMessage;
/// Base type for all errors that can happen during config parsing
struct config_error : std::runtime_error {
using std::runtime_error::runtime_error;
};
/// Type thrown for bad signatures (bad or missing signature).
struct signature_error : config_error {
using config_error::config_error;
};
/// Type thrown for a missing signature when a signature is required.
struct missing_signature : signature_error {
using signature_error::signature_error;
};
/// Type thrown for an unparseable config (e.g. keys with invalid types, or keys before "#" or after
/// "~").
struct config_parse_error : config_error {
using config_error::config_error;
};
/// Class for a parsed, read-only config message; also serves as the base class of a
/// MutableConfigMessage which allows setting values.
class ConfigMessage {
public:
using lagged_diffs_t = std::map<seqno_hash_t, oxenc::bt_dict>;
#ifndef SESSION_TESTING_EXPOSE_INTERNALS
protected:
#endif
dict data_;
// diff data for *this* message, parsed during construction. Subclasses may use this for
// managing their own diff in the `diff()` method.
oxenc::bt_dict diff_;
// diffs of previous messages that are included in this message.
lagged_diffs_t lagged_diffs_;
// Unknown top-level config keys which we preserve even though we don't understand what they
// mean.
oxenc::bt_dict unknown_;
/// Seqno and hash of the message; we calculate this when loading. Subclasses put the hash here
/// (so that they can return a reference to it).
seqno_hash_t seqno_hash_{0, {0}};
bool verified_signature_ = false;
bool merged_ = false;
public:
constexpr static int DEFAULT_DIFF_LAGS = 5;
/// Verification function: this is passed the data that should have been signed and the 64-byte
/// signature. Should return true to accept the signature, false to reject it and skip the
/// message. It can also throw to abort message construction (that is: returning false skips
/// the message when loading multiple messages, but can still continue with other messages;
/// throwing aborts the entire construction).
using verify_callable = std::function<bool(ustring_view data, ustring_view signature)>;
/// Signing function: this is passed the data to be signed and returns the 64-byte signature.
using sign_callable = std::function<std::string(ustring_view data)>;
ConfigMessage();
ConfigMessage(const ConfigMessage&) = default;
ConfigMessage& operator=(const ConfigMessage&) = default;
ConfigMessage(ConfigMessage&&) = default;
ConfigMessage& operator=(ConfigMessage&&) = default;
virtual ~ConfigMessage() = default;
/// Initializes a config message by parsing a serialized message. Throws on any error. See the
/// vector version below for argument descriptions.
explicit ConfigMessage(
std::string_view serialized,
verify_callable verifier = nullptr,
sign_callable signer = nullptr,
int lag = DEFAULT_DIFF_LAGS,
bool signature_optional = false);
/// Constructs a new ConfigMessage by loading and potentially merging multiple serialized
/// ConfigMessages together, according to the config conflict resolution rules. The result
/// of this call can either be one of the config messages directly (if one is found that
/// includes all the others), or can be a new config message that merges multiple configs
/// together. You can check `.merged()` to see which happened.
///
/// This constructor always requires at least one valid config from the given inputs; if all are
/// empty,
///
/// verifier - a signature verification function. If provided and not nullptr this will be
/// called to verify each signature in the provided messages: any that are missing a signature
/// or for which the verifier returns false will be dropped from consideration for merging. If
/// *all* messages fail verification an exception is raised.
///
/// signer - a signature generation function. This is not used directly by the ConfigMessage,
/// but providing it will allow it to be passed automatically to any MutableConfigMessage
/// derived from this ConfigMessage.
///
/// lag - the lag setting controlling the config merging rules. Any config message with lagged
/// diffs that exceeding this lag value will have those early lagged diffs dropping during
/// loading.
///
/// signature_optional - if true then accept a message with no signature even when a verifier is
/// set, thus allowing unsigned messages (though messages with an invalid signature are still
/// not allowed). This option is ignored when verifier is not set.
///
/// error_callback - if set then any config message parsing error will be passed to this
/// function for handling: the callback typically warns and, if the overall construction should
/// abort, rethrows the error. If this function is omitted then the default skips (without
/// failing) individual parse errors and only aborts construction if *all* messages fail to
/// parse. A simple handler such as `[](const auto& e) { throw e; }` can be used to make any
/// parse error of any message fatal.
explicit ConfigMessage(
const std::vector<std::string_view>& configs,
verify_callable verifier = nullptr,
sign_callable signer = nullptr,
int lag = DEFAULT_DIFF_LAGS,
bool signature_optional = false,
std::function<void(const config_error&)> error_handler = nullptr);
/// Returns a read-only reference to the contained data. (To get a mutable config object use
/// MutableConfigMessage).
const dict& data() const { return data_; }
/// The verify function; if loading a message with a signature and this is set then it will
/// be called to verify the signature of the message. Takes a pointer to the signing data,
/// the data length, and a pointer to the 64-byte signature.
verify_callable verifier;
/// The signing function; this is not directly used by the non-mutable base class, but will be
/// propagated to mutable config messages that are derived e.g. by calling `.increment()`. This
/// is called when serializing a config message to add a signature. If it is nullptr then no
/// signature is added to the serialized data.
sign_callable signer;
/// How many lagged config diffs that should be carried forward to resolve conflicts,
/// including this message. If 0 then config messages won't have any diffs and will not be
/// mergeable.
int lag = DEFAULT_DIFF_LAGS;
/// The diff structure for changes in *this* config message. Subclasses that need to override
/// should populate into `diff_` and return a reference to it (internal code assumes `diff_` is
/// correct immediately after a call to this).
virtual const oxenc::bt_dict& diff();
/// Returns the seqno of this message
const seqno_t& seqno() const { return seqno_hash_.first; }
/// Calculates the hash of the current message. For a ConfigMessage this is calculated when the
/// message is first loaded; for a MutableConfigMessage this serializes the current value to
/// properly compute the current hash. Subclasses must ensure that seqno_hash_.second is set to
/// the correct value when this is called (and typically return a reference to it).
virtual const hash_t& hash() { return seqno_hash_.second; }
/// After loading multiple config files this flag indicates whether or not we had to produce a
/// new, merged configuration message (true) or did not need to merge (false). (For config
/// messages that were not loaded from serialized data this is always true).
bool merged() const { return merged_; }
/// Returns true if this message contained a valid, verified signature when it was parsed.
/// Returns false otherwise (e.g. not loaded from verification at all; loaded without a
/// verification function; or had no signature and a signature wasn't required).
bool verified_signature() const { return verified_signature_; }
/// Constructs a new MutableConfigMessage from this config message with an incremented seqno.
/// The new config message's diff will reflect changes made after this construction.
virtual MutableConfigMessage increment() const;
/// Serializes this config's data. Note that if the ConfigMessage was constructed from signed,
/// serialized input, this will only produce an exact copy of the original serialized input if
/// it uses the identical, deterministic signing function used to construct the original.
///
/// The optional `enable_signing` argument can be specified as false to disable signing (this is
/// typically for a local serialization value that isn't being pushed to the server). Note that
/// signing is always disabled if there is no signing callback set, regardless of the value of
/// this argument.
virtual std::string serialize(bool enable_signing = true);
protected:
std::string serialize_impl(const oxenc::bt_dict& diff, bool enable_signing = true);
};
// Constructor tag
struct increment_seqno_t {};
inline constexpr increment_seqno_t increment_seqno{};
class MutableConfigMessage : public ConfigMessage {
protected:
dict orig_data_{data_};
friend class ConfigMessage;
public:
MutableConfigMessage(const MutableConfigMessage&) = default;
MutableConfigMessage& operator=(const MutableConfigMessage&) = default;
MutableConfigMessage(MutableConfigMessage&&) = default;
MutableConfigMessage& operator=(MutableConfigMessage&&) = default;
/// Constructs a new, empty config message. Takes various fields to pre-fill the various
/// properties during construction (these are for convenience and equivalent to setting them via
/// properties/methods after construction).
///
/// seqno -- the message's seqno, default 0
/// lags -- number of lags to keep (when deriving messages, e.g. via increment())
/// signer -- if specified and not nullptr then this message will be signed when serialized
/// using the given signing function. If omitted no signing takes place.
explicit MutableConfigMessage(
seqno_t seqno = 0, int lag = DEFAULT_DIFF_LAGS, sign_callable signer = nullptr) {
this->lag = lag;
this->seqno(seqno);
this->signer = signer;
}
/// Wraps the ConfigMessage constructor with the same arguments but always produces a
/// MutableConfigMessage. In particular this means that if the base constructor performed a
/// merge (and thus incremented seqno) then the config stays as is, but contained in a Mutable
/// message that can be changed. If it did *not* merge (i.e. the highest seqno message it found
/// did not conflict with any other messages) then this construction is equivalent to doing a
/// base load followed by a .increment() call. In other words: this constructor *always* gives
/// you an incremented seqno value from the highest valid input config message.
///
/// This is almost equivalent to ConfigMessage{args...}.increment(), except that this
/// constructor only increments seqno once while the indirect version would increment twice in
/// the case of a required merge conflict resolution.
explicit MutableConfigMessage(
const std::vector<std::string_view>& configs,
verify_callable verifier = nullptr,
sign_callable signer = nullptr,
int lag = DEFAULT_DIFF_LAGS,
bool signature_optional = false,
std::function<void(const config_error&)> error_handler = nullptr);
/// Wrapper around the above that takes a single string view to load a single message, doesn't
/// take an error handler and instead always throws on parse errors (the above also throws for
/// an erroneous single message, but with a less specific "no valid config messages" error).
explicit MutableConfigMessage(
std::string_view config,
verify_callable verifier = nullptr,
sign_callable signer = nullptr,
int lag = DEFAULT_DIFF_LAGS,
bool signature_optional = false);
/// Does the same as the base incrementing, but also records any diff info from the current
/// MutableConfigMessage. *this* object gets pruned and signed as part of this call. If the
/// sign argument is omitted/nullptr then the current object's `sign` callback gets copied into
/// the new object. After this call you typically do not want to further modify *this (because
/// any modifications will change the hash, making *this no longer a parent of the new object).
MutableConfigMessage increment() const override;
/// Constructor that does the same thing as the `m.increment()` factory method. The second
/// value should be the literal `increment_seqno` value (to select this constructor).
explicit MutableConfigMessage(const ConfigMessage& m, increment_seqno_t);
using ConfigMessage::data;
/// Returns a mutable reference to the underlying config data.
dict& data() { return data_; }
using ConfigMessage::seqno;
/// Sets the seqno of the message to a specific value. You usually want to use `.increment()`
/// from an existing config message rather than manually adjusting the seqno.
void seqno(seqno_t new_seqno) { seqno_hash_.first = new_seqno; }
/// Returns the current diff for this data relative to its original data. The data is pruned
/// implicitly by this call.
const oxenc::bt_dict& diff() override;
/// Prunes empty dicts/sets from data. This is called automatically when serializing or
/// calculating a diff. Returns true if the data was actually changed, false if nothing needed
/// pruning.
bool prune();
/// Calculates the hash of the current message. Can optionally be given the already-serialized
/// value, if available; if empty/omitted, `serialize()` will be called to compute it.
const hash_t& hash() override;
protected:
const hash_t& hash(std::string_view serialized);
void increment_impl();
};
/// Encrypts a config message using XChaCha20-Poly1305, using a blake2b keyed hash of the message
/// for the nonce (rather than pure random) so that different clients will encrypt the same data to
/// the same encrypted value (thus allowing for server-side deduplication of identical messages).
///
/// `key_base` must be 32 bytes. This value is a fixed key that all clients that might receive this
/// message can calculate independently (for instance a value derived from a secret key, or a shared
/// random key). This key will be hashed with the message size and domain suffix (see below) to
/// determine the actual encryption key.
///
/// `domain` is a short string (1-24 chars) used for the keyed hash. Typically this is the type of
/// config, e.g. "closed-group" or "contacts". The full key will be
/// "session-config-encrypted-message-[domain]". This value is also used for the encrypted key (see
/// above).
///
/// The returned result will consist of encrypted data with authentication tag and appended nonce,
/// suitable for being passed to decrypt() to authenticate and decrypt.
///
/// Throw std::invalid_argument on bad input (i.e. from invalid key_base or domain).
ustring encrypt(ustring_view message, ustring_view key_base, std::string_view domain);
/// Same as above but works with strings/string_views instead of ustring/ustring_view
std::string encrypt(std::string_view message, std::string_view key_base, std::string_view domain);
/// Thrown if decrypt() fails.
struct decrypt_error : std::runtime_error {
using std::runtime_error::runtime_error;
};
/// Takes a value produced by `encrypt()` and decrypts it. `key_base` and `domain` must be the same
/// given to encrypt or else decryption fails. Upon decryption failure a std::
ustring decrypt(ustring_view ciphertext, ustring_view key_base, std::string_view domain);
/// Same as above but using std::string/string_view
std::string decrypt(
std::string_view ciphertext, std::string_view key_base, std::string_view domain);
} // namespace session::config
namespace oxenc::detail {
template <>
struct bt_serialize<session::config::dict_value> : bt_serialize<session::config::dict_variant> {};
} // namespace oxenc::detail

View File

@ -0,0 +1,85 @@
#pragma once
#ifdef __cplusplus
extern "C" {
#endif
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#if defined(_WIN32) || defined(WIN32)
#define LIBSESSION_EXPORT __declspec(dllexport)
#else
#define LIBSESSION_EXPORT __attribute__((visibility("default")))
#endif
#define LIBSESSION_C_API extern "C" LIBSESSION_EXPORT
typedef int64_t seqno_t;
// Config object base type: this type holds the internal object and is initialized by the various
// config-dependent settings (e.g. config_user_profile_init) then passed to the various functions.
typedef struct config_object {
// Internal opaque object pointer; calling code should leave this alone.
void* internals;
// When an error occurs in the C API this string will be set to the specific error message. May
// be NULL.
const char* last_error;
} config_object;
// Common functions callable on any config instance:
/// Frees a config object created with one of the config-dependent ..._init functions (e.g.
/// user_profile_init).
void config_free(config_object* conf);
/// Returns the numeric namespace in which config messages of this type should be stored.
int16_t config_storage_namespace(const config_object* conf);
/// Merges the config object with one or more remotely obtained config strings. After this call the
/// config object may be unchanged, complete replaced, or updated and needing a push, depending on
/// the messages that are merged; the caller should check config_needs_push().
///
/// `configs` is an array of pointers to the start of the strings; `lengths` is an array of string
/// lengths; `count` is the length of those two arrays.
void config_merge(config_object* conf, const char** configs, const size_t* lengths, size_t count);
/// Returns true if this config object contains updated data that has not yet been confirmed stored
/// on the server.
bool config_needs_push(const config_object* conf);
/// Obtains the configuration data that needs to be pushed to the server. A new buffer of the
/// appropriate size is malloc'd and set to `out` The output is written to a new malloc'ed buffer of
/// the appropriate size; the buffer and the output length are set in the `out` and `outlen`
/// parameters. Note that this is binary data, *not* a null-terminated C string.
///
/// Generally this call should be guarded by a call to `config_needs_push`, however it can be used
/// to re-obtain the current serialized config even if no push is needed (for example, if the client
/// wants to re-submit it after a network error).
///
/// NB: The returned buffer belongs to the caller: that is, the caller *MUST* free() it when done
/// with it.
seqno_t config_push(config_object* conf, char** out, size_t* outlen);
/// Reports that data obtained from `config_push` has been successfully stored on the server. The
/// seqno value is the one returned by the config_push call that yielded the config data.
void config_confirm_pushed(config_object* conf, seqno_t seqno);
/// Returns a binary dump of the current state of the config object. This dump can be used to
/// resurrect the object at a later point (e.g. after a restart). Allocates a new buffer and sets
/// it in `out` and the length in `outlen`. Note that this is binary data, *not* a null-terminated
/// C string.
///
/// NB: It is the caller's responsibility to `free()` the buffer when done with it.
///
/// Immediately after this is called `config_needs_dump` will start returning true (until the
/// configuration is next modified).
void config_dump(config_object* conf, char** out, size_t* outlen);
/// Returns true if something has changed since the last call to `dump()` that requires calling
/// and saving the `config_dump()` data again.
bool config_needs_dump(const config_object* conf);
#ifdef __cplusplus
} // extern "C"
#endif

View File

@ -0,0 +1,503 @@
#pragma once
#include <memory>
#include <session/config.hpp>
#include <type_traits>
#include <variant>
#include "base.h"
#include "namespaces.hpp"
namespace session::config {
template <typename T, typename... U>
static constexpr bool is_one_of = (std::is_same_v<T, U> || ...);
/// True for a dict_value direct subtype, but not scalar sub-subtypes.
template <typename T>
static constexpr bool is_dict_subtype = is_one_of<T, config::scalar, config::set, config::dict>;
/// True for a dict_value or any of the types containable within a dict value
template <typename T>
static constexpr bool is_dict_value =
is_dict_subtype<T> || is_one_of<T, dict_value, int64_t, std::string>;
// Levels for the logging callback
enum class LogLevel { debug, info, warning, error };
/// Our current config state
enum class ConfigState : int {
/// Clean means the config is confirmed stored on the server and we haven't changed anything.
Clean = 0,
/// Dirty means we have local changes, and the changes haven't been serialized yet for sending
/// to the server.
Dirty = 1,
/// Waiting is halfway in-between clean and dirty: the caller has serialized the data, but
/// hasn't yet reported back that the data has been stored, *and* we haven't made any changes
/// since the data was serialize.
Waiting = 2,
};
/// Base config type for client-side configs containing common functionality needed by all config
/// sub-types.
class ConfigBase {
private:
// The object (either base config message or MutableConfigMessage) that stores the current
// config message. Subclasses do not directly access this: instead they call `dirty()` if they
// intend to make changes, or the `set_config_field` wrapper.
std::unique_ptr<ConfigMessage> _config;
// Tracks our current state
ConfigState _state = ConfigState::Clean;
protected:
// Constructs an empty base config with no config settings and seqno set to 0.
ConfigBase();
// Constructs a base config by loading the data from a dump as produced by `dump()`.
explicit ConfigBase(std::string_view dump);
// Tracks whether we need to dump again; most mutating methods should set this to true (unless
// calling set_state, which sets to to true implicitly).
bool _needs_dump = false;
// Sets the current state; this also sets _needs_dump to true.
void set_state(ConfigState s) {
_state = s;
_needs_dump = true;
}
// If set then we log things by calling this callback
std::function<void(LogLevel lvl, std::string msg)> logger;
// Invokes the above if set, does nothing if there is no logger.
void log(LogLevel lvl, std::string msg) {
if (logger)
logger(lvl, std::move(msg));
}
// Returns a reference to the current MutableConfigMessage. If the current message is not
// already dirty (i.e. Clean or Waiting) then calling this increments the seqno counter.
MutableConfigMessage& dirty();
// class for proxying subfield access; this class should never be stored but only used
// ephemerally (most of its methods are rvalue-qualified). This lets constructs such as
// foo["abc"]["def"]["ghi"] = 12;
// work, auto-vivifying (or trampling, if not a dict) subdicts to reach the target. It also
// allows non-vivifying value retrieval via .string(), .integer(), etc. methods.
class DictFieldProxy {
private:
ConfigBase& _conf;
std::vector<std::string> _inter_keys;
std::string _last_key;
// See if we can find the key without needing to create anything, so that we can attempt to
// access values without mutating anything (which allows, among other things, for assigning
// of the existing value to not dirty anything). Returns nullptr if the value or something
// along its path would need to be created, or has the wrong type; otherwise a const pointer
// to the value. The templated type, if provided, can be one of the types a dict_value can
// hold to also check that the returned value has a particular type; if omitted you get back
// the dict_value pointer itself.
template <typename T = dict_value, typename = std::enable_if_t<is_dict_value<T>>>
const T* get_clean() const {
const config::dict* data = &_conf._config->data();
// All but the last need to be dicts:
for (const auto& key : _inter_keys) {
auto it = data->find(key);
data = it != data->end() ? std::get_if<config::dict>(&it->second) : nullptr;
if (!data)
return nullptr;
}
const dict_value* val;
// The last can be any value type:
if (auto it = data->find(_last_key); it != data->end())
val = &it->second;
else
return nullptr;
if constexpr (std::is_same_v<T, dict_value>)
return val;
else if constexpr (is_dict_subtype<T>) {
if (auto* v = std::get_if<T>(val))
return v;
} else { // int64 or std::string, i.e. the config::scalar sub-types.
if (auto* scalar = std::get_if<config::scalar>(val))
return std::get_if<T>(scalar);
}
return nullptr;
}
// Returns a lvalue reference to the value, stomping its way through the dict as it goes to
// create subdicts as needed to reach the target value. If given a template type then we
// also cast the final dict_value variant into the given type (and replace if with a
// default-constructed value if it has the wrong type) then return a reference to that.
template <typename T = dict_value, typename = std::enable_if_t<is_dict_value<T>>>
T& get_dirty() {
config::dict* data = &_conf.dirty().data();
for (const auto& key : _inter_keys) {
auto& val = (*data)[key];
data = std::get_if<config::dict>(&val);
if (!data)
data = &val.emplace<config::dict>();
}
auto& val = (*data)[_last_key];
if constexpr (std::is_same_v<T, dict_value>)
return val;
else if constexpr (is_dict_subtype<T>) {
if (auto* v = std::get_if<T>(&val))
return *v;
return val.emplace<T>();
} else { // int64 or std::string, i.e. the config::scalar sub-types.
if (auto* scalar = std::get_if<config::scalar>(&val)) {
if (auto* v = std::get_if<T>(scalar))
return *v;
return scalar->emplace<T>();
}
return val.emplace<scalar>().emplace<T>();
}
}
template <typename T>
void assign_if_changed(T value) {
// Try to avoiding dirtying the config if this assignment isn't changing anything
if (!_conf.is_dirty())
if (auto current = get_clean<T>(); current && *current == value)
return;
get_dirty<T>() = std::move(value);
}
void insert_if_missing(config::scalar&& value) {
if (!_conf.is_dirty())
if (auto current = get_clean<config::set>(); current && current->count(value))
return;
get_dirty<config::set>().insert(std::move(value));
}
void set_erase_impl(const config::scalar& value) {
if (!_conf.is_dirty())
if (auto current = get_clean<config::set>(); current && !current->count(value))
return;
config::dict* data = &_conf.dirty().data();
for (const auto& key : _inter_keys) {
auto it = data->find(key);
data = it != data->end() ? std::get_if<config::dict>(&it->second) : nullptr;
if (!data)
return;
}
auto it = data->find(_last_key);
if (it == data->end())
return;
auto& val = it->second;
if (auto* current = std::get_if<config::set>(&val))
current->erase(value);
else
val.emplace<config::set>();
}
public:
DictFieldProxy(ConfigBase& b, std::string key) : _conf{b}, _last_key{std::move(key)} {}
/// Descends into a dict, returning a copied proxy object for the path to the requested
/// field. Nothing is created by doing this unless you actually assign to a value.
DictFieldProxy operator[](std::string subkey) const& {
DictFieldProxy subfield{_conf, std::move(subkey)};
subfield._inter_keys.reserve(_inter_keys.size() + 1);
subfield._inter_keys.insert(
subfield._inter_keys.end(), _inter_keys.begin(), _inter_keys.end());
subfield._inter_keys.push_back(_last_key);
return subfield;
}
// Same as above, but when called on an rvalue reference we just mutate the current proxy to
// the new dict path.
DictFieldProxy&& operator[](std::string subkey) && {
_inter_keys.push_back(std::move(_last_key));
_last_key = std::move(subkey);
return std::move(*this);
}
/// Returns a const pointer to the string if one exists at the given location, nullptr
/// otherwise.
const std::string* string() const { return get_clean<std::string>(); }
/// returns the value as a string_view or a fallback if the value doesn't exist (or isn't a
/// string). The returned view is directly into the value (or fallback) and so mustn't be
/// used beyond the validity of either.
std::string_view string_view_or(std::string_view fallback) const {
if (auto* s = string())
return {*s};
return fallback;
}
/// Returns a copy of the value as a string, if it exists and is a string; returns
/// `fallback` otherwise.
std::string string_or(std::string fallback) const {
if (auto* s = string())
return *s;
return std::move(fallback);
}
/// Returns a const pointer to the integer if one exists at the given location, nullptr
/// otherwise.
const int64_t* integer() const { return get_clean<int64_t>(); }
/// Returns the value as an integer or a fallback if the value doesn't exist (or isn't an
/// integer).
int64_t integer_or(int64_t fallback) const {
if (auto* i = integer())
return *i;
return fallback;
}
/// Returns a const pointer to the set if one exists at the given location, nullptr
/// otherwise.
const config::set* set() const { return get_clean<config::set>(); }
/// Returns a const pointer to the dict if one exists at the given location, nullptr
/// otherwise. (You typically don't need to use this but can rather just use [] to descend
/// into the dict).
const config::dict* dict() const { return get_clean<config::dict>(); }
/// Replaces the current value with the given string. This also auto-vivifies any
/// intermediate dicts needed to reach the given key, including replacing non-dict values if
/// they currently exist along the path.
void operator=(std::string value) { assign_if_changed(std::move(value)); }
/// Same as above, but takes a string_view for convenience.
void operator=(std::string_view value) { *this = std::string{value}; }
/// Replace the current value with the given integer. See above.
void operator=(int64_t value) { assign_if_changed(value); }
/// Replace the current value with the given set. See above.
void operator=(config::set value) { assign_if_changed(std::move(value)); }
/// Replace the current value with the given dict. See above. This often isn't needed
/// because of how other assignment operations work.
void operator=(config::dict value) { assign_if_changed(std::move(value)); }
/// Returns true if there is a value at the current key. If a template type T is given, it
/// only returns true if that value also is a `T`.
template <typename T = dict_value, typename = std::enable_if_t<is_dict_value<T>>>
bool exists() const {
return get_clean<T>() != nullptr;
}
// Alias for `exists<T>()`
template <typename T>
bool is() const {
return exists<T>();
}
/// Removes the value at the current location, regardless of what it currently is. This
/// does nothing if the current location does not have a value.
void erase() {
if (!_conf.is_dirty() && !get_clean())
return;
config::dict* data = &_conf.dirty().data();
for (const auto& key : _inter_keys) {
auto it = data->find(key);
data = it != data->end() ? std::get_if<config::dict>(&it->second) : nullptr;
if (!data)
return;
}
data->erase(_last_key);
}
/// Adds a value to the set at the current location. If the current value is not a set or
/// does not exist then dicts will be created to reach it and a new set will be created.
void set_insert(std::string_view value) {
insert_if_missing(config::scalar{std::string{value}});
}
void set_insert(int64_t value) { insert_if_missing(config::scalar{value}); }
/// Removes a value from the set at the current location. If the current value does not
/// exist then nothing happens. If it does exist, but is not a set, it will be replaced
/// with an empty set. Otherwise the given value will be removed from the set, if present.
void set_erase(std::string_view value) {
set_erase_impl(config::scalar{std::string{value}});
}
void set_erase(int64_t value) { set_erase_impl(scalar{value}); }
/// Emplaces a value at the current location. As with assignment, this creates dicts as
/// needed along the keys to reach the target. The existing value (if present) is destroyed
/// to make room for the new one.
template <
typename T,
typename... Args,
typename = std::enable_if_t<
is_one_of<T, config::set, config::dict, int64_t, std::string>>>
T& emplace(Args&&... args) {
if constexpr (is_one_of<T, int64_t, std::string>)
return get_dirty<scalar>().emplace<T>(std::forward<Args>(args)...);
return get_dirty().emplace<T>(std::forward<Args>(args)...);
}
};
/// Wrapper for the ConfigBase's root `data` field to provide data access. Only provides a []
/// that gets you into a DictFieldProxy.
class DictFieldRoot {
ConfigBase& _conf;
DictFieldRoot(DictFieldRoot&&) = delete;
DictFieldRoot(const DictFieldRoot&) = delete;
DictFieldRoot& operator=(DictFieldRoot&&) = delete;
DictFieldRoot& operator=(const DictFieldRoot&) = delete;
public:
DictFieldRoot(ConfigBase& b) : _conf{b} {}
/// Access a dict element. This returns a proxy object for accessing the value, but does
/// *not* auto-vivify the path (unless/until you assign to it).
DictFieldProxy operator[](std::string key) const& {
return DictFieldProxy{_conf, std::move(key)};
}
};
// Called when dumping to obtain any extra data that a subclass needs to store to reconstitute
// the object. The base implementation does nothing. The counterpart to this,
// `load_extra_data()`, is called when loading from a dump that has extra data; a subclass
// should either override both (if it needs to serialize extra data) or neither (if it needs no
// extra data). Internally this extra data (if non-empty) is stored in the "+" key of the dump.
virtual oxenc::bt_dict extra_data() const { return {}; }
// Called when constructing from a dump that has extra data. The base implementation does
// nothing.
virtual void load_extra_data(oxenc::bt_dict extra) {}
public:
virtual ~ConfigBase() = default;
// Proxy class providing read and write access to the contained config data.
const DictFieldRoot data{*this};
// Accesses the storage namespace where this config type is to be stored/loaded from. See
// namespaces.hpp for the underlying integer values.
virtual Namespace storage_namespace() const = 0;
// How many config lags should be used for this object; default to 5. Implementing subclasses
// can override to return a different constant if desired. More lags require more "diff"
// storage in the config messages, but also allow for a higher tolerance of simultaneous message
// conflicts.
virtual int config_lags() const { return 5; }
// This takes all of the messages pulled down from the server and does whatever is necessary to
// merge (or replace) the current values.
//
// After this call the caller should check `needs_push()` to see if the data on hand was updated
// and needs to be pushed to the server again.
//
// Will throw on serious error (i.e. if neither the current nor any of the given configs are
// parseable).
virtual void merge(const std::vector<std::string_view>& configs);
// Returns true if we are currently dirty (i.e. have made changes that haven't been serialized
// yet).
bool is_dirty() const { return _state == ConfigState::Dirty; }
// Returns true if we are curently clean (i.e. our current config is stored on the server and
// unmodified).
bool is_clean() const { return _state == ConfigState::Clean; }
// Returns true if this object contains updated data that has not yet been confirmed stored on
// the server. This will be true whenever `is_clean()` is false: that is, if we are currently
// "dirty" (i.e. have changes that haven't been pushed) or are still awaiting confirmation of
// storage of the most recent serialized push data.
virtual bool needs_push() const;
// Returns the data to push to the server along with the seqno value of the data. If the config
// is currently dirty (i.e. has previously unsent modifications) then this marks it as
// awaiting-confirmation instead of dirty so that any future change immediately increments the
// seqno.
virtual std::pair<std::string, seqno_t> push();
// Should be called after the push is confirmed stored on the storage server swarm to let the
// object know the data is stored. (Once this is called `needs_push` will start returning false
// until something changes). Takes the seqno that was pushed so that the object can ensure that
// the latest version was pushed (i.e. in case there have been other changes since the `push()`
// call that returned this seqno).
//
// It is safe to call this multiple times with the same seqno value, and with out-of-order
// seqnos (e.g. calling with seqno 122 after having called with 123; the duplicates and earlier
// ones will just be ignored).
virtual void confirm_pushed(seqno_t seqno);
// Returns a dump of the current state for storage in the database; this value would get passed
// into the constructor to reconstitute the object (including the push/not pushed status). This
// method is *not* virtual: if subclasses need to store extra data they should set it in the
// `subclass_data` field.
std::string dump();
// Returns true if something has changed since the last call to `dump()` that requires calling
// and saving the `dump()` data again.
virtual bool needs_dump() const { return _needs_dump; }
};
// The C++ struct we hold opaquely inside the C internals struct. This is designed so that any
// internals<T> has the same layout so that it doesn't matter whether we unbox to an
// internals<ConfigBase> or internals<SubType>.
template <
typename ConfigT = ConfigBase,
std::enable_if_t<std::is_base_of_v<ConfigBase, ConfigT>, int> = 0>
struct internals final {
std::unique_ptr<ConfigBase> config;
std::string error;
// Dereferencing falls through to the ConfigBase object
ConfigT* operator->() {
if constexpr (std::is_same_v<ConfigT, ConfigBase>)
return config.get();
else {
auto* c = dynamic_cast<ConfigT*>(config.get());
assert(c);
return c;
}
}
const ConfigT* operator->() const {
if constexpr (std::is_same_v<ConfigT, ConfigBase>)
return config.get();
else {
auto* c = dynamic_cast<ConfigT*>(config.get());
assert(c);
return c;
}
}
ConfigT& operator*() { return *operator->(); }
const ConfigT& operator*() const { return *operator->(); }
};
template <typename T = ConfigBase, std::enable_if_t<std::is_base_of_v<ConfigBase, T>, int> = 0>
inline internals<T>& unbox(config_object* conf) {
return *static_cast<internals<T>*>(conf->internals);
}
template <typename T = ConfigBase, std::enable_if_t<std::is_base_of_v<ConfigBase, T>, int> = 0>
inline const internals<T>& unbox(const config_object* conf) {
return *static_cast<const internals<T>*>(conf->internals);
}
// Sets an error message in the internals.error string and updates the last_error pointer in the
// outer (C) config_object struct to point at it.
void set_error(config_object* conf, std::string e);
// Same as above, but gets the error string out of an exception and passed through a return value.
// Intended to simplify catch-and-return-error such as:
// try {
// whatever();
// } catch (const std::exception& e) {
// return set_error(conf, LIB_SESSION_ERR_OHNOES, e);
// }
inline int set_error(config_object* conf, int errcode, const std::exception& e) {
set_error(conf, e.what());
return errcode;
}
// Copies a value contained in a string into a new malloced char buffer, returning the buffer and
// size via the two pointer arguments.
void copy_out(const std::string& data, char** out, size_t* outlen);
} // namespace session::config

View File

@ -0,0 +1,23 @@
#pragma once
#ifdef __cplusplus
extern "C" {
#endif
enum config_error {
/// Value returned for no error
SESSION_ERR_NONE = 0,
/// Error indicating that initialization failed because the dumped data being loaded is invalid.
SESSION_ERR_INVALID_DUMP = 1,
/// Error indicated a bad value, e.g. if trying to set something invalid in a config field.
SESSION_ERR_BAD_VALUE = 2,
};
// Returns a generic string for a given integer error code as returned by some functions. Depending
// on the call, a more details error string may be available in the config_object's `last_error`
// field.
const char* config_errstr(int err);
#ifdef __cplusplus
} // extern "C"
#endif

View File

@ -0,0 +1,12 @@
#pragma once
#include <cstdint>
namespace session::config {
enum class Namespace : std::int16_t {
UserProfile = 2,
ClosedGroupInfo = 11,
};
} // namespace session::config

View File

@ -0,0 +1,51 @@
#pragma once
#ifdef __cplusplus
extern "C" {
#endif
#include "base.h"
/// Constructs a user profile config object and sets a pointer to it in `conf`. To restore an
/// existing dump produced by a past instantiation's call to `dump()` pass the dump value via `dump`
/// and `dumplen`; to construct a new, empty profile pass NULL and 0.
///
/// `error` must either be NULL or a pointer to a buffer of at least 256 bytes.
///
/// Returns 0 on success; returns a non-zero error code and sets error (if not NULL) to the
/// exception message on failure.
///
/// When done with the object the `config_object` must be destroyed by passing the pointer to
/// config_free() (in `session/config/base.h`).
int user_profile_init(config_object** conf, const char* dump, size_t dumplen, char* error)
__attribute__((warn_unused_result));
/// Returns a pointer to the currently-set name (null-terminated), or NULL if there is no name at
/// all. Should be copied right away as the pointer may not remain valid beyond other API calls.
const char* user_profile_get_name(const config_object* conf);
/// Sets the user profile name to the null-terminated C string. Returns 0 on success, non-zero on
/// error (and sets the config_object's error string).
int user_profile_set_name(config_object* conf, const char* name);
typedef struct user_profile_pic {
// Null-terminated C string containing the uploaded URL of the pic. Will be NULL if there is no
// profile pic.
const char* url;
// The profile pic decryption key, in bytes. This is a byte buffer of length `keylen`, *not* a
// null-terminated C string. Will be NULL if there is no profile pic.
const char* key;
size_t keylen;
} user_profile_pic;
// Obtains the current profile pic. The pointers in the returned struct will be NULL if a profile
// pic is not currently set, and otherwise should be copied right away (they will not be valid
// beyond other API calls on this config object).
user_profile_pic user_profile_get_pic(const config_object* conf);
// Sets a user profile
int user_profile_set_pic(config_object* conf, user_profile_pic pic);
#ifdef __cplusplus
} // extern "C"
#endif

View File

@ -0,0 +1,43 @@
#pragma once
#include <memory>
#include <session/config.hpp>
#include "base.hpp"
#include "namespaces.hpp"
namespace session::config {
/// keys used in this config, either currently or in the past (so that we don't reuse):
///
/// n - user profile name
/// p - user profile url
/// q - user profile decryption key (binary)
class UserProfile final : public ConfigBase {
public:
/// Constructs a new, blank user profile.
UserProfile() = default;
/// Constructs a user profile from existing data
explicit UserProfile(std::string_view dumped) : ConfigBase{dumped} {}
Namespace storage_namespace() const override { return Namespace::UserProfile; }
/// Returns the user profile name, or nullptr if there is no profile name set.
const std::string* get_name() const;
/// Sets the user profile name
void set_name(std::string_view new_name);
/// Gets the user's current profile pic URL and decryption key. Returns nullptr for *both*
/// values if *either* value is unset or empty in the config data.
std::pair<const std::string*, const std::string*> get_profile_pic() const;
/// Sets the user's current profile pic to a new URL and decryption key. Clears both if either
/// one is empty.
void set_profile_pic(std::string url, std::string key);
};
} // namespace session::config

View File

@ -0,0 +1,43 @@
#pragma once
#include <array>
#include <chrono>
#include <cstdint>
#include <string>
namespace session {
using namespace std::literals;
/// An uploaded file is its URL + decryption key
struct Uploaded {
std::string url;
std::string key;
};
/// A conversation disappearing messages setting
struct Disappearing {
/// The possible modes of a disappearing messages setting.
enum class Mode : int { None = 0, AfterSend = 1, AfterRead = 2 };
/// The mode itself
Mode mode = Mode::None;
/// The timer value; this is only used when mode is not None.
std::chrono::seconds timer = 0s;
};
/// A Session ID: an x25519 pubkey, with a 05 identifying prefix. On the wire we send just the
/// 32-byte pubkey value (i.e. not hex, without the prefix).
struct SessionID {
/// The fixed session netid, 0x05
static constexpr unsigned char netid = 0x05;
/// The raw x25519 pubkey, as bytes
std::array<unsigned char, 32> pubkey;
/// Returns the full pubkey in hex, including the netid prefix.
std::string hex() const;
};
} // namespace session

View File

@ -0,0 +1,34 @@
#pragma once
#ifdef __cplusplus
extern "C" {
#endif
/// XEd25519-signed a message given a curve25519 privkey and message. Writes the 64-byte signature
/// to `sig` on success and returns 0. Returns non-zero on failure.
__attribute__((warn_unused_result)) int session_xed25519_sign(
unsigned char* signature /* 64 byte buffer */,
const unsigned char* curve25519_privkey /* 32 bytes */,
const unsigned char* msg,
const unsigned int msg_len);
/// Verifies an XEd25519-signed message given a 64-byte signature, 32-byte curve25519 pubkey, and
/// message. Returns 0 if the signature verifies successfully, non-zero on failure.
__attribute__((warn_unused_result)) int session_xed25519_verify(
const unsigned char* signature /* 64 bytes */,
const unsigned char* pubkey /* 32-bytes */,
const unsigned char* msg,
const unsigned int msg_len);
/// Given a curve25519 pubkey, this writes the associated XEd25519-derived Ed25519 pubkey into
/// ed25519_pubkey. Note, however, that there are *two* possible Ed25519 pubkeys that could result
/// in a given curve25519 pubkey: this always returns the positive value. You can get the other
/// possibility (the negative) by flipping the sign bit, i.e. `returned_pubkey[31] |= 0x80`.
/// Returns 0 on success, non-0 on failure.
__attribute__((warn_unused_result)) int session_xed25519_pubkey(
unsigned char* ed25519_pubkey /* 32-byte output buffer */,
const unsigned char* curve25519_pubkey /* 32 bytes */);
#ifdef __cplusplus
}
#endif

View File

@ -0,0 +1,38 @@
#pragma once
#include <array>
#include <string>
#include <string_view>
namespace session::xed25519 {
using ustring_view = std::basic_string_view<unsigned char>;
/// XEd25519-signs a message given the curve25519 privkey and message.
std::array<unsigned char, 64> sign(
ustring_view curve25519_privkey /* 32 bytes */, ustring_view msg);
/// "Softer" version that takes and returns strings of regular chars
std::string sign(std::string_view curve25519_privkey /* 32 bytes */, std::string_view msg);
/// Verifies a curve25519 message allegedly signed by the given curve25519 pubkey
[[nodiscard]] bool verify(
ustring_view signature /* 64 bytes */,
ustring_view curve25519_pubkey /* 32 bytes */,
ustring_view msg);
/// "Softer" version that takes strings of regular chars
[[nodiscard]] bool verify(
std::string_view signature /* 64 bytes */,
std::string_view curve25519_pubkey /* 32 bytes */,
std::string_view msg);
/// Given a curve25519 pubkey, this returns the associated XEd25519-derived Ed25519 pubkey. Note,
/// however, that there are *two* possible Ed25519 pubkeys that could result in a given curve25519
/// pubkey: this always returns the positive value. You can get the other possibility (the
/// negative) by flipping the sign bit, i.e. `returned_pubkey[31] |= 0x80`.
std::array<unsigned char, 32> pubkey(ustring_view curve25519_pubkey);
/// "Softer" version that takes/returns strings of regular chars
std::string pubkey(std::string_view curve25519_pubkey);
} // namespace session::xed25519

View File

@ -0,0 +1,30 @@
package network.loki.messenger.libsession_util
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.Assert.*
/**
* Instrumented test, which will execute on an Android device.
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
@RunWith(AndroidJUnit4::class)
class ExampleInstrumentedTest {
@Test
fun useAppContext() {
// Context of the app under test.
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
assertEquals("network.loki.messenger.libsession_util.test", appContext.packageName)
}
@Test
fun jni_accessible() {
assertEquals("Hello from C++", NativeLib().stringFromJNI())
}
}

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="network.loki.messenger.libsession_util">
</manifest>

View File

@ -0,0 +1,57 @@
# For more information about using CMake with Android Studio, read the
# documentation: https://d.android.com/studio/projects/add-native-code.html
# Sets the minimum version of CMake required to build the native library.
cmake_minimum_required(VERSION 3.18.1)
# Declares and names the project.
project("session_util")
# Creates and names a library, sets it as either STATIC
# or SHARED, and provides the relative paths to its source code.
# You can define multiple libraries, and CMake builds them for you.
# Gradle automatically packages shared libraries with your APK.
add_library( # Sets the name of the library.
session_util
# Sets the library as a shared library.
SHARED
# Provides a relative path to your source file(s).
session_util.cpp)
# Searches for a specified prebuilt library and stores the path as a
# variable. Because CMake includes system libraries in the search path by
# default, you only need to specify the name of the public NDK library
# you want to add. CMake verifies that the library exists before
# completing its build.
find_library( # Sets the name of the path variable.
log-lib
# Specifies the name of the NDK library that
# you want CMake to locate.
log)
# Add the libsession-util library here
set(distribution_DIR ${CMAKE_CURRENT_SOURCE_DIR}/../../../distribution)
add_library(external-libsession-util STATIC IMPORTED)
set_target_properties(external-libsession-util PROPERTIES IMPORTED_LOCATION
${distribution_DIR}/libsession-util-android/lib/${ANDROID_ABI}/libsession-util.a)
target_include_directories(session_util PRIVATE
${distribution_DIR}/libsession-util-android/include)
# Specifies libraries CMake should link to your target library. You
# can link multiple libraries, such as libraries you define in this
# build script, prebuilt third-party libraries, or system libraries.
target_link_libraries( # Specifies the target library.
session_util
external-libsession-util
# Links the target library to the log library
# included in the NDK.
${log-lib})

View File

@ -0,0 +1,19 @@
#include <jni.h>
#include <string>
#include "session/config/user_profile.hpp"
extern "C" JNIEXPORT jobject JNICALL
Java_network_loki_messenger_libsession_1util_Config_00024Companion_newInstance(
JNIEnv* env,
jobject /*this*/) {
auto* profile = new session::config::UserProfile();
jclass configClass = env->FindClass("network/loki/messenger/libsession_util/Config");
jobject newConfig = env->AllocObject(configClass);
jfieldID pointerField = env->GetFieldID(configClass, "pointer", "J");
env->SetLongField(newConfig, pointerField, reinterpret_cast<jlong>(profile));
return newConfig;
}

View File

@ -0,0 +1,13 @@
package network.loki.messenger.libsession_util
data class Config(private val /* yucky */ pointer: Long) {
companion object {
external fun newInstance(): Config
}
var lastError: String? = null
}

View File

@ -0,0 +1,17 @@
package network.loki.messenger.libsession_util
class NativeLib {
/**
* A native method that is implemented by the 'libsession_util' native library,
* which is packaged with this application.
*/
external fun stringFromJNI(): String
companion object {
// Used to load the 'libsession_util' library on application startup.
init {
System.loadLibrary("session_util")
}
}
}

View File

@ -0,0 +1,14 @@
package network.loki.messenger.libsession_util
import org.junit.Test
import org.junit.Assert.*
/**
* Example local unit test, which will execute on the development machine (host).
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
class ExampleUnitTest {
}

View File

@ -3,4 +3,5 @@ rootProject.name = "session-android"
include ':app' include ':app'
include ':liblazysodium' include ':liblazysodium'
include ':libsession' include ':libsession'
include ':libsignal' include ':libsignal'
include ':libsession-util'