| // Copyright 2020 The Pigweed Authors |
| // |
| // Licensed under the Apache License, Version 2.0 (the "License"); you may not |
| // use this file except in compliance with the License. You may obtain a copy of |
| // the License at |
| // |
| // https://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, software |
| // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT |
| // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the |
| // License for the specific language governing permissions and limitations under |
| // the License. |
| #pragma once |
| |
| #include <cstddef> |
| #include <cstdint> |
| #include <limits> |
| #include <string_view> |
| #include <type_traits> |
| |
| #include "pw_kvs/flash_memory.h" |
| #include "pw_span/span.h" |
| #include "pw_status/status.h" |
| #include "pw_status/status_with_size.h" |
| |
| namespace pw::kvs { |
| namespace cfg { |
| |
| // KVS requires a temporary buffer for some operations, this config allows |
| // tuning the buffer size. This is a trade-off between a value which is large |
| // and therefore requires more RAM, or having a value which is small which will |
| // result in some operations taking longer, as the operations are broken into |
| // smaller chunks. |
| // NOTE: This value can not be smaller then the flash alignment, and it will |
| // round the size down to be a multiple of the flash alignment for all |
| // operations. |
| inline constexpr size_t kKvsBufferSize = 64; |
| |
| // This represents the maximum amount of keys which can be in the KVS at any |
| // given time. |
| inline constexpr uint8_t kKvsMaxKeyCount = 50; |
| |
| // This is the maximum amount of sectors the KVS can operate on, an invalid |
| // value will cause an error during enable. |
| inline constexpr uint32_t kKvsMaxSectorCount = 20; |
| |
| } // namespace cfg |
| |
| namespace internal { |
| |
| // Traits class to detect if the type is a span. std::is_same is insufficient |
| // because span is a class template. This is used to ensure that the correct |
| // overload of the Put function is selected. |
| template <typename> |
| struct IsSpan : std::false_type {}; |
| |
| template <typename T, size_t kExtent> |
| struct IsSpan<span<T, kExtent>> : std::true_type {}; |
| |
| } // namespace internal |
| |
| // This object is very large (can be over 1500 B, depending on configuration) |
| // and should not typically be placed on the stack. |
| class KeyValueStore { |
| public: |
| constexpr KeyValueStore(FlashPartition* partition) : partition_(*partition) {} |
| |
| KeyValueStore(const KeyValueStore&) = delete; |
| KeyValueStore& operator=(const KeyValueStore&) = delete; |
| |
| // Enable the KVS, scans the sectors of the partition for any current KVS |
| // data. Erases and initializes any sectors which are not initialized. |
| // Checks the CRC of all data in the KVS; on failure the corrupted data is |
| // lost and Enable will return Status::DATA_LOSS, but the KVS will still load |
| // other data and will be enabled. |
| Status Enable(); |
| |
| bool IsEnabled() const { return enabled_; } |
| |
| void Disable() { |
| if (enabled_ == false) { |
| return; |
| } |
| // TODO: LOCK MUTEX |
| enabled_ = false; |
| } |
| |
| // Erase a key value element. |
| // key is a null terminated c string. |
| // Returns OK if erased |
| // NOT_FOUND if key was not found. |
| Status Erase(const std::string_view& key); |
| |
| // Copy the first size bytes of key's value into value buffer. |
| // key is a null terminated c string. Size is the amount of bytes to read, |
| // Optional offset argument supports reading size bytes starting at the |
| // specified offset. |
| // Returns OK if successful |
| // NOT_FOUND if key was not found |
| // DATA_LOSS if the value failed crc check |
| Status Get(const std::string_view& key, |
| const span<std::byte>& value, |
| uint16_t offset = 0); |
| |
| // Interpret the key's value as the given type and return it. |
| // Returns OK if successful |
| // NOT_FOUND if key was not found |
| // DATA_LOSS if the value failed crc check |
| // INVALID_ARGUMENT if size of value != sizeof(T) |
| template <typename T> |
| Status Get(const std::string_view& key, T* value) { |
| static_assert(std::is_trivially_copyable<T>(), "KVS values must copyable"); |
| static_assert(!std::is_pointer<T>(), "KVS values cannot be pointers"); |
| |
| StatusWithSize result = GetValueSize(key); |
| if (!result.ok()) { |
| return result.status(); |
| } |
| if (result.size() != sizeof(T)) { |
| return Status::INVALID_ARGUMENT; |
| } |
| return Get(key, as_writable_bytes(span(value, 1))); |
| } |
| |
| // Writes the key value store to the partition. If the key already exists it |
| // will be deleted before writing the new value. |
| // |
| // Returns OK if successful |
| // RESOURCE_EXHAUSTED if there is not enough available space |
| Status Put(const std::string_view& key, const span<const std::byte>& value); |
| |
| // Alternate Put function that takes an object. The object must be trivially |
| // copyable and cannot be a pointer or span. |
| template <typename T, |
| typename = std::enable_if_t<std::is_trivially_copyable_v<T> && |
| !std::is_pointer_v<T> && |
| !internal::IsSpan<T>()>> |
| Status Put(const std::string_view& key, const T& value) { |
| return Put(key, as_bytes(span(&value, 1))); |
| } |
| |
| // Gets the size of the value stored for provided key. |
| // Returns OK if successful |
| // INVALID_ARGUMENT if args are invalid. |
| // FAILED_PRECONDITION if KVS is not enabled. |
| // NOT_FOUND if key was not found. |
| StatusWithSize GetValueSize(const std::string_view& key); |
| |
| // Tests if the proposed key value entry can be stored in the KVS. |
| bool CanFitEntry(uint16_t key_len, uint16_t value_len) { |
| return kSectorInvalid != FindSpace(ChunkSize(key_len, value_len)); |
| } |
| |
| // CleanAll cleans each sector which is currently marked for cleaning. |
| // Note: if any data is invalid/corrupt it could be lost. |
| Status CleanAll() { |
| // TODO: LOCK MUTEX |
| return CleanAllInternal(); |
| } |
| size_t PendingCleanCount() { |
| // TODO: LOCK MUTEX |
| size_t ret = 0; |
| for (size_t i = 0; i < SectorCount(); i++) { |
| ret += sector_space_remaining_[i] == 0 ? 1 : 0; |
| } |
| return ret; |
| } |
| |
| // Clean a single sector and return, if all sectors are clean it will set |
| // all_sectors_have_been_cleaned to true and return. |
| Status CleanOneSector(bool* all_sectors_have_been_cleaned); |
| |
| // For debugging, logging, and testing. (Don't use in regular code) |
| // Note: a key_index is not valid after an element is erased or updated. |
| size_t KeyCount() const; |
| std::string_view GetKey(size_t idx) const; |
| size_t GetValueSize(size_t idx) const; |
| size_t GetMaxKeys() const { return kListCapacityMax; } |
| bool HasEmptySector() const { return HasEmptySectorImpl(kSectorInvalid); } |
| |
| static constexpr size_t kHeaderSize = 8; // Sector and KVS Header size |
| static constexpr uint16_t MaxValueLength() { return kChunkValueLengthMax; } |
| |
| private: |
| using KeyIndex = uint8_t; |
| using SectorIndex = uint32_t; |
| |
| static constexpr uint16_t kVersion = 1; |
| static constexpr KeyIndex kListCapacityMax = cfg::kKvsMaxKeyCount; |
| static constexpr SectorIndex kSectorCountMax = cfg::kKvsMaxSectorCount; |
| |
| // Number of bits for fields in KVSHeader: |
| static constexpr int kChunkHeaderKeyFieldNumBits = 4; |
| static constexpr int kChunkHeaderChunkFieldNumBits = 12; |
| |
| static constexpr uint16_t kSectorReadyValue = 0xABCD; |
| static constexpr uint16_t kChunkSyncValue = 0x55AA; |
| |
| static constexpr uint16_t kChunkLengthMax = |
| ((1 << kChunkHeaderChunkFieldNumBits) - 1); |
| static constexpr uint16_t kChunkKeyLengthMax = |
| ((1 << kChunkHeaderKeyFieldNumBits) - 1); |
| static constexpr uint16_t kChunkValueLengthMax = |
| (kChunkLengthMax - kChunkKeyLengthMax); |
| |
| static constexpr SectorIndex kSectorInvalid = 0xFFFFFFFFul; |
| static constexpr FlashPartition::Address kAddressInvalid = 0xFFFFFFFFul; |
| static constexpr uint64_t kSectorCleanNotPending = 0xFFFFFFFFFFFFFFFFull; |
| |
| static constexpr uint16_t kFlagsIsErasedMask = 0x0001; |
| static constexpr uint16_t kMaxAlignmentBytes = 128; |
| |
| // This packs into 16 bytes. |
| struct KvsSectorHeaderMeta { |
| uint16_t synchronize_token; |
| uint16_t version; |
| uint16_t alignment_bytes; // alignment used for each chunk in this sector. |
| uint16_t padding; // padding to support uint64_t alignment. |
| }; |
| static_assert(sizeof(KvsSectorHeaderMeta) == kHeaderSize, |
| "Invalid KvsSectorHeaderMeta size"); |
| |
| // sector_clean_pending is broken off from KvsSectorHeaderMeta to support |
| // larger than sizeof(KvsSectorHeaderMeta) flash write alignments. |
| struct KvsSectorHeaderCleaning { |
| // When this sector is not marked for cleaning this will be in the erased |
| // state. When marked for clean this value indicates the order the sectors |
| // were marked for cleaning, and therfore which data is the newest. |
| // To remain backwards compatible with v2 and v3 KVS this is 8 bytes, if the |
| // alignment is greater than 8 we will check the entire alignment is in the |
| // default erased state. |
| uint64_t sector_clean_order; |
| }; |
| static_assert(sizeof(KvsSectorHeaderCleaning) == kHeaderSize, |
| "Invalid KvsSectorHeaderCleaning size"); |
| |
| // This packs into 8 bytes, to support uint64_t alignment. |
| struct KvsHeader { |
| uint16_t synchronize_token; |
| uint16_t crc; // Crc of the key + data (Ignoring any padding bytes) |
| // flags is a Bitmask: bits 15-1 reserved = 0, |
| // bit 0: is_erased [0 when not erased, 1 when erased] |
| uint16_t flags; |
| // On little endian the length fields map to memory as follows: |
| // byte array: [ 0x(c0to3 k0to3) 0x(c8to11 c4to7) ] |
| // Key length does not include trailing zero. That is not stored. |
| uint16_t key_len : kChunkHeaderKeyFieldNumBits; |
| // Chunk length is the total length of the chunk before alignment. |
| // That way we can work out the length of the value as: |
| // (chunk length - key length - size of chunk header). |
| uint16_t chunk_len : kChunkHeaderChunkFieldNumBits; |
| }; |
| static_assert(sizeof(KvsHeader) == kHeaderSize, "Invalid KvsHeader size"); |
| |
| struct KeyMap { |
| std::string_view key() const { return {key_buffer, key_length}; } |
| |
| FlashPartition::Address address; |
| char key_buffer[kChunkKeyLengthMax + 1]; // +1 for null terminator |
| uint8_t key_length; |
| bool is_erased; |
| uint16_t chunk_len; |
| |
| static_assert(kChunkKeyLengthMax <= |
| std::numeric_limits<decltype(key_length)>::max()); |
| }; |
| |
| static constexpr bool InvalidKey(const std::string_view& key) { |
| return key.empty() || key.size() > kChunkKeyLengthMax; |
| } |
| |
| // NOTE: All public APIs handle the locking, the internal methods assume the |
| // lock has already been acquired. |
| |
| SectorIndex AddressToSectorIndex(FlashPartition::Address address) const { |
| return address / partition_.GetSectorSizeBytes(); |
| } |
| |
| FlashPartition::Address SectorIndexToAddress(SectorIndex index) const { |
| return index * partition_.GetSectorSizeBytes(); |
| } |
| |
| // Returns kAddressInvalid if no space is found, otherwise the address. |
| FlashPartition::Address FindSpace(size_t requested_size) const; |
| |
| // Attempts to rewrite a key's value by appending the new value to the same |
| // sector. If the sector is full the value is written to another sector, and |
| // the sector is marked for cleaning. |
| // Returns RESOURCE_EXHAUSTED if no space is available, OK otherwise. |
| Status RewriteValue(KeyIndex key_index, |
| const span<const std::byte>& value, |
| bool is_erased = false); |
| |
| bool ValueMatches(KeyIndex key_index, |
| const span<const std::byte>& value, |
| bool is_erased); |
| |
| // ResetSector erases the sector and writes the sector header. |
| Status ResetSector(SectorIndex sector_index); |
| Status WriteKeyValue(FlashPartition::Address address, |
| const std::string_view& key, |
| const span<const std::byte>& value, |
| bool is_erased = false); |
| uint32_t SectorSpaceRemaining(SectorIndex sector_index) const; |
| |
| // Returns idx if key is found, otherwise kListCapacityMax. |
| KeyIndex FindKeyInMap(const std::string_view& key) const; |
| bool IsKeyInMap(const std::string_view& key) const { |
| return FindKeyInMap(key) != kListCapacityMax; |
| } |
| |
| void RemoveFromMap(KeyIndex key_index); |
| Status AppendToMap(const std::string_view& key, |
| FlashPartition::Address address, |
| size_t chunk_len, |
| bool is_erased = false); |
| void UpdateMap(KeyIndex key_index, |
| FlashPartition::Address address, |
| uint16_t chunk_len, |
| bool is_erased = false); |
| uint16_t CalculateCrc(const std::string_view& key, |
| const span<const std::byte>& value) const; |
| |
| // Calculates the CRC by reading the value from flash in chunks. |
| Status CalculateCrcFromValueAddress(const std::string_view& key, |
| FlashPartition::Address value_address, |
| uint16_t value_size, |
| uint16_t* crc_ret); |
| |
| uint16_t RoundUpForAlignment(uint16_t size) const { |
| if (size % alignment_bytes_ != 0) { |
| return size + alignment_bytes_ - size % alignment_bytes_; |
| } |
| return size; |
| } |
| |
| // The buffer is chosen to be no larger then the config value, and |
| // be a multiple of the flash alignment. |
| size_t TempBufferAlignedSize() const { |
| CHECK_GE(sizeof(temp_buffer_), partition_.GetAlignmentBytes()); |
| return sizeof(temp_buffer_) - |
| (sizeof(temp_buffer_) % partition_.GetAlignmentBytes()); |
| } |
| |
| // Moves a key/value chunk from one address to another, all fields expected to |
| // be aligned. |
| Status MoveChunk(FlashPartition::Address dest_address, |
| FlashPartition::Address src_address, |
| uint16_t size); |
| |
| // Size of a chunk including header, key, value, and alignment padding. |
| size_t ChunkSize(size_t key_length, size_t value_length) const { |
| return RoundUpForAlignment(sizeof(KvsHeader)) + |
| RoundUpForAlignment(key_length) + RoundUpForAlignment(value_length); |
| } |
| |
| // Sectors should be cleaned when full, every valid (Most recent, not erased) |
| // chunk of data is moved to another sector and the sector is erased. |
| // TODO: Clean sectors with lots of invalid data, without a rewrite |
| // or erase triggering it. |
| Status MarkSectorForClean(SectorIndex sector); |
| Status CleanSector(SectorIndex sector); |
| Status CleanAllInternal(); |
| Status GarbageCollectImpl(bool clean_pending_sectors, |
| bool exit_when_have_free_sector); |
| Status FullGarbageCollect(); |
| Status EnforceFreeSector(); |
| |
| SectorIndex SectorCount() const { |
| return std::min(partition_.GetSectorCount(), kSectorCountMax); |
| } |
| |
| size_t SectorSpaceAvailableWhenEmpty() const { |
| return partition_.GetSectorSizeBytes() - |
| RoundUpForAlignment(sizeof(KvsSectorHeaderMeta)) - |
| RoundUpForAlignment(sizeof(KvsSectorHeaderCleaning)); |
| } |
| |
| bool HasEmptySectorImpl(SectorIndex skip_sector) const { |
| for (SectorIndex i = 0; i < SectorCount(); i++) { |
| if (i != skip_sector && |
| sector_space_remaining_[i] == SectorSpaceAvailableWhenEmpty()) { |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| bool IsInLastFreeSector(FlashPartition::Address address) { |
| return sector_space_remaining_[AddressToSectorIndex(address)] == |
| SectorSpaceAvailableWhenEmpty() && |
| !HasEmptySectorImpl(AddressToSectorIndex(address)); |
| } |
| |
| FlashPartition& partition_; |
| // TODO: MUTEX |
| bool enabled_ = false; |
| uint8_t alignment_bytes_ = 0; |
| uint64_t next_sector_clean_order_ = 0; |
| |
| // Free space available in each sector, set to 0 when clean is pending/active |
| uint32_t sector_space_remaining_[kSectorCountMax] = {0}; |
| uint64_t sector_clean_order_[kSectorCountMax] = {kSectorCleanNotPending}; |
| KeyMap key_map_[kListCapacityMax] = {}; |
| KeyIndex map_size_ = 0; |
| |
| // Add +1 for a null terminator. The keys are used as string_views, but the |
| // null-terminator provides additional safetly. |
| char temp_key_buffer_[kChunkKeyLengthMax + 1u] = {0}; |
| uint8_t temp_buffer_[cfg::kKvsBufferSize] = {0}; |
| }; |
| |
| } // namespace pw::kvs |