mirror of
https://github.com/oxen-io/session-android.git
synced 2024-11-25 02:55:23 +00:00
feat: start libsession-util integration and experimentation
This commit is contained in:
parent
1dbcffe40b
commit
4045333dbc
2
libsession-util/.gitignore
vendored
Normal file
2
libsession-util/.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
/build
|
||||||
|
/.cxx/
|
49
libsession-util/build.gradle
Normal file
49
libsession-util/build.gradle
Normal 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'
|
||||||
|
}
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,5 @@
|
|||||||
|
#pragma once
|
||||||
|
#include "bt_value.h"
|
||||||
|
#include "bt_serialize.h"
|
||||||
|
#include "bt_producer.h"
|
||||||
|
#include "bt_value_producer.h"
|
@ -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
@ -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}} {}
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
@ -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));
|
||||||
|
}
|
||||||
|
}
|
@ -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;
|
||||||
|
|
||||||
|
}
|
@ -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
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -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
|
@ -0,0 +1,5 @@
|
|||||||
|
namespace oxenc {
|
||||||
|
constexpr int VERSION_MAJOR = 1;
|
||||||
|
constexpr int VERSION_MINOR = 0;
|
||||||
|
constexpr int VERSION_PATCH = 6;
|
||||||
|
}
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -0,0 +1,12 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
|
||||||
|
namespace session::config {
|
||||||
|
|
||||||
|
enum class Namespace : std::int16_t {
|
||||||
|
UserProfile = 2,
|
||||||
|
ClosedGroupInfo = 11,
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace session::config
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -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())
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
5
libsession-util/src/main/AndroidManifest.xml
Normal file
5
libsession-util/src/main/AndroidManifest.xml
Normal 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>
|
57
libsession-util/src/main/cpp/CMakeLists.txt
Normal file
57
libsession-util/src/main/cpp/CMakeLists.txt
Normal 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})
|
19
libsession-util/src/main/cpp/session_util.cpp
Normal file
19
libsession-util/src/main/cpp/session_util.cpp
Normal 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;
|
||||||
|
}
|
@ -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
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
@ -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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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 {
|
||||||
|
|
||||||
|
}
|
@ -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'
|
||||||
|
Loading…
Reference in New Issue
Block a user