/*
|
|
|
|
Part of the SETTINGS MODULE
|
|
|
|
Copyright (C) 2020 by Maxim Prokhorov <prokhorov dot max at outlook dot com>
|
|
|
|
Reimplementation of the Embedis storage format:
|
|
- https://github.com/thingSoC/embedis
|
|
|
|
*/
|
|
|
|
#pragma once
|
|
|
|
#include <Arduino.h>
|
|
|
|
#include <algorithm>
|
|
#include <memory>
|
|
#include <vector>
|
|
|
|
#include "settings_helpers.h"
|
|
#include "libs/TypeChecks.h"
|
|
|
|
namespace settings {
|
|
namespace embedis {
|
|
|
|
// Sum total is calculated from:
|
|
// - 4 bytes to store length of 2 values (stored as big-endian)
|
|
// - N bytes of values themselves
|
|
// We can't save empty keys but can save empty values as just 2 length bytes
|
|
inline size_t estimate(const String& key, const String& value) {
|
|
if (!key.length()) {
|
|
return 0;
|
|
}
|
|
|
|
return (4 + key.length() + value.length());
|
|
}
|
|
|
|
// Note: KeyValueStore is templated to avoid having to provide RawStorageBase via virtual inheritance.
|
|
|
|
template <typename RawStorageBase>
|
|
class KeyValueStore {
|
|
|
|
// -----------------------------------------------------------------------------------
|
|
// Notice: we can only use sfinae checks with the current compiler version
|
|
|
|
// TODO: provide actual benchmark comparison with 'lambda'-list-as-vtable (old Embedis style)
|
|
// and vtable approach (write(), read() and commit() as pure virtual)
|
|
// TODO: consider overrides for bulk operations like move (see ::del method)
|
|
|
|
template <typename T>
|
|
using storage_can_write_t = decltype(std::declval<T>().write(
|
|
std::declval<uint16_t>(), std::declval<uint8_t>()));
|
|
template <typename T>
|
|
using storage_can_write = is_detected<storage_can_write_t, T>;
|
|
|
|
template <typename T>
|
|
using storage_can_read_t = decltype(std::declval<T>().read(std::declval<uint16_t>()));
|
|
template <typename T>
|
|
using storage_can_read = is_detected<storage_can_read_t, T>;
|
|
|
|
template <typename T>
|
|
using storage_can_commit_t = decltype(std::declval<T>().commit());
|
|
template <typename T>
|
|
using storage_can_commit = is_detected<storage_can_commit_t, T>;
|
|
|
|
static_assert(
|
|
(storage_can_commit<RawStorageBase>{} &&
|
|
storage_can_read<RawStorageBase>{} &&
|
|
storage_can_write<RawStorageBase>{}),
|
|
"Storage class must implement read(index), write(index, byte) and commit()"
|
|
);
|
|
|
|
// -----------------------------------------------------------------------------------
|
|
|
|
protected:
|
|
|
|
// Tracking state of the parser inside of _raw_read()
|
|
enum class State {
|
|
Begin,
|
|
End,
|
|
LenByte1,
|
|
LenByte2,
|
|
Value
|
|
};
|
|
|
|
// Pointer to the region of data that we are using
|
|
//
|
|
// XXX: It does not matter right now, but we **will** overflow position when using sizes >= (2^16) - 1
|
|
// Note: Implementation is also in the header b/c c++ won't allow us
|
|
// to have a plain member (not a ptr or ref) of unknown size.
|
|
// Note: There was a considiration to implement this as 'stashing iterator' to be compatible with stl algorithms.
|
|
// In such implementation, we would store intermediate index and allow the user to receive a `value_proxy`,
|
|
// temporary returned by `value_proxy& operator*()' that is bound to Cursor instance.
|
|
// This **will** cause problems with 'reverse_iterator' or anything like it, as it expects reference to
|
|
// outlive the iterator object (specifically, result of `return *--tmp`, where `tmp` is created inside of a function block)
|
|
struct Cursor {
|
|
|
|
Cursor(RawStorageBase& storage, uint16_t position_, uint16_t begin_, uint16_t end_) :
|
|
position(position_),
|
|
begin(begin_),
|
|
end(end_),
|
|
_storage(storage)
|
|
{}
|
|
|
|
Cursor(RawStorageBase& storage, uint16_t begin_, uint16_t end_) :
|
|
Cursor(storage, 0, begin_, end_)
|
|
{}
|
|
|
|
explicit Cursor(RawStorageBase& storage) :
|
|
Cursor(storage, 0, 0, 0)
|
|
{}
|
|
|
|
static Cursor merge(RawStorageBase& storage, const Cursor& key, const Cursor& value) {
|
|
return Cursor(storage, key.begin, value.end);
|
|
}
|
|
|
|
static Cursor fromEnd(RawStorageBase& storage, uint16_t begin, uint16_t end) {
|
|
return Cursor(storage, end - begin, begin, end);
|
|
}
|
|
|
|
Cursor() = delete;
|
|
|
|
void reset(uint16_t begin_, uint16_t end_) {
|
|
position = 0;
|
|
begin = begin_;
|
|
end = end_;
|
|
}
|
|
|
|
uint8_t read() {
|
|
return _storage.read(begin + position);
|
|
}
|
|
|
|
void write(uint8_t value) {
|
|
_storage.write(begin + position, value);
|
|
}
|
|
|
|
void resetBeginning() {
|
|
position = 0;
|
|
}
|
|
|
|
void resetEnd() {
|
|
position = end - begin;
|
|
}
|
|
|
|
size_t size() {
|
|
return (end - begin);
|
|
}
|
|
|
|
bool inRange(uint16_t position_) {
|
|
return (position_ < (end - begin));
|
|
}
|
|
|
|
operator bool() {
|
|
return inRange(position);
|
|
}
|
|
|
|
uint8_t operator[](size_t position_) const {
|
|
return _storage.read(begin + position_);
|
|
}
|
|
|
|
bool operator ==(const Cursor& other) {
|
|
return (begin == other.begin) && (end == other.end);
|
|
}
|
|
|
|
bool operator !=(const Cursor& other) {
|
|
return !(*this == other);
|
|
}
|
|
|
|
Cursor& operator++() {
|
|
++position;
|
|
return *this;
|
|
}
|
|
|
|
Cursor operator++(int) {
|
|
Cursor other(*this);
|
|
++*this;
|
|
return other;
|
|
}
|
|
|
|
Cursor& operator--() {
|
|
--position;
|
|
return *this;
|
|
}
|
|
|
|
Cursor operator--(int) {
|
|
Cursor other(*this);
|
|
--*this;
|
|
return other;
|
|
}
|
|
|
|
uint16_t position;
|
|
uint16_t begin;
|
|
uint16_t end;
|
|
|
|
private:
|
|
|
|
RawStorageBase& _storage;
|
|
|
|
};
|
|
|
|
public:
|
|
|
|
// Store value location in a more reasonable forward-iterator-style manner
|
|
// Allows us to skip string creation when just searching for specific values
|
|
// XXX: be cautious that cursor positions **will** break when underlying storage changes
|
|
struct ReadResult {
|
|
|
|
friend class KeyValueStore<RawStorageBase>;
|
|
|
|
ReadResult(const Cursor& cursor_) :
|
|
length(0),
|
|
cursor(cursor_),
|
|
result(false)
|
|
{}
|
|
|
|
ReadResult(RawStorageBase& storage) :
|
|
length(0),
|
|
cursor(storage),
|
|
result(false)
|
|
{}
|
|
|
|
operator bool() {
|
|
return result;
|
|
}
|
|
|
|
String read() {
|
|
String out;
|
|
out.reserve(length);
|
|
if (!length) {
|
|
return out;
|
|
}
|
|
|
|
decltype(length) index = 0;
|
|
cursor.resetBeginning();
|
|
while (index < length) {
|
|
out += static_cast<char>(cursor.read());
|
|
++cursor;
|
|
++index;
|
|
}
|
|
|
|
return out;
|
|
}
|
|
|
|
uint16_t length;
|
|
|
|
private:
|
|
|
|
Cursor cursor;
|
|
bool result;
|
|
|
|
};
|
|
|
|
// Internal storage consists of sequences of <byte-range><length>
|
|
struct KeyValueResult {
|
|
explicit operator bool() {
|
|
return (key) && (value) && (key.length > 0);
|
|
}
|
|
|
|
bool operator !() {
|
|
return !(static_cast<bool>(*this));
|
|
}
|
|
|
|
template <typename T = ReadResult>
|
|
KeyValueResult(T&& key_, T&& value_) :
|
|
key(std::forward<T>(key_)),
|
|
value(std::forward<T>(value_))
|
|
{}
|
|
|
|
KeyValueResult(RawStorageBase& storage) :
|
|
key(storage),
|
|
value(storage)
|
|
{}
|
|
|
|
ReadResult key;
|
|
ReadResult value;
|
|
};
|
|
|
|
// one and only possible constructor, simply move the class object into the
|
|
// member variable to avoid forcing the user of the API to keep 2 objects alive.
|
|
KeyValueStore(RawStorageBase&& storage, uint16_t begin, uint16_t end) :
|
|
_storage(std::move(storage)),
|
|
_cursor(_storage, begin, end),
|
|
_state(State::Begin)
|
|
{}
|
|
|
|
// Try to find the matching key. Datastructure that we use does not specify
|
|
// any value 'type' inside of it. We expect 'key' to be the first non-empty string,
|
|
// 'value' can be empty.
|
|
ValueResult get(const String& key) {
|
|
return _get(key, true);
|
|
}
|
|
|
|
bool has(const String& key) {
|
|
return static_cast<bool>(_get(key, false));
|
|
}
|
|
|
|
// We going be using this pattern all the time here, because we need 2 consecutive **valid** ranges
|
|
// TODO: expose _read_kv() and _cursor_reset_end() so we can have 'break' here?
|
|
// perhaps as a wrapper object, allow something like next() and seekBegin()
|
|
template <typename CallbackType>
|
|
void foreach(CallbackType callback) {
|
|
_cursor_reset_end();
|
|
do {
|
|
auto kv = _read_kv();
|
|
if (!kv) {
|
|
break;
|
|
}
|
|
callback(std::move(kv));
|
|
} while (_state != State::End);
|
|
}
|
|
|
|
// read every key into a vector
|
|
std::vector<String> keys() {
|
|
std::vector<String> out;
|
|
out.reserve(count());
|
|
|
|
foreach([&](KeyValueResult&& kv) {
|
|
out.push_back(kv.key.read());
|
|
});
|
|
|
|
return out;
|
|
}
|
|
|
|
// set or update key with value contents. ensure 'key' isn't empty, 'value' can be empty
|
|
bool set(const String& key, const String& value) {
|
|
|
|
// ref. 'estimate()' implementation in regards to the storage calculation
|
|
auto need = estimate(key, value);
|
|
if (!need) {
|
|
return false;
|
|
}
|
|
|
|
auto key_len = key.length();
|
|
auto value_len = value.length();
|
|
|
|
Cursor to_erase(_storage);
|
|
bool need_erase = false;
|
|
|
|
// we need the position at the 'end' of the free space
|
|
auto start_pos = _cursor_reset_end();
|
|
|
|
do {
|
|
auto kv = _read_kv();
|
|
if (!kv) {
|
|
break;
|
|
}
|
|
|
|
start_pos = kv.value.cursor.begin;
|
|
|
|
// in the very special case we can match the existing key, we either
|
|
if ((kv.key.length == key_len) && (kv.key.read() == key)) {
|
|
if (kv.value.length == value.length()) {
|
|
// - do nothing, as the value is already set
|
|
if (kv.value.read() == value) {
|
|
return true;
|
|
}
|
|
// - overwrite the space again, with the new kv of the same length
|
|
start_pos = kv.key.cursor.end;
|
|
break;
|
|
}
|
|
// - or, erase the existing kv and place new kv at the end
|
|
to_erase.reset(kv.value.cursor.begin, kv.key.cursor.end);
|
|
need_erase = true;
|
|
}
|
|
|
|
} while (_state != State::End);
|
|
|
|
if (need_erase) {
|
|
_raw_erase(start_pos, to_erase);
|
|
start_pos += to_erase.size();
|
|
}
|
|
|
|
// we should only insert when possition is still within possible size
|
|
if (start_pos && (start_pos >= need)) {
|
|
auto writer = Cursor::fromEnd(_storage, start_pos - need, start_pos);
|
|
|
|
// put the length of the value as 2 bytes and then write the data
|
|
(--writer).write(key_len & 0xff);
|
|
(--writer).write((key_len >> 8) & 0xff);
|
|
while (key_len--) {
|
|
(--writer).write(key[key_len]);
|
|
}
|
|
|
|
(--writer).write(value_len & 0xff);
|
|
(--writer).write((value_len >> 8) & 0xff);
|
|
|
|
while (value_len--) {
|
|
(--writer).write(value[value_len]);
|
|
}
|
|
|
|
// we also need to add an empty key *after* the value
|
|
// but, only when we still have some space left
|
|
if (writer.begin >= 2) {
|
|
_cursor_set_position(writer.begin - _cursor.begin);
|
|
auto next_kv = _read_kv();
|
|
if (!next_kv) {
|
|
auto empty = Cursor::fromEnd(_storage, writer.begin - 2, writer.begin);
|
|
(--empty).write(0);
|
|
(--empty).write(0);
|
|
}
|
|
}
|
|
|
|
_storage.commit();
|
|
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
// remove key from the storage. will check that 'key' argument isn't empty
|
|
bool del(const String& key) {
|
|
size_t key_len = key.length();
|
|
if (!key_len) {
|
|
return false;
|
|
}
|
|
|
|
// we should only compare strings of equal length.
|
|
// when matching, record { value ... key } range + 4 bytes for length
|
|
// continue searching for available keys and set start_pos and the 'end' of the free space
|
|
size_t start_pos = _cursor_reset_end();
|
|
auto to_erase = Cursor::fromEnd(_storage, _cursor.begin, _cursor.end);
|
|
|
|
foreach([&](KeyValueResult&& kv) {
|
|
start_pos = kv.value.cursor.begin;
|
|
if (!to_erase && (kv.key.length == key_len) && (kv.key.read() == key)) {
|
|
to_erase.reset(kv.value.cursor.begin, kv.key.cursor.end);
|
|
}
|
|
});
|
|
|
|
if (to_erase) {
|
|
_raw_erase(start_pos, to_erase);
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
// Simply count key-value pairs that we could parse
|
|
size_t count() {
|
|
size_t result = 0;
|
|
foreach([&result](KeyValueResult&&) {
|
|
++result;
|
|
});
|
|
|
|
return result;
|
|
}
|
|
|
|
// Do exactly the same thing as 'keys' does, but return the amount
|
|
// of bytes to the left of the last kv
|
|
size_t available() {
|
|
size_t result = _cursor.size();
|
|
foreach([&result](KeyValueResult&& kv) {
|
|
result -= kv.key.cursor.size();
|
|
result -= kv.value.cursor.size();
|
|
});
|
|
|
|
return result;
|
|
}
|
|
|
|
// How much bytes can be used is directly read from the cursor, based on begin and end values
|
|
size_t size() {
|
|
return _cursor.size();
|
|
}
|
|
|
|
protected:
|
|
|
|
// Try to find the matching key. Datastructure that we use does not specify
|
|
// any value 'type' inside of it. We expect 'key' to be the first non-empty string,
|
|
// 'value' can be empty.
|
|
// To implement has(), allow to skip reading the value
|
|
ValueResult _get(const String& key, bool read_value) {
|
|
ValueResult out;
|
|
auto len = key.length();
|
|
|
|
_cursor_reset_end();
|
|
|
|
do {
|
|
auto kv = _read_kv();
|
|
if (!kv) {
|
|
break;
|
|
}
|
|
|
|
// no point in comparing keys when length does not match
|
|
// (and we also don't want to allocate the string)
|
|
if (kv.key.length != len) {
|
|
continue;
|
|
}
|
|
|
|
auto key_result = kv.key.read();
|
|
if (key_result == key) {
|
|
out = read_value
|
|
? std::move(kv.value.read())
|
|
: String();
|
|
break;
|
|
}
|
|
} while (_state != State::End);
|
|
|
|
return out;
|
|
}
|
|
|
|
// Place cursor at the `end` and resets the parser to expect length byte
|
|
uint16_t _cursor_reset_end() {
|
|
_cursor.resetEnd();
|
|
_state = State::Begin;
|
|
return _cursor.end;
|
|
}
|
|
|
|
uint16_t _cursor_set_position(uint16_t position) {
|
|
_state = State::Begin;
|
|
_cursor.position = position;
|
|
return _cursor.position;
|
|
}
|
|
|
|
// implementation quirk is that `Cursor::operator=` won't work because of the `RawStorageBase&` member
|
|
// right now, just construct in place and assume that compiler will inline things
|
|
KeyValueResult _read_kv() {
|
|
auto key = _raw_read();
|
|
if (!key || !key.length) {
|
|
return {_storage};
|
|
}
|
|
|
|
auto value = _raw_read();
|
|
|
|
return {key, value};
|
|
};
|
|
|
|
void _raw_erase(size_t start_pos, Cursor& to_erase) {
|
|
// we either end up to the left or to the right of the boundary
|
|
|
|
size_t new_pos = (start_pos < to_erase.begin)
|
|
? (start_pos + to_erase.size())
|
|
: (to_erase.end);
|
|
|
|
if (start_pos < to_erase.begin) {
|
|
// shift storage to the right, overwriting over the now empty space
|
|
auto from = Cursor::fromEnd(_storage, start_pos, to_erase.begin);
|
|
auto to = Cursor::fromEnd(_storage, start_pos + to_erase.size(), to_erase.end);
|
|
|
|
while (--from && --to) {
|
|
to.write(from.read());
|
|
from.write(0xff);
|
|
}
|
|
} else {
|
|
// overwrite the now empty space with 0xff
|
|
to_erase.resetEnd();
|
|
while (--to_erase) {
|
|
to_erase.write(0xff);
|
|
}
|
|
}
|
|
|
|
// same as set(), add empty key as padding
|
|
auto empty = Cursor::fromEnd(_storage, new_pos - 2, new_pos);
|
|
(--empty).write(0);
|
|
(--empty).write(0);
|
|
|
|
_storage.commit();
|
|
}
|
|
|
|
// Returns Cursor to the region that holds the data
|
|
// Result object itself does not contain any data, we need to explicitly request it by calling read()
|
|
//
|
|
// Cursor object is always expected to point to something, e.g. minimum:
|
|
// 0x00 0x00
|
|
// len2 len1
|
|
// Position will be 0, end will be 2. Total length is 2, data length is 0
|
|
//
|
|
// Or, non-empty value:
|
|
// 0x01 0x00 0x01
|
|
// data len2 len1
|
|
// Position will be 0, end will be 3. Total length is 3, data length is 1
|
|
ReadResult _raw_read() {
|
|
uint16_t len = 0;
|
|
ReadResult out(_storage);
|
|
|
|
do {
|
|
// storage is written right-to-left, cursor is always decreasing
|
|
switch (_state) {
|
|
case State::Begin:
|
|
if (_cursor.position >= 2) {
|
|
--_cursor;
|
|
_state = State::LenByte1;
|
|
} else {
|
|
_state = State::End;
|
|
}
|
|
break;
|
|
// len is 16 bit uint (bigendian)
|
|
// special case is 0, which is valid and should be returned when encountered
|
|
// another special case is 0xffff, meaning we just hit an empty space
|
|
case State::LenByte1:
|
|
len = _cursor.read();
|
|
_state = State::LenByte2;
|
|
break;
|
|
case State::LenByte2:
|
|
{
|
|
uint8_t len2 = (--_cursor).read();
|
|
if ((0xff == len) && (0xff == len2)) {
|
|
_state = State::End;
|
|
} else {
|
|
len |= len2 << 8;
|
|
_state = State::Value;
|
|
}
|
|
break;
|
|
}
|
|
case State::Value: {
|
|
// ensure we don't go out-of-bounds
|
|
if (len && _cursor.position < len) {
|
|
_state = State::End;
|
|
break;
|
|
}
|
|
|
|
// and point at the beginning of the value
|
|
_cursor.position -= len;
|
|
auto value_start = (_cursor.begin + _cursor.position);
|
|
|
|
out.cursor.reset(value_start, value_start + len + 2);
|
|
out.length = len;
|
|
out.result = true;
|
|
|
|
_state = State::Begin;
|
|
|
|
goto return_result;
|
|
}
|
|
case State::End:
|
|
default:
|
|
break;
|
|
}
|
|
|
|
} while (_state != State::End);
|
|
|
|
return_result:
|
|
|
|
return out;
|
|
}
|
|
|
|
RawStorageBase _storage;
|
|
Cursor _cursor;
|
|
State _state { State::Begin };
|
|
};
|
|
|
|
} // namespace embedis
|
|
} // namespace settings
|