-
Notifications
You must be signed in to change notification settings - Fork 16
Expand file tree
/
Copy pathbase.hpp
More file actions
1557 lines (1418 loc) · 69.8 KB
/
base.hpp
File metadata and controls
1557 lines (1418 loc) · 69.8 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
#pragma once
#include <fmt/format.h>
#include <cassert>
#include <chrono>
#include <list>
#include <memory>
#include <session/config.hpp>
#include <session/types.hpp>
#include <session/util.hpp>
#include <span>
#include <type_traits>
#include <unordered_set>
#include <variant>
#include <vector>
#include "../hash.hpp"
#include "../logging.hpp"
#include "../sodium_array.hpp"
#include "base.h"
#include "namespaces.hpp"
using namespace std::literals;
namespace oxenc {
class bt_dict_producer;
class bt_dict_consumer;
} // namespace oxenc
namespace session::config {
/// 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>;
/// 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,
};
using Ed25519PubKey = std::array<unsigned char, 32>;
using Ed25519Secret = sodium_array<unsigned char>;
// Helper base class for holding a config signing keypair
class ConfigSig {
protected:
// Contains an optional signing keypair; if the public key is set then incoming messages must
// contain a valid signature from that key to be loaded. If the private key is set then a
// signature will be added to the message signed by that key. (Note that if a public key is set
// but not a private key then this config object cannot push config changes!)
std::optional<Ed25519PubKey> _sign_pk = std::nullopt;
Ed25519Secret _sign_sk;
ConfigSig() = default;
// Returns a blake2b 32-byte hash of the config signing seed using hash key `key`. `key` must
// be 64 bytes or less, and should generally be unique for each key use case.
//
// Throws if a secret key hasn't been set via `set_sig_keys`.
std::array<unsigned char, 32> seed_hash(std::string_view key) const;
virtual void set_verifier(ConfigMessage::verify_callable v) = 0;
virtual void set_signer(ConfigMessage::sign_callable v) = 0;
// Meant to be called from the subclass constructor after other necessary initialization; calls
// set_sig_keys, set_sig_pubkey, or clear_sig_keys() for you, based on which are non-nullopt.
//
// Throws if given invalid data (i.e. wrong key size, or mismatched pubkey/secretkey).
void init_sig_keys(
std::optional<std::span<const unsigned char>> ed25519_pubkey,
std::optional<std::span<const unsigned char>> ed25519_secretkey);
public:
virtual ~ConfigSig() = default;
/// API: base/ConfigSig::set_sig_keys
///
/// Sets an Ed25519 keypair pair for signing and verifying config messages. When set, this adds
/// an additional signature for verification into the config message (*after* decryption) that
/// validates a config message.
///
/// This is used in config contexts where the encryption/decryption keys are insufficient for
/// permission verification to produce new messages, such as in groups where non-admins need to
/// be able to decrypt group data, but are not permitted to push new group data. In such a case
/// only the admins have the secret key with which messages can be signed; regular users can
/// only read, but cannot write, config messages.
///
/// When a signature public key (with or without a secret key) is set the config object enters
/// a "signing-required" mode, which has some implications worth noting:
/// - incoming messages must contain a signature that verifies with the public key; messages
/// without such a signature will be dropped as invalid.
/// - because of the above, a config object cannot push config updates without the secret key:
/// thus any attempt to modify the config message with a pubkey-only config object will raise
/// an exception.
///
/// Inputs:
/// - `secret` -- the 64-byte sodium-style Ed25519 "secret key" (actually the seed+pubkey
/// concatenated together) that sets both the secret key and public key.
void set_sig_keys(std::span<const unsigned char> secret);
/// API: base/ConfigSig::set_sig_pubkey
///
/// Sets a Ed25519 signing pubkey which incoming messages must be signed by to be acceptable.
/// This is intended for use when the secret key is not known (see `set_sig_keys()` to set both
/// secret and pubkey keys together).
///
/// Inputs:
/// - `pubkey` -- the 32 byte Ed25519 pubkey that must have signed incoming messages
void set_sig_pubkey(std::span<const unsigned char> pubkey);
/// API: base/ConfigSig::get_sig_pubkey
///
/// Returns a const reference to the 32-byte Ed25519 signing pubkey, if set.
///
/// Inputs: none.
///
/// Outputs:
/// - reference to the 32-byte pubkey, or `std::nullopt` if not set.
const std::optional<std::array<unsigned char, 32>>& get_sig_pubkey() const { return _sign_pk; }
/// API: base/ConfigSig::clear_sig_keys
///
/// Drops the signature pubkey and/or secret key, if the object has them.
///
/// Inputs: none.
void clear_sig_keys();
};
/// Base config type for client-side configs containing common functionality needed by all config
/// sub-types.
class ConfigBase : public ConfigSig {
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;
static constexpr size_t KEY_SIZE = 32;
// Contains the base key(s) we use to encrypt/decrypt messages. If non-empty, the .front()
// element will be used when encrypting a new message to push. When decrypting, we attempt each
// of them, starting with .front(), until decryption succeeds.
using Key = std::array<unsigned char, KEY_SIZE>;
sodium_vector<Key> _keys;
// Contains the current active message hash(es), as fed into us in `confirm_pushed()`.
// Typically just one hash, but multiple will occur when dealing with multipart config messages.
// Empty if we don't know it yet. When we dirty the config these hashes get moved into
// `old_hashes_` to be removed by the next push.
std::unordered_set<std::string> _curr_hashes;
// Contains obsolete known message hashes that are obsoleted by the most recent merge or push;
// these are returned (and cleared) when `push` or `old_hashes` are called.
std::unordered_set<std::string> _old_hashes;
struct PartialMessage {
int index; // 0-based index of this part
std::string message_id; // storage server message hash of this part
std::vector<unsigned char> data; // Data chunk
PartialMessage(
int index, std::string_view message_id, std::span<const unsigned char> data) :
index{index}, message_id{message_id}, data{data.begin(), data.end()} {}
};
struct PartialMessages {
bool done = false; // Will be true if this is an already-processed multipart set. We keep
// such stubs around after completing them so that we can optimize away
// reprocessing duplicate parts that might arrive in the near future.
int size = 0; // Total number of parts of this multipart message, if still being
// accumulated. 0 if the message is done.
std::list<PartialMessage> parts; // The individual message parts, in ascending order.
// The expiry of this message info. This gets updated whenever we receive a new part of the
// same message set, and when we complete processing.
std::chrono::system_clock::time_point expiry;
// Shortcut for resetting all fields as appropriate for a finished record: this clears
// parts, sets size to 0, and updates the expiry to now plus the given expiry duration
// (usually the MULTIPART_MAX_REMEMBER value).
void finish(std::chrono::milliseconds lifetime) {
done = true;
size = 0;
parts.clear();
expiry = std::chrono::system_clock::now() + lifetime;
}
};
// Partial message sets that we have received but not yet been able to join into a full message.
// The key is a hash of the final combined data (included in each part to identify related
// parts) used as a unique identifier and checksum; the value is the PartialMessages struct
// containing set metadata and individual parts. (This is an ordered hash, because we relying
// on the keys being sorted when dumping our state to a config item.)
std::map<hash_t, PartialMessages> _multiparts;
// Parses a new multipart message, handling parsing, adding to _multiparts, etc. This is called
// by _merge when it finds a `m`-type message for handling.
//
// - msg_id is the storage-server assigned message id (which is, in current implementation a
// base64 encoded hash, but storage server is allowed to do whatever it wants for this).
// - message is the full message body received (i.e. including the `m` message type prefix).
//
// Returns pair of:
// - true/false indicating whether the single given message was accepted (i.e. parsed correctly
// and didn't have invalid parameters).
// - optional pair that will be non-null only when this message part completed a set of message
// parts resulting in a new, previously unseen message. The first element is a list of
// individual message ids (which will include the input one); the second is the reconstituted
// (and decompressed, if needed) final message body.
//
// For new parts that don't complete a set, errors, and already seen messages the optional
// value will be nullopt.
std::pair<bool, std::optional<std::pair<std::list<std::string>, std::vector<unsigned char>>>>
_handle_multipart(std::string_view msg_id, std::span<const unsigned char> message);
// Writes multipart data into the sub-dict of the dump data.
void _dump_multiparts(oxenc::bt_dict_producer&& multi) const;
// Loads multipart data from the sub-dict of the dump data.
void _load_multiparts(oxenc::bt_dict_consumer&& multi);
// Cleans up any expired multipart data.
void _expire_multiparts();
protected:
// Constructs a base config by loading the data from a dump as produced by `dump()`. If the
// dump is nullopt then an empty base config is constructed with no config settings and seqno
// set to 0.
//
// Can optionally be passed a pubkey or secretkey (or both, but the pubkey can be obtained from
// the secretkey automatically): if either is given, the config object is set up to require
// verification of incoming messages using the associated pubkey, and will be signed using the
// secretkey (if a secret key is given).
explicit ConfigBase(
std::optional<std::span<const unsigned char>> dump = std::nullopt,
std::optional<std::span<const unsigned char>> ed25519_pubkey = std::nullopt,
std::optional<std::span<const unsigned char>> ed25519_secretkey = std::nullopt);
// Initializes the base config object with dump data and keys; this is typically invoked by the
// constructor, but is exposed to subclasses so that they can delay initial processing by
// default-constructing the base class and then calling this from their own constructor. This
// two-step call pattern is *required* when using extra data in particular [because the virtual
// load_extra_data call and any derived class fields are not available before derived class
// construction].
//
// This method must not be called outside derived class construction!
void init(
std::optional<std::span<const unsigned char>> dump = std::nullopt,
std::optional<std::span<const unsigned char>> ed25519_pubkey = std::nullopt,
std::optional<std::span<const unsigned char>> ed25519_secretkey = std::nullopt);
// 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. If transitioning to a dirty
// state and we know our current message hash, that hash gets added to `old_hashes_` to be
// deleted at the next push.
void set_state(ConfigState s);
// 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();
void set_verifier(ConfigMessage::verify_callable v) override;
void set_signer(ConfigMessage::sign_callable s) override;
// Virtual method to be overloaded by derived classes. Protobuf wrapped messages are used
// for legacy types, so we need different logic depending on the class in question. All new
// types will reject the protobuf and directly handle the binary data. Old types will try
// protobuf parsing on incoming messages and handle all outgoing messages directly in binary
virtual bool accepts_protobuf() const { return false; }
public:
// 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;
/// API: base/ConfigBase::DictFieldProxy::get_clean_pair
///
/// 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 nullptrs if the value or
/// something along its path would need to be created, or has the wrong type; otherwise a
/// const pointer to the key and 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. If the field exists but is
/// not the requested `T` type, you get back the key string pointer with a nullptr value.
///
/// Inputs: None
///
/// Outputs:
/// - `const std::string*` -- Key
/// - `const T*` -- Value
template <typename T = dict_value, typename = std::enable_if_t<is_dict_value<T>>>
std::pair<const std::string*, const T*> get_clean_pair() 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, nullptr};
}
const std::string* key;
const dict_value* val;
// The last can be any value type:
if (auto it = data->find(_last_key); it != data->end()) {
key = &it->first;
val = &it->second;
} else
return {nullptr, nullptr};
if constexpr (std::is_same_v<T, dict_value>)
return {key, val};
else if constexpr (is_dict_subtype<T>) {
return {key, std::get_if<T>(val)};
} else { // int64 or std::string, i.e. the config::scalar sub-types.
if (auto* scalar = std::get_if<config::scalar>(val))
return {key, std::get_if<T>(scalar)};
return {key, nullptr};
}
}
/// API: base/ConfigBase::DictFieldProxy::get_clean
///
/// Same as above `get_clean_pair()` but just gives back the value, not the key
///
/// Inputs: None
///
/// Outputs:
/// - `const T*` -- Value
template <typename T = dict_value, typename = std::enable_if_t<is_dict_value<T>>>
const T* get_clean() const {
return get_clean_pair<T>().second;
}
/// API: base/ConfigBase::DictFieldProxy::get_dirty
///
/// 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.
///
/// Inputs: None
///
/// Outputs:
/// - `T&` -- Value
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>();
}
}
/// API: base/ConfigBase::DictFieldProxy::assign_if_changed
///
/// Takes a value and assigns it to the dict only if that value is different.
/// Will avoid dirtying the config if the assignement isn't changing anything
///
/// Inputs:
/// - `value` -- This will be assigned to the dict if it has changed
template <typename T>
void assign_if_changed(T value) {
if constexpr (is_one_of<T, config::set, config::dict>) {
// If we're assigning an empty set or dict then that's really the same as deleting
// the element, since empty sets/dicts get pruned. If we *don't* do this, then
// assigning an empty value will dirty even though, ultimately, we aren't changing
// anything.
if (value.empty()) {
erase();
return;
}
}
// 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);
}
/// API: base/ConfigBase::DictFieldProxy::insert_if_missing
///
/// Takes a value and assigns it to the dict if it does not exist
///
/// Inputs:
/// - `value` -- This will be assigned to the dict if it is missing
//
/// Ouputs:
/// - `bool` -- True if the value was inserted, false otherwise
bool insert_if_missing(config::scalar&& value) {
if (!_conf.is_dirty())
if (auto current = get_clean<config::set>(); current && current->count(value))
return false;
get_dirty<config::set>().insert(std::move(value));
return true;
}
/// API: base/ConfigBase::DictFieldProxy::set_erase_impl
///
/// Erases from the dict
///
/// Inputs:
/// - `value` -- This will be deleted from the dict
///
/// Outputs:
/// - `bool` -- True if an element was erased, false otherwise
bool set_erase_impl(const config::scalar& value) {
if (!_conf.is_dirty())
if (auto current = get_clean<config::set>(); current && !current->count(value))
return false;
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 false;
}
auto it = data->find(_last_key);
if (it == data->end())
return false;
auto& val = it->second;
bool result = false;
if (auto* current = std::get_if<config::set>(&val)) {
current->erase(value);
result = true;
} else {
val.emplace<config::set>();
}
return result;
}
public:
DictFieldProxy(ConfigBase& b, std::string key) : _conf{b}, _last_key{std::move(key)} {}
/// API: base/ConfigBase::DictFieldProxy::operator[]&
///
/// 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.
///
/// Inputs:
/// - `subkey` -- searches through the dict this requested field
///
/// Outputs:
/// - `DictFieldProxy` -- Returns a copied proxy object
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;
}
/// API: base/ConfigBase::DictFieldProxy::operator[]&&
///
/// Same as above `operator[]&`, but when called on an rvalue reference we just mutate the
/// current proxy to the new dict path.
///
/// Inputs:
/// - `subkey` -- searches through the dict this requested field
///
/// Outputs:
/// - `DictFieldProxy&&` -- 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);
}
/// API: base/ConfigBase::DictFieldProxy::key
///
/// Returns a pointer to the (deepest level) key for this dict pair *if* a pair exists at
/// the given location, nullptr otherwise. This allows a caller to get a reference to the
/// actual key, rather than an ephemeral copy of the current key value.
///
/// Inputs: None
///
/// Outputs:
/// - `std::string*` -- Returns a pointer to the key if the pair exists
const std::string* key() const { return get_clean_pair().first; }
/// API: base/ConfigBase::DictFieldProxy::string
///
/// Returns a const pointer to the string if one exists at the given location, nullptr
/// otherwise.
///
/// Inputs: None
///
/// Outputs:
/// - `std::string*` -- Returns a pointer to the string if one exists
const std::string* string() const { return get_clean<std::string>(); }
/// API: base/ConfigBase::DictFieldProxy::uview
///
/// Returns the value as a std::span<const unsigned char>, if it exists and is a string;
/// nullopt otherwise.
///
/// Inputs: None
///
/// Outputs:
/// - `std::optional<std::span<const unsigned char>>` -- Returns a value as a view if it
/// exists
std::optional<std::span<const unsigned char>> uview() const {
if (auto* s = get_clean<std::string>())
return std::span<const unsigned char>{
reinterpret_cast<const unsigned char*>(s->data()), s->size()};
return std::nullopt;
}
/// API: base/ConfigBase::DictFieldProxy::string_view_or
///
/// 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.
///
/// Inputs:
/// - `fallback` -- this value will be returned if it the requested value doesn't exist
///
/// Outputs:
/// - `std::string_view` -- Returned string view
std::string_view string_view_or(std::string_view fallback) const {
if (auto* s = string())
return {*s};
return fallback;
}
/// API: base/ConfigBase::DictFieldProxy::string_or
///
/// Returns a copy of the value as a string, if it exists and is a string; returns
/// `fallback` otherwise.
///
/// Inputs:
/// - `fallback` -- this value will be returned if it the requested value doesn't exist
///
/// Outputs:
/// - `std::string` -- Returned string
std::string string_or(std::string fallback) const {
if (auto* s = string())
return *s;
return fallback;
}
/// API: base/ConfigBase::DictFieldProxy::integer
///
/// Returns a const pointer to the integer if one exists at the given location, nullptr
/// otherwise.
///
/// Inputs: None
///
/// Outputs:
/// - `int64_t*` -- Pointer to the integer if one exists
const int64_t* integer() const { return get_clean<int64_t>(); }
/// API: base/ConfigBase::DictFieldProxy::integer_or
///
/// Returns the value as an integer or a fallback if the value doesn't exist (or isn't an
/// integer).
///
/// Inputs:
/// - `fallback` -- this value will be returned if it the requested value doesn't exist
///
/// Outputs:
/// - `int64_t` -- Returned Integer
int64_t integer_or(int64_t fallback) const {
if (auto* i = integer())
return *i;
return fallback;
}
/// API: base/ConfigBase::DictFieldProxy::sys_time
///
/// Returns the integer value loaded into a seconds-since-epoch std::chrono::sys_seconds
/// value if an integer value exists at the given location, std::nullopt otherwise.
///
/// Inputs: None
///
/// Outputs:
/// - `std::optional<std::chrono::sys_time>` -- nullopt if the value doesn't exist (or isn't
/// an integer), otherwise the integer value loaded as a seconds-from-epoch.
std::optional<std::chrono::sys_seconds> sys_seconds() const {
if (const auto* i = integer())
return std::make_optional<std::chrono::sys_seconds>(std::chrono::seconds{*i});
return std::nullopt;
}
/// API: base/ConfigBase::DictFieldProxy::sys_time_or
///
/// Returns the value as a std::chrono::sys_time or a fallback if the value doesn't exist
/// (or isn't an integer).
///
/// Inputs:
/// - `fallback` -- this value will be returned if it the requested value doesn't exist
///
/// Outputs:
/// - `int64_t` -- Returned Integer
std::chrono::sys_seconds sys_seconds_or(std::chrono::sys_seconds fallback) const {
return sys_seconds().value_or(fallback);
}
/// API: base/ConfigBase::DictFieldProxy::set
///
/// Returns a const pointer to the set if one exists at the given location, nullptr
/// otherwise.
///
/// Inputs: None
///
/// Outputs:
/// - `config::set*` -- Returned pointer to the set if one exists
const config::set* set() const { return get_clean<config::set>(); }
/// API: base/ConfigBase::DictFieldProxy::dict
///
/// 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).
///
/// Inputs: None
///
/// Outputs:
/// - `config::dict*` -- Returned pointer to the dict if one exists
const config::dict* dict() const { return get_clean<config::dict>(); }
/// API: base/ConfigBase::DictFieldProxy::operator=(std::string&&)
///
/// 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.
///
/// Inputs:
/// - `value` -- replaces current value with given string
void operator=(std::string&& value) { assign_if_changed(std::move(value)); }
/// API: base/ConfigBase::DictFieldProxy::operator=(std::string_view)
///
/// Replaces the current value with the given string_view. 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 (this makes a copy).
///
/// Inputs:
/// - `value` -- replaces current value with given string view
void operator=(std::string_view value) { *this = std::string{value}; }
/// API: base/ConfigBase::DictFieldProxy::operator=(std::span)
///
/// Replaces the current value with the given std::span<const unsigned char>. 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 (this makes a copy).
///
/// Inputs:
/// - `value` -- replaces current value with given std::span<const unsigned char>
///
/// Same as above, but takes a std::span<const unsigned char>
void operator=(std::span<const unsigned char> value) {
*this = std::string{reinterpret_cast<const char*>(value.data()), value.size()};
}
/// API: base/ConfigBase::DictFieldProxy::operator=(int64_t)
///
/// Replaces the current value with the given integer. 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.
///
/// Inputs:
/// - `value` -- replaces current value with given integer
void operator=(int64_t value) { assign_if_changed(value); }
/// API: base/ConfigBase::DictFieldProxy::operator=(std::chrono::sys_seconds)
///
/// Replaces the current value with an integer containing the seconds-since-epoch of the
/// given system time point. 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.
///
/// Inputs:
/// - `value` -- replaces current value with given sys_seconds's time_since_epoch() value.
void operator=(std::chrono::sys_seconds value) {
assign_if_changed(static_cast<int64_t>(value.time_since_epoch().count()));
}
/// API: base/ConfigBase::DictFieldProxy::operator=(config::set)
///
/// Replaces the current value with the given set. 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.
///
/// Inputs:
/// - `value` -- replaces current value with given set
void operator=(config::set value) { assign_if_changed(std::move(value)); }
/// API: base/ConfigBase::DictFieldProxy::operator=(config::set)
///
/// Replaces the current value with the given dict. This often isn't needed because of how
/// other assignment operations work 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.
///
/// Inputs:
/// - `value` -- replaces current value with given dict
void operator=(config::dict value) { assign_if_changed(std::move(value)); }
/// API: base/ConfigBase::DictFieldProxy::exists
///
/// 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`.
///
/// Inputs: None
///
/// Outputs:
/// - `bool` -- True if there is a value at the current key
template <typename T = dict_value, typename = std::enable_if_t<is_dict_value<T>>>
bool exists() const {
return get_clean<T>() != nullptr;
}
/// API: base/ConfigBase::DictFieldProxy::exists
///
/// Alias for `exists<T>()`
///
/// Inputs: None
///
/// Outputs:
/// - `bool` -- True if there is a value at the current key
template <typename T>
bool is() const {
return exists<T>();
}
/// API: base/ConfigBase::DictFieldProxy::erase
///
/// 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.
///
/// Inputs: None
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);
}
/// API: base/ConfigBase::DictFieldProxy::set_insert(std::string)
///
/// 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.
///
/// Inputs:
/// - `value` -- The value to be set
bool set_insert(std::string_view value) {
return insert_if_missing(config::scalar{std::string{value}});
}
/// API: base/ConfigBase::DictFieldProxy::set_insert(int64_t)
///
/// 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.
///
/// Inputs:
/// - `value` -- The value to be set
bool set_insert(int64_t value) { return insert_if_missing(config::scalar{value}); }
/// API: base/ConfigBase::DictFieldProxy::set_erase(std::string_view)
///
/// 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.
///
/// Inputs:
/// - `value` -- The value to be set
///
/// Outputs:
/// - `bool` -- True if an element was erased, false otherwise
bool set_erase(std::string_view value) {
return set_erase_impl(config::scalar{std::string{value}});
}
/// API: base/ConfigBase::DictFieldProxy::set_erase(int64_t)
///
/// 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.
///
/// Inputs:
/// - `value` -- The value to be set
///
/// Outputs:
/// - `bool` -- True if an element was erased, false otherwise
bool set_erase(int64_t value) { return set_erase_impl(scalar{value}); }
/// API: base/ConfigBase::DictFieldProxy::emplace
///
/// 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.
///
/// Inputs:
/// - `args` -- Value to be emplaced at current location
///
/// Outputs:
/// - `T&` -- Returns a reference to the templated type
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} {}
/// API: base/ConfigBase::DictFieldRoot::operator[]
///
/// 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).
///
/// Inputs:
/// - `key` -- Access a dict element with this key
///
/// Outputs:
/// - `DictFieldProxy` -- Returns a proxy object for accessing the value
DictFieldProxy operator[](std::string key) const& {
return DictFieldProxy{_conf, std::move(key)};
}
};
protected:
/// API: base/ConfigBase::_merge
///
/// Internal implementation of merge. This takes all of the messages pulled down from the server
/// and does whatever is necessary to merge (or replace) the current values.
///
/// Values are pairs of the message hash (as provided by the server) and the raw message body.
///
/// 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 (for example, because the data contained
/// conflicts that required another update to resolve).
///
/// Returns the ids of the given config messages that were successfully parsed.
///
/// Will throw on serious error (i.e. if neither the current nor any of the given configs are
/// parseable). This should not happen (the current config, at least, should always be
/// re-parseable).
///
/// Inputs:
/// - `configs` -- span of pairs containing the message hash and the raw message body
///
/// Outputs:
/// - unordered_set of successfully parsed hashes. Note that this does not mean the hash was
/// recent or that it changed the config, merely that the returned hash was properly parsed
/// and processed as a config message, even if it was too old to be useful (or was already
/// known to be included).
std::unordered_set<std::string> _merge(
std::span<const std::pair<std::string, std::span<const unsigned char>>> configs);
/// API: base/ConfigBase::extra_data
///
/// Called when dumping to obtain any extra data that a subclass needs to store to reconstitute
/// the object. The base implementation does nothing (i.e. extra data will be an empty dict).
/// 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 is stored in the "+" key of
/// the dump.
///
/// Note that loading extra properly requires two-step construction: the subclass constructor
/// must construct the ConfigBase object without extra data, and then call `init_from_dump()`
/// from within its own constructor to load the dump. Failing to do this two-step
/// initialization will result in the subclass load_extra_data not being called (because the
/// subclass instance, and thus the overridden method, does not yet exist during the ConfigBase
/// constructor).
///
/// Inputs:
/// - `extra` -- An empty dict producer into which extra data can be added.
virtual void extra_data(oxenc::bt_dict_producer&&) const {}
/// API: base/ConfigBase::load_extra_data
///
/// Called when constructing from a dump with the extra data dict. The base implementation does
/// nothing. See extra_data() for a description.
///
/// Inputs:
/// - `extra` -- bt_dict_consumer over the extra data subdict.
virtual void load_extra_data(oxenc::bt_dict_consumer&&) {}
/// API: base/ConfigBase::load_key
///
/// Called to load an ed25519 key for encryption; this is meant for use by single-ownership
/// config types, like UserProfile, but not shared config types (groups).
///
/// Takes a binary string which is either the 32-byte seed, or 64-byte libsodium secret (which
/// is just the seed and pubkey concatenated together), and then calls `key(...)` with the seed.
/// Throws std::invalid_argument if given something that doesn't match the required input.
///
/// Inputs:
/// - `ed25519_secret_key` -- key is loaded for encryption
void load_key(std::span<const unsigned char> ed25519_secretkey);
public:
virtual ~ConfigBase() = default;
// Object is non-movable and non-copyable; you need to hold it in a smart pointer if it needs to
// be managed.
ConfigBase(ConfigBase&&) = delete;
ConfigBase(const ConfigBase&) = delete;
ConfigBase& operator=(ConfigBase&&) = delete;
ConfigBase& operator=(const ConfigBase&) = delete;
// Proxy class providing read and write access to the contained config data.
const DictFieldRoot data{*this};
/// API: base/ConfigBase::storage_namespace
///
/// Accesses the storage namespace where this config type is to be stored/loaded from. See
/// namespaces.hpp for the underlying integer values.
///
/// Inputs: None
///
/// Outputs:
/// - `Namespace` -- Returns the namespace where config type is stored/loaded
virtual Namespace storage_namespace() const = 0;
/// API: base/ConfigBase::encryption_domain
///
/// Subclasses must override this to return a constant string that is unique per config type;
/// this value is used for domain separation in encryption. The string length must be between 1
/// and 24 characters; use the class name (e.g. "UserProfile") unless you have something better