pw_kvs: Initial commit of key value store module
This commit does not build or pass presubmit checks.
Change-Id: I3d4dd393ede1c778888c3cd8be9f12dfbf92fb88
diff --git a/pw_kvs/flash.cc b/pw_kvs/flash.cc
new file mode 100644
index 0000000..bcc695f
--- /dev/null
+++ b/pw_kvs/flash.cc
@@ -0,0 +1,63 @@
+// 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.
+
+#include "pw_kvs/util/flash.h"
+
+#include "pw_kvs/config.h"
+
+namespace pw {
+
+Status PaddedWrite(FlashPartition* partition,
+ FlashPartition::Address address,
+ const uint8_t* buffer,
+ uint16_t size) {
+ RETURN_STATUS_IF(
+ address % partition->GetAlignmentBytes() ||
+ partition->GetAlignmentBytes() > cfg::kFlashUtilMaxAlignmentBytes,
+ Status::INVALID_ARGUMENT);
+ uint8_t alignment_buffer[cfg::kFlashUtilMaxAlignmentBytes] = {0};
+ uint16_t aligned_bytes = size - size % partition->GetAlignmentBytes();
+ RETURN_IF_ERROR(partition->Write(address, buffer, aligned_bytes));
+ uint16_t remaining_bytes = size - aligned_bytes;
+ if (remaining_bytes > 0) {
+ memcpy(alignment_buffer, &buffer[aligned_bytes], remaining_bytes);
+ RETURN_IF_ERROR(partition->Write(address + aligned_bytes,
+ alignment_buffer,
+ partition->GetAlignmentBytes()));
+ }
+ return Status::OK;
+}
+
+Status UnalignedRead(FlashPartition* partition,
+ uint8_t* buffer,
+ FlashPartition::Address address,
+ uint16_t size) {
+ RETURN_STATUS_IF(
+ address % partition->GetAlignmentBytes() ||
+ partition->GetAlignmentBytes() > cfg::kFlashUtilMaxAlignmentBytes,
+ Status::INVALID_ARGUMENT);
+ uint16_t aligned_bytes = size - size % partition->GetAlignmentBytes();
+ RETURN_IF_ERROR(partition->Read(buffer, address, aligned_bytes));
+ uint16_t remaining_bytes = size - aligned_bytes;
+ if (remaining_bytes > 0) {
+ uint8_t alignment_buffer[cfg::kFlashUtilMaxAlignmentBytes];
+ RETURN_IF_ERROR(partition->Read(alignment_buffer,
+ address + aligned_bytes,
+ partition->GetAlignmentBytes()));
+ memcpy(&buffer[aligned_bytes], alignment_buffer, remaining_bytes);
+ }
+ return Status::OK;
+}
+
+} // namespace pw
diff --git a/pw_kvs/key_value_store.cc b/pw_kvs/key_value_store.cc
new file mode 100644
index 0000000..ce21740
--- /dev/null
+++ b/pw_kvs/key_value_store.cc
@@ -0,0 +1,805 @@
+// 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.
+
+// KVS is a key-value storage format for flash based memory.
+//
+// Currently it stores key-value sets in chunks aligned with the flash memory.
+// Each chunk contains a header (KvsHeader), which is immediately followed by
+// the key, and then the data blob. To support different alignments both the
+// key length and value length are rounded up to be aligned.
+//
+// Example memory layout of sector with two KVS chunks:
+// [ SECTOR_HEADER - Meta | alignment_padding ]
+// [ SECTOR_HEADER - Cleaning State | alignment_padding ]
+// [First Chunk Header | alignment_padding ]
+// [First Chunk Key | alignment_padding ]
+// [First Chunk Value | alignment_padding ]
+// [Second Chunk Header | alignment_padding ]
+// [Second Chunk Key | alignment_padding ]
+// [Second Chunk Value | alignment_padding ]
+//
+// For efficiency if a key's value is rewritten the new value is just appended
+// to the same sector, a clean of the sector is only needed if there is no more
+// room. Cleaning the sector involves moving the most recent value of each key
+// to another sector and erasing the sector. Erasing data is treated the same
+// as rewriting data, but an erased chunk has the erased flag set, and no data.
+//
+// KVS maintains a data structure in RAM for quick indexing of each key's most
+// recent value, but no caching of data is ever performed. If a write/erase
+// function returns successfully, it is guaranteed that the data has been
+// pushed to flash. The KVS should also be resistant to sudden unplanned power
+// outages and be capable of recovering even mid clean (this is accomplished
+// using a flag which marks the sector before the clean is started).
+
+#include "pw_kvs/key_value_store.h"
+
+#include <cstring>
+
+#include "pw_kvs/os/mutex.h"
+#include "pw_kvs/util/ccitt_crc16.h"
+#include "pw_kvs/util/constexpr.h"
+#include "pw_kvs/util/flash.h"
+
+namespace pw {
+
+// Declare static constexpr variables so it can be used for pass-by-reference
+// functions.
+constexpr uint16_t KeyValueStore::kSectorReadyValue;
+
+Status KeyValueStore::Enable() {
+ os::MutexLock lock(&lock_);
+ if (enabled_) {
+ return Status::OK;
+ }
+
+ // Reset parameters.
+ memset(sector_space_remaining_, 0, sizeof(sector_space_remaining_));
+ map_size_ = 0;
+
+ // For now alignment is set to use partitions alignment.
+ alignment_bytes_ = partition_.GetAlignmentBytes();
+ DCHECK(alignment_bytes_ <= kMaxAlignmentBytes);
+
+ LOG_WARN_IF(partition_.GetSectorCount() > kSectorCountMax,
+ "Partition is larger then KVS max sector count, not all space "
+ "will be used.");
+ // Load map and setup sectors if needed (first word isn't kSectorReadyValue).
+ next_sector_clean_order_ = 0;
+ for (SectorIndex i = 0; i < SectorCount(); i++) {
+ KvsSectorHeaderMeta sector_header_meta;
+ // Because underlying flash can be encrypted + erased, trying to readback
+ // may give random values. It's important to make sure that data is not
+ // erased before trying to see if there is a token match.
+ bool is_sector_meta_erased;
+ RETURN_IF_ERROR(partition_.IsChunkErased(
+ SectorIndexToAddress(i),
+ RoundUpForAlignment(sizeof(sector_header_meta)),
+ &is_sector_meta_erased));
+ RETURN_IF_ERROR(
+ UnalignedRead(&partition_,
+ reinterpret_cast<uint8_t*>(§or_header_meta),
+ SectorIndexToAddress(i),
+ sizeof(sector_header_meta)));
+
+ constexpr int kVersion3 = 3; // Version 3 only cleans 1 sector at a time.
+ constexpr int kVersion2 = 2; // Version 2 is always 1 byte aligned.
+ if (is_sector_meta_erased ||
+ sector_header_meta.synchronize_token != kSectorReadyValue) {
+ // Sector needs to be setup
+ RETURN_IF_ERROR(ResetSector(i));
+ continue;
+ } else if (sector_header_meta.version != kVersion &&
+ sector_header_meta.version != kVersion3 && // Allow version 3
+ sector_header_meta.version != kVersion2) { // Allow version 2
+ LOG(ERROR) << "Unsupported KVS version in sector: " << i;
+ return Status::FAILED_PRECONDITION;
+ }
+ uint32_t sector_header_cleaning_offset =
+ RoundUpForAlignment(sizeof(KvsSectorHeaderMeta));
+
+ bool clean_not_pending;
+ RETURN_IF_ERROR(partition_.IsChunkErased(
+ SectorIndexToAddress(i) + sector_header_cleaning_offset,
+ RoundUpForAlignment(sizeof(KvsSectorHeaderCleaning)),
+ &clean_not_pending));
+
+ if (!clean_not_pending) {
+ // Sector is marked for cleaning, read the sector_clean_order
+ RETURN_IF_ERROR(
+ UnalignedRead(&partition_,
+ reinterpret_cast<uint8_t*>(§or_clean_order_[i]),
+ SectorIndexToAddress(i) + sector_header_cleaning_offset,
+ sizeof(KvsSectorHeaderCleaning::sector_clean_order)));
+ next_sector_clean_order_ =
+ std::max(sector_clean_order_[i] + 1, next_sector_clean_order_);
+ } else {
+ sector_clean_order_[i] = kSectorCleanNotPending;
+ }
+
+ // Handle alignment
+ if (sector_header_meta.version == kVersion2) {
+ sector_header_meta.alignment_bytes = 1;
+ }
+ if (sector_header_meta.alignment_bytes != alignment_bytes_) {
+ // NOTE: For now all sectors must have same alignment.
+ LOG(ERROR) << "Sector " << i << " has unexpected alignment "
+ << alignment_bytes_
+ << " != " << sector_header_meta.alignment_bytes;
+ return Status::FAILED_PRECONDITION;
+ }
+
+ // Scan through sector and add key/value pairs to map.
+ FlashPartition::Address offset =
+ RoundUpForAlignment(sizeof(KvsSectorHeaderMeta)) +
+ RoundUpForAlignment(sizeof(KvsSectorHeaderCleaning));
+ sector_space_remaining_[i] = partition_.GetSectorSizeBytes() - offset;
+ while (offset < partition_.GetSectorSizeBytes() -
+ RoundUpForAlignment(sizeof(KvsHeader))) {
+ FlashPartition::Address address = SectorIndexToAddress(i) + offset;
+
+ // Read header
+ KvsHeader header;
+ bool is_kvs_header_erased;
+ // Because underlying flash can be encrypted + erased, trying to readback
+ // may give random values. It's important to make sure that data is not
+ // erased before trying to see if there is a token match.
+ RETURN_IF_ERROR(partition_.IsChunkErased(
+ address, RoundUpForAlignment(sizeof(header)), &is_kvs_header_erased));
+ RETURN_IF_ERROR(UnalignedRead(&partition_,
+ reinterpret_cast<uint8_t*>(&header),
+ address,
+ sizeof(header)));
+ if (is_kvs_header_erased || header.synchronize_token != kChunkSyncValue) {
+ if (!is_kvs_header_erased) {
+ LOG_ERROR("Next sync_token is not clear!");
+ // TODO: handle this?
+ }
+ break; // End of elements in sector
+ }
+
+ CHECK(header.key_len <= kChunkKeyLengthMax);
+ static_assert(sizeof(temp_key_buffer_) >= (kChunkKeyLengthMax + 1u),
+ "Key buffer must be at least big enough for a key and a "
+ "nul-terminator.");
+
+ // Read key and add to map
+ RETURN_IF_ERROR(
+ UnalignedRead(&partition_,
+ reinterpret_cast<uint8_t*>(&temp_key_buffer_),
+ address + RoundUpForAlignment(sizeof(header)),
+ header.key_len));
+ temp_key_buffer_[header.key_len] = '\0';
+ bool is_erased = header.flags & kFlagsIsErasedMask;
+
+ KeyIndex index = FindKeyInMap(temp_key_buffer_);
+ if (index == kListCapacityMax) {
+ RETURN_IF_ERROR(AppendToMap(
+ temp_key_buffer_, address, header.chunk_len, is_erased));
+ } else if (sector_clean_order_[i] >=
+ sector_clean_order_[AddressToSectorIndex(
+ key_map_[index].address)]) {
+ // The value being added is also in another sector (which is marked for
+ // cleaning), but the current sector's order is larger and thefore this
+ // is a newer version then what is already in the map.
+ UpdateMap(index, address, header.chunk_len, is_erased);
+ }
+
+ // Increment search
+ offset += ChunkSize(header.key_len, header.chunk_len);
+ }
+ sector_space_remaining_[i] =
+ clean_not_pending ? partition_.GetSectorSizeBytes() - offset : 0;
+ }
+
+ LOG_IF_ERROR(EnforceFreeSector())
+ << "Failed to force clean at boot, no free sectors available!";
+ enabled_ = true;
+ return Status::OK;
+}
+
+Status KeyValueStore::Get(const char* key,
+ void* raw_value,
+ uint16_t size,
+ uint16_t offset) {
+ uint8_t* const value = reinterpret_cast<uint8_t*>(raw_value);
+
+ if (key == nullptr || value == nullptr) {
+ return Status::INVALID_ARGUMENT;
+ }
+
+ size_t key_len = util::StringLength(key, kChunkKeyLengthMax + 1u);
+ if (key_len == 0 || key_len > kChunkKeyLengthMax) {
+ return Status::INVALID_ARGUMENT;
+ }
+
+ // TODO: Support unaligned offset reads.
+ if (offset % alignment_bytes_ != 0) {
+ LOG_ERROR("Currently unaligned offsets are not supported");
+ return Status::INVALID_ARGUMENT;
+ }
+ os::MutexLock lock(&lock_);
+ if (!enabled_) {
+ return Status::FAILED_PRECONDITION;
+ }
+
+ KeyIndex key_index = FindKeyInMap(key);
+ if (key_index == kListCapacityMax || key_map_[key_index].is_erased) {
+ return Status::NOT_FOUND;
+ }
+ KvsHeader header;
+ // TODO: Could cache the CRC and avoid reading the header.
+ RETURN_IF_ERROR(UnalignedRead(&partition_,
+ reinterpret_cast<uint8_t*>(&header),
+ key_map_[key_index].address,
+ sizeof(header)));
+ if (kChunkSyncValue != header.synchronize_token) {
+ return Status::DATA_LOSS;
+ }
+ if (size + offset > header.chunk_len) {
+ LOG_ERROR("Out of bounds read: offset(%u) + size(%u) > data_size(%u)",
+ offset,
+ size,
+ header.chunk_len);
+ return Status::INVALID_ARGUMENT;
+ }
+ RETURN_IF_ERROR(UnalignedRead(
+ &partition_,
+ value,
+ key_map_[key_index].address + RoundUpForAlignment(sizeof(KvsHeader)) +
+ RoundUpForAlignment(header.key_len) + offset,
+ size));
+
+ // Verify CRC only when full packet was read.
+ if (offset == 0 && size == header.chunk_len) {
+ uint16_t crc = CalculateCrc(key, key_len, value, size);
+ if (crc != header.crc) {
+ LOG_ERROR("KVS CRC does not match for key=%s [expected %u, found %u]",
+ key,
+ header.crc,
+ crc);
+ return Status::DATA_LOSS;
+ }
+ }
+ return Status::OK;
+}
+
+uint16_t KeyValueStore::CalculateCrc(const char* key,
+ uint16_t key_size,
+ const uint8_t* value,
+ uint16_t value_size) const {
+ CcittCrc16 crc;
+ crc.AppendBytes(ConstBuffer(reinterpret_cast<const uint8_t*>(key), key_size));
+ return crc.AppendBytes(ConstBuffer(value, value_size));
+}
+
+Status KeyValueStore::CalculateCrcFromValueAddress(
+ const char* key,
+ uint16_t key_size,
+ FlashPartition::Address value_address,
+ uint16_t value_size,
+ uint16_t* crc_ret) {
+ CcittCrc16 crc;
+ crc.AppendBytes(ConstBuffer(reinterpret_cast<const uint8_t*>(key), key_size));
+ for (size_t i = 0; i < value_size; i += TempBufferAlignedSize()) {
+ auto read_size = std::min(value_size - i, TempBufferAlignedSize());
+ RETURN_IF_ERROR(
+ UnalignedRead(&partition_, temp_buffer_, value_address + i, read_size));
+ crc.AppendBytes(ConstBuffer(temp_buffer_, read_size));
+ }
+ *crc_ret = crc.CurrentValue();
+ return Status::OK;
+}
+
+Status KeyValueStore::Put(const char* key,
+ const void* raw_value,
+ uint16_t size) {
+ const uint8_t* const value = reinterpret_cast<const uint8_t*>(raw_value);
+ if (key == nullptr || value == nullptr) {
+ return Status::INVALID_ARGUMENT;
+ }
+
+ size_t key_len = util::StringLength(key, (kChunkKeyLengthMax + 1u));
+ if (key_len == 0 || key_len > kChunkKeyLengthMax ||
+ size > kChunkValueLengthMax) {
+ return Status::INVALID_ARGUMENT;
+ }
+
+ os::MutexLock lock(&lock_);
+ if (!enabled_) {
+ return Status::FAILED_PRECONDITION;
+ }
+
+ KeyIndex index = FindKeyInMap(key);
+ if (index != kListCapacityMax) { // Key already in map, rewrite value
+ return RewriteValue(index, value, size);
+ }
+
+ FlashPartition::Address address = FindSpace(ChunkSize(key_len, size));
+ if (address == kSectorInvalid) {
+ return Status::RESOURCE_EXHAUSTED;
+ }
+
+ // Check if this would use the last empty sector on KVS with multiple sectors
+ if (SectorCount() > 1 && IsInLastFreeSector(address)) {
+ // Forcing a full garbage collect to free more sectors.
+ RETURN_IF_ERROR(FullGarbageCollect());
+ address = FindSpace(ChunkSize(key_len, size));
+ if (address == kSectorInvalid || IsInLastFreeSector(address)) {
+ // Couldn't find space, KVS is full.
+ return Status::RESOURCE_EXHAUSTED;
+ }
+ }
+
+ RETURN_IF_ERROR(WriteKeyValue(address, key, value, size));
+ RETURN_IF_ERROR(AppendToMap(key, address, size));
+
+ return Status::OK;
+}
+
+// Garbage collection cleans sectors to try to free up space.
+// If clean_pending_sectors is true, it will only clean sectors which are
+// currently pending a clean.
+// If clean_pending_sectors is false, it will only clean sectors which are not
+// currently pending a clean, instead it will mark them for cleaning and attempt
+// a clean.
+// If exit_when_have_free_sector is true, it will exit once a single free sector
+// exists.
+Status KeyValueStore::GarbageCollectImpl(bool clean_pending_sectors,
+ bool exit_when_have_free_sector) {
+ // Try to clean any pending sectors
+ for (SectorIndex sector = 0; sector < SectorCount(); sector++) {
+ if (clean_pending_sectors ==
+ (sector_clean_order_[sector] != kSectorCleanNotPending)) {
+ if (!clean_pending_sectors) {
+ RETURN_IF_ERROR(MarkSectorForClean(sector));
+ }
+ Status status = CleanSector(sector);
+ if (!status.ok() && status != Status::RESOURCE_EXHAUSTED) {
+ return status;
+ }
+ if (exit_when_have_free_sector && HaveEmptySectorImpl()) {
+ return Status::OK; // Now have a free sector
+ }
+ }
+ }
+ return Status::OK;
+}
+
+Status KeyValueStore::EnforceFreeSector() {
+ if (SectorCount() == 1 || HasEmptySector()) {
+ return Status::OK;
+ }
+ LOG_INFO("KVS garbage collecting to get a free sector");
+ RETURN_IF_ERROR(GarbageCollectImpl(true /*clean_pending_sectors*/,
+ true /*exit_when_have_free_sector*/));
+ if (HasEmptySector()) {
+ return Status::OK;
+ }
+ LOG_INFO("KVS: trying to clean non-pending sectors for more space");
+ RETURN_IF_ERROR(GarbageCollectImpl(false /*clean_pending_sectors*/,
+ true /*exit_when_have_free_sector*/));
+ return HaveEmptySectorImpl() ? Status::OK : Status::RESOURCE_EXHAUSTED;
+}
+
+Status KeyValueStore::FullGarbageCollect() {
+ LOG_INFO("KVS: Full garbage collecting to try to free space");
+ RETURN_IF_ERROR(GarbageCollectImpl(true /*clean_pending_sectors*/,
+ false /*exit_when_have_free_sector*/));
+ return GarbageCollectImpl(false /*clean_pending_sectors*/,
+ false /*exit_when_have_free_sector*/);
+}
+
+Status KeyValueStore::RewriteValue(KeyIndex key_index,
+ const uint8_t* value,
+ uint16_t size,
+ bool is_erased) {
+ // Compare values, return if values are the same.
+ if (ValueMatches(key_index, value, size, is_erased)) {
+ return Status::OK;
+ }
+
+ size_t key_length =
+ util::StringLength(key_map_[key_index].key, (kChunkKeyLengthMax + 1u));
+ RETURN_STATUS_IF(key_length > kChunkKeyLengthMax, Status::INTERNAL);
+
+ uint32_t space_required = ChunkSize(key_length, size);
+ SectorIndex sector = AddressToSectorIndex(key_map_[key_index].address);
+ uint32_t sector_space_remaining = SectorSpaceRemaining(sector);
+
+ FlashPartition::Address address = kSectorInvalid;
+ if (sector_space_remaining >= space_required) {
+ // Space available within sector, append to end
+ address = SectorIndexToAddress(sector) + partition_.GetSectorSizeBytes() -
+ sector_space_remaining;
+ } else {
+ // No space in current sector, mark sector for clean and use another sector.
+ RETURN_IF_ERROR(MarkSectorForClean(sector));
+ address = FindSpace(ChunkSize(key_length, size));
+ }
+ if (address == kSectorInvalid) {
+ return Status::RESOURCE_EXHAUSTED;
+ }
+ RETURN_IF_ERROR(
+ WriteKeyValue(address, key_map_[key_index].key, value, size, is_erased));
+ UpdateMap(key_index, address, size, is_erased);
+
+ return EnforceFreeSector();
+}
+
+bool KeyValueStore::ValueMatches(KeyIndex index,
+ const uint8_t* value,
+ uint16_t size,
+ bool is_erased) {
+ // Compare sizes of CRC.
+ if (size != key_map_[index].chunk_len) {
+ return false;
+ }
+ KvsHeader header;
+ UnalignedRead(&partition_,
+ reinterpret_cast<uint8_t*>(&header),
+ key_map_[index].address,
+ sizeof(header));
+ uint8_t key_len =
+ util::StringLength(key_map_[index].key, (kChunkKeyLengthMax + 1u));
+ if (key_len > kChunkKeyLengthMax) {
+ return false;
+ }
+
+ if ((header.flags & kFlagsIsErasedMask) != is_erased) {
+ return false;
+ } else if ((header.flags & kFlagsIsErasedMask) && is_erased) {
+ return true;
+ }
+
+ // Compare checksums.
+ if (header.crc != CalculateCrc(key_map_[index].key, key_len, value, size)) {
+ return false;
+ }
+ FlashPartition::Address address = key_map_[index].address +
+ RoundUpForAlignment(sizeof(KvsHeader)) +
+ RoundUpForAlignment(key_len);
+ // Compare full values.
+ for (size_t i = 0; i < key_map_[index].chunk_len;
+ i += TempBufferAlignedSize()) {
+ auto read_size =
+ std::min(key_map_[index].chunk_len - i, TempBufferAlignedSize());
+ auto status =
+ UnalignedRead(&partition_, temp_buffer_, address + i, read_size);
+ if (!status.ok()) {
+ LOG(ERROR) << "Failed to read chunk: " << status;
+ return false;
+ }
+ if (memcmp(value + i, temp_buffer_, read_size) != 0) {
+ return false;
+ }
+ }
+ return true;
+}
+
+Status KeyValueStore::Erase(const char* key) {
+ if (key == nullptr) {
+ return Status::INVALID_ARGUMENT;
+ }
+
+ size_t key_len = util::StringLength(key, (kChunkKeyLengthMax + 1u));
+ if (key_len == 0 || key_len > kChunkKeyLengthMax) {
+ return Status::INVALID_ARGUMENT;
+ }
+ os::MutexLock lock(&lock_);
+ if (!enabled_) {
+ return Status::FAILED_PRECONDITION;
+ }
+
+ KeyIndex key_index = FindKeyInMap(key);
+ if (key_index == kListCapacityMax || key_map_[key_index].is_erased) {
+ return Status::NOT_FOUND;
+ }
+ return RewriteValue(key_index, nullptr, 0, true);
+}
+
+Status KeyValueStore::ResetSector(SectorIndex sector_index) {
+ KvsSectorHeaderMeta sector_header = {.synchronize_token = kSectorReadyValue,
+ .version = kVersion,
+ .alignment_bytes = alignment_bytes_,
+ .padding = 0xFFFF};
+ bool sector_erased = false;
+ partition_.IsChunkErased(SectorIndexToAddress(sector_index),
+ partition_.GetSectorSizeBytes(),
+ §or_erased);
+ auto status = partition_.Erase(SectorIndexToAddress(sector_index), 1);
+
+ // If erasure failed, check first to see if it's merely unimplemented
+ // (as in sub-sector KVSs).
+ if (!status.ok() && !(status == Status::UNIMPLEMENTED && sector_erased)) {
+ return status;
+ }
+
+ RETURN_IF_ERROR(PaddedWrite(&partition_,
+ SectorIndexToAddress(sector_index),
+ reinterpret_cast<const uint8_t*>(§or_header),
+ sizeof(sector_header)));
+
+ // Update space remaining
+ sector_clean_order_[sector_index] = kSectorCleanNotPending;
+ sector_space_remaining_[sector_index] = SectorSpaceAvailableWhenEmpty();
+ return Status::OK;
+}
+
+Status KeyValueStore::WriteKeyValue(FlashPartition::Address address,
+ const char* key,
+ const uint8_t* value,
+ uint16_t size,
+ bool is_erased) {
+ uint16_t key_length = util::StringLength(key, (kChunkKeyLengthMax + 1u));
+ RETURN_STATUS_IF(key_length > kChunkKeyLengthMax, Status::INTERNAL);
+
+ constexpr uint16_t kFlagDefaultValue = 0;
+ KvsHeader header = {
+ .synchronize_token = kChunkSyncValue,
+ .crc = CalculateCrc(key, key_length, value, size),
+ .flags = is_erased ? kFlagsIsErasedMask : kFlagDefaultValue,
+ .key_len = key_length,
+ .chunk_len = size};
+
+ SectorIndex sector = AddressToSectorIndex(address);
+ RETURN_IF_ERROR(PaddedWrite(&partition_,
+ address,
+ reinterpret_cast<uint8_t*>(&header),
+ sizeof(header)));
+ address += RoundUpForAlignment(sizeof(header));
+ RETURN_IF_ERROR(PaddedWrite(
+ &partition_, address, reinterpret_cast<const uint8_t*>(key), key_length));
+ address += RoundUpForAlignment(key_length);
+ if (size > 0) {
+ RETURN_IF_ERROR(PaddedWrite(&partition_, address, value, size));
+ }
+ sector_space_remaining_[sector] -= ChunkSize(key_length, size);
+ return Status::OK;
+}
+
+Status KeyValueStore::MoveChunk(FlashPartition::Address dest_address,
+ FlashPartition::Address src_address,
+ uint16_t size) {
+ DCHECK_EQ(src_address % partition_.GetAlignmentBytes(), 0);
+ DCHECK_EQ(dest_address % partition_.GetAlignmentBytes(), 0);
+ DCHECK_EQ(size % partition_.GetAlignmentBytes(), 0);
+
+ // Copy data over in chunks to reduce the size of the temporary buffer
+ for (size_t i = 0; i < size; i += TempBufferAlignedSize()) {
+ size_t move_size = std::min(size - i, TempBufferAlignedSize());
+ DCHECK_EQ(move_size % alignment_bytes_, 0);
+ RETURN_IF_ERROR(partition_.Read(temp_buffer_, src_address + i, move_size));
+ RETURN_IF_ERROR(
+ partition_.Write(dest_address + i, temp_buffer_, move_size));
+ }
+ return Status::OK;
+}
+
+Status KeyValueStore::MarkSectorForClean(SectorIndex sector) {
+ if (sector_clean_order_[sector] != kSectorCleanNotPending) {
+ return Status::OK; // Sector is already marked for clean
+ }
+
+ // Flag the sector as clean being active. This ensures we can handle losing
+ // power while a clean is active.
+ const KvsSectorHeaderCleaning kValue = {next_sector_clean_order_};
+ RETURN_IF_ERROR(
+ PaddedWrite(&partition_,
+ SectorIndexToAddress(sector) +
+ RoundUpForAlignment(sizeof(KvsSectorHeaderMeta)),
+ reinterpret_cast<const uint8_t*>(&kValue),
+ sizeof(kValue)));
+ sector_space_remaining_[sector] = 0;
+ sector_clean_order_[sector] = next_sector_clean_order_;
+ next_sector_clean_order_++;
+ return Status::OK;
+}
+
+Status KeyValueStore::CleanSector(SectorIndex sector) {
+ // Iterate through the map, for each valid element which is in this sector:
+ // - Find space in another sector which can fit this chunk.
+ // - Add to the other sector and update the map.
+ for (KeyValueStore::KeyIndex i = 0; i < map_size_; i++) {
+ // If this key is an erased chunk don't need to move it.
+ while (i < map_size_ &&
+ sector == AddressToSectorIndex(key_map_[i].address) &&
+ key_map_[i].is_erased) { // Remove this key from the map.
+ RemoveFromMap(i);
+ // NOTE: i is now a new key, and map_size_ has been decremented.
+ }
+
+ if (i < map_size_ && sector == AddressToSectorIndex(key_map_[i].address)) {
+ uint8_t key_len =
+ util::StringLength(key_map_[i].key, (kChunkKeyLengthMax + 1u));
+ FlashPartition::Address address = key_map_[i].address;
+ auto size = ChunkSize(key_len, key_map_[i].chunk_len);
+ FlashPartition::Address move_address = FindSpace(size);
+ if (move_address == kSectorInvalid) {
+ return Status::RESOURCE_EXHAUSTED;
+ }
+ RETURN_IF_ERROR(MoveChunk(move_address, address, size));
+ sector_space_remaining_[AddressToSectorIndex(move_address)] -= size;
+ key_map_[i].address = move_address; // Update map
+ }
+ }
+ ResetSector(sector);
+ return Status::OK;
+}
+
+Status KeyValueStore::CleanOneSector(bool* all_sectors_have_been_cleaned) {
+ if (all_sectors_have_been_cleaned == nullptr) {
+ return Status::INVALID_ARGUMENT;
+ }
+ os::MutexLock lock(&lock_);
+ bool have_cleaned_sector = false;
+ for (SectorIndex sector = 0; sector < SectorCount(); sector++) {
+ if (sector_clean_order_[sector] != kSectorCleanNotPending) {
+ if (have_cleaned_sector) { // only clean 1 sector
+ *all_sectors_have_been_cleaned = false;
+ return Status::OK;
+ }
+ RETURN_IF_ERROR(CleanSector(sector));
+ have_cleaned_sector = true;
+ }
+ }
+ *all_sectors_have_been_cleaned = true;
+ return Status::OK;
+}
+
+Status KeyValueStore::CleanAllInternal() {
+ for (SectorIndex sector = 0; sector < SectorCount(); sector++) {
+ if (sector_clean_order_[sector] != kSectorCleanNotPending) {
+ RETURN_IF_ERROR(CleanSector(sector));
+ }
+ }
+ return Status::OK;
+}
+
+FlashPartition::Address KeyValueStore::FindSpace(
+ uint16_t requested_size) const {
+ if (requested_size > SectorSpaceAvailableWhenEmpty()) {
+ return kSectorInvalid; // This would never fit
+ }
+ // Iterate through sectors, find first available sector with enough space.
+ SectorIndex first_empty_sector = kSectorInvalid;
+ for (SectorIndex i = 0; i < SectorCount(); i++) {
+ uint32_t space_remaining = SectorSpaceRemaining(i);
+ if (space_remaining == SectorSpaceAvailableWhenEmpty() &&
+ first_empty_sector == kSectorInvalid) {
+ // Skip the first empty sector to encourage keeping one sector free.
+ first_empty_sector = i;
+ continue;
+ }
+ if (space_remaining >= requested_size) {
+ return SectorIndexToAddress(i) + partition_.GetSectorSizeBytes() -
+ space_remaining;
+ }
+ }
+ // Use first empty sector if that is all that is available.
+ if (first_empty_sector != kSectorInvalid) {
+ return SectorIndexToAddress(first_empty_sector) +
+ partition_.GetSectorSizeBytes() - SectorSpaceAvailableWhenEmpty();
+ }
+ return kSectorInvalid;
+}
+
+uint32_t KeyValueStore::SectorSpaceRemaining(SectorIndex sector_index) const {
+ return sector_space_remaining_[sector_index];
+}
+
+Status KeyValueStore::GetValueSize(const char* key, uint16_t* value_size) {
+ if (key == nullptr || value_size == nullptr) {
+ return Status::INVALID_ARGUMENT;
+ }
+
+ size_t key_len = util::StringLength(key, (kChunkKeyLengthMax + 1u));
+ if (key_len == 0 || key_len > kChunkKeyLengthMax) {
+ return Status::INVALID_ARGUMENT;
+ }
+ os::MutexLock lock(&lock_);
+ if (!enabled_) {
+ return Status::FAILED_PRECONDITION;
+ }
+
+ uint8_t idx = FindKeyInMap(key);
+ if (idx == kListCapacityMax || key_map_[idx].is_erased) {
+ return Status::NOT_FOUND;
+ }
+ *value_size = key_map_[idx].chunk_len;
+ return Status::OK;
+}
+
+Status KeyValueStore::AppendToMap(const char* key,
+ FlashPartition::Address address,
+ uint16_t chunk_len,
+ bool is_erased) {
+ if (map_size_ >= kListCapacityMax) {
+ LOG_ERROR("Can't add: reached max supported keys %d", kListCapacityMax);
+ return Status::INTERNAL;
+ }
+
+ // Copy incoming key into map entry, ensuring size checks and nul-termination.
+ StringBuilder key_builder(key_map_[map_size_].key,
+ sizeof(key_map_[map_size_].key));
+ key_builder.append(key);
+
+ if (!key_builder.status().ok()) {
+ LOG_ERROR("Can't add: got invalid key: %s!", key_builder.status().str());
+ return Status::INTERNAL;
+ }
+
+ key_map_[map_size_].address = address;
+ key_map_[map_size_].chunk_len = chunk_len;
+ key_map_[map_size_].is_erased = is_erased;
+ map_size_++;
+
+ return Status::OK;
+}
+
+KeyValueStore::KeyIndex KeyValueStore::FindKeyInMap(const char* key) const {
+ for (KeyIndex i = 0; i < map_size_; i++) {
+ if (strncmp(key, key_map_[i].key, sizeof(key_map_[i].key)) == 0) {
+ return i;
+ }
+ }
+ return kListCapacityMax;
+}
+
+void KeyValueStore::UpdateMap(KeyIndex index,
+ FlashPartition::Address address,
+ uint16_t chunk_len,
+ bool is_erased) {
+ key_map_[index].address = address;
+ key_map_[index].chunk_len = chunk_len;
+ key_map_[index].is_erased = is_erased;
+}
+
+void KeyValueStore::RemoveFromMap(KeyIndex key_index) {
+ key_map_[key_index] = key_map_[--map_size_];
+}
+
+uint8_t KeyValueStore::KeyCount() const {
+ uint8_t count = 0;
+ for (unsigned i = 0; i < map_size_; i++) {
+ count += key_map_[i].is_erased ? 0 : 1;
+ }
+ return count;
+}
+
+const char* KeyValueStore::GetKey(uint8_t idx) const {
+ uint8_t count = 0;
+ for (unsigned i = 0; i < map_size_; i++) {
+ if (!key_map_[i].is_erased) {
+ if (idx == count) {
+ return key_map_[i].key;
+ }
+ count++;
+ }
+ }
+ return nullptr;
+}
+
+uint16_t KeyValueStore::GetValueSize(uint8_t idx) const {
+ uint8_t count = 0;
+ for (unsigned i = 0; i < map_size_; i++) {
+ if (!key_map_[i].is_erased) {
+ if (idx == count++) {
+ return key_map_[i].chunk_len;
+ }
+ }
+ }
+ return 0;
+}
+
+} // namespace pw
diff --git a/pw_kvs/key_value_store_sub_sector_test.cc b/pw_kvs/key_value_store_sub_sector_test.cc
new file mode 100644
index 0000000..ff908f5
--- /dev/null
+++ b/pw_kvs/key_value_store_sub_sector_test.cc
@@ -0,0 +1,130 @@
+// 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.
+
+#include "pw_kvs/key_value_store.h"
+
+#include "pw_kvs/assert.h"
+#include "pw_kvs/devices/flash_memory.h"
+#include "pw_kvs/platform/board.h"
+#include "pw_kvs/status.h"
+#include "pw_kvs/test/fixture.h"
+#include "pw_kvs/test/framework.h"
+#include "pw_kvs/test/status_macros.h"
+
+#if USE_MEMORY_BUFFER
+#include "pw_kvs/test/fakes/in_memory_fake_flash.h"
+#endif // USE_MEMORY_BUFFER
+
+namespace pw {
+namespace {
+
+#if USE_MEMORY_BUFFER
+InMemoryFakeFlash<1024, 4> test_flash(8); // 4 x 1k sectors, 8 byte alignment
+FlashPartition test_partition(&test_flash, 0, test_flash.GetSectorCount());
+// Test KVS against FlashSubSector
+FlashMemorySubSector test_subsector_flash(&test_flash,
+ 0,
+ 128); // Expose less than a sector.
+#else // Device test
+FlashPartition& test_partition = Board::Instance().FlashExternalTestPartition();
+FlashMemorySubSector& test_subsector_flash =
+ Board::Instance().FlashMemorySubSectorTestChunk();
+#endif // USE_MEMORY_BUFFER
+
+FlashPartition test_subsector_partition(&test_subsector_flash, 0, 1);
+KeyValueStore subsector_kvs(&test_subsector_partition);
+
+std::array<const char*, 3> keys{"TestKey1", "Key2", "TestKey3"};
+
+} // namespace
+
+TEST(KeyValueStoreTest, WorksWithFlashSubSector) {
+ // The subsector region is assumed to be a part of the test partition.
+ // In order to clear state before the test, we must erase the entire test
+ // partition because erase operations are disallowed on FlashMemorySubSectors.
+ ASSERT_OK(test_partition.Erase(0, test_partition.GetSectorCount()));
+
+ // Reset KVS
+ subsector_kvs.Disable();
+ ASSERT_OK(subsector_kvs.Enable());
+
+ // Add some data
+ uint8_t value1 = 0xDA;
+ ASSERT_OK(subsector_kvs.Put(keys[0], &value1, sizeof(value1)));
+
+ uint32_t value2 = 0xBAD0301f;
+ ASSERT_OK(subsector_kvs.Put(
+ keys[1], reinterpret_cast<uint8_t*>(&value2), sizeof(value2)));
+
+ // Verify data
+ uint32_t test2;
+ EXPECT_OK(subsector_kvs.Get(
+ keys[1], reinterpret_cast<uint8_t*>(&test2), sizeof(test2)));
+ uint8_t test1;
+ ASSERT_OK(subsector_kvs.Get(
+ keys[0], reinterpret_cast<uint8_t*>(&test1), sizeof(test1)));
+
+ EXPECT_EQ(test1, value1);
+
+ // Erase a key
+ ASSERT_OK(subsector_kvs.Erase(keys[0]));
+
+ // Verify it was erased
+ EXPECT_EQ(subsector_kvs
+ .Get(keys[0], reinterpret_cast<uint8_t*>(&test1), sizeof(test1))
+ .code(),
+ Status::NOT_FOUND);
+ test2 = 0;
+ ASSERT_OK(subsector_kvs.Get(
+ keys[1], reinterpret_cast<uint8_t*>(&test2), sizeof(test2)));
+ EXPECT_EQ(test2, value2);
+
+ // Erase other key
+ subsector_kvs.Erase(keys[1]);
+
+ // Verify it was erased
+ EXPECT_EQ(subsector_kvs.KeyCount(), 0);
+}
+
+TEST(KeyValueStoreTest, WorksWithFlashSubSector_MemoryExhausted) {
+ // The subsector region is assumed to be a part of the test partition.
+ // In order to clear state before the test, we must erase the entire test
+ // partition because erase operations are disallowed on FlashMemorySubSectors.
+ ASSERT_OK(test_partition.Erase(0, test_partition.GetSectorCount()));
+
+ // Reset KVS
+ subsector_kvs.Disable();
+ ASSERT_OK(subsector_kvs.Enable());
+
+ // Store as much data as possible in the KVS until it fills up.
+ uint64_t test = 0;
+ for (; test < test_subsector_flash.GetSizeBytes(); test++) {
+ Status status = subsector_kvs.Put(keys[0], test);
+ if (status.ok()) {
+ continue;
+ }
+ // Expected code for erasure failure.
+ ASSERT_EQ(Status::RESOURCE_EXHAUSTED, status);
+ break;
+ }
+ EXPECT_GT(test, 0U); // Should've at least succeeded with one Put.
+
+ // Even though we failed to fill the KVS, it still works, and we
+ // should have the previous test value as the most recent value in the KVS.
+ uint64_t value = 0;
+ ASSERT_OK(subsector_kvs.Get(keys[0], &value));
+ EXPECT_EQ(test - 1, value);
+}
+
+} // namespace pw
diff --git a/pw_kvs/key_value_store_test.cc b/pw_kvs/key_value_store_test.cc
new file mode 100644
index 0000000..fa5eaf9
--- /dev/null
+++ b/pw_kvs/key_value_store_test.cc
@@ -0,0 +1,1396 @@
+// 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.
+
+#include "pw_kvs/key_value_store.h"
+
+#include "pw_kvs/assert.h"
+#include "pw_kvs/devices/flash_memory.h"
+#include "pw_kvs/os/stack_checks.h"
+#include "pw_kvs/platform/board.h"
+#include "pw_kvs/status.h"
+#include "pw_kvs/test/fixture.h"
+#include "pw_kvs/test/framework.h"
+#include "pw_kvs/test/status_macros.h"
+#include "pw_kvs/util/ccitt_crc16.h"
+
+#if USE_MEMORY_BUFFER
+#include "pw_kvs/test/fakes/in_memory_fake_flash.h"
+#endif // USE_MEMORY_BUFFER
+
+namespace pw {
+namespace {
+
+#if USE_MEMORY_BUFFER
+// Although it might be useful to test other configurations, some tests require
+// at least 3 sectors; therfore it should have this when checked in.
+InMemoryFakeFlash<4 * 1024, 4> test_flash(
+ 16); // 4 x 4k sectors, 16 byte alignment
+FlashPartition test_partition(&test_flash, 0, test_flash.GetSectorCount());
+InMemoryFakeFlash<1024, 60> large_test_flash(8);
+FlashPartition large_test_partition(&large_test_flash,
+ 0,
+ large_test_flash.GetSectorCount());
+#else // Device test
+FlashPartition& test_partition = Board::Instance().FlashExternalTestPartition();
+#endif // USE_MEMORY_BUFFER
+
+KeyValueStore kvs(&test_partition);
+
+// Use test fixture for logging support
+class KeyValueStoreTest : public ::testing::Test {
+ protected:
+ KeyValueStoreTest() : kvs_local_(&test_partition) {}
+
+ KeyValueStore kvs_local_;
+};
+
+std::array<uint8_t, 512> buffer;
+std::array<const char*, 3> keys{"TestKey1", "Key2", "TestKey3"};
+
+Status PaddedWrite(FlashPartition* partition,
+ FlashPartition::Address address,
+ const uint8_t* buf,
+ uint16_t size) {
+ static constexpr uint16_t kMaxAlignmentBytes = 128;
+ uint8_t alignment_buffer[kMaxAlignmentBytes] = {0};
+ uint16_t aligned_bytes = size - (size % partition->GetAlignmentBytes());
+ RETURN_IF_ERROR(partition->Write(address, buf, aligned_bytes));
+ uint16_t remaining_bytes = size - aligned_bytes;
+ if (remaining_bytes > 0) {
+ memcpy(alignment_buffer, &buf[aligned_bytes], remaining_bytes);
+ RETURN_IF_ERROR(partition->Write(address + aligned_bytes,
+ alignment_buffer,
+ partition->GetAlignmentBytes()));
+ }
+ return Status::OK;
+}
+
+size_t RoundUpForAlignment(size_t size) {
+ uint16_t alignment = test_partition.GetAlignmentBytes();
+ if (size % alignment != 0) {
+ return size + alignment - size % alignment;
+ }
+ return size;
+}
+
+// This class gives attributes of KVS that we are testing against
+class KvsAttributes {
+ public:
+ KvsAttributes(size_t key_size, size_t data_size)
+ : sector_header_meta_size_(
+ RoundUpForAlignment(KeyValueStore::kHeaderSize)),
+ sector_header_clean_size_(
+ RoundUpForAlignment(KeyValueStore::kHeaderSize)),
+ chunk_header_size_(RoundUpForAlignment(KeyValueStore::kHeaderSize)),
+ data_size_(RoundUpForAlignment(data_size)),
+ key_size_(RoundUpForAlignment(key_size)),
+ erase_size_(chunk_header_size_ + key_size_),
+ min_put_size_(chunk_header_size_ + key_size_ + data_size_) {}
+
+ size_t SectorHeaderSize() {
+ return sector_header_meta_size_ + sector_header_clean_size_;
+ }
+ size_t SectorHeaderMetaSize() { return sector_header_meta_size_; }
+ size_t ChunkHeaderSize() { return chunk_header_size_; }
+ size_t DataSize() { return data_size_; }
+ size_t KeySize() { return key_size_; }
+ size_t EraseSize() { return erase_size_; }
+ size_t MinPutSize() { return min_put_size_; }
+
+ private:
+ const size_t sector_header_meta_size_;
+ const size_t sector_header_clean_size_;
+ const size_t chunk_header_size_;
+ const size_t data_size_;
+ const size_t key_size_;
+ const size_t erase_size_;
+ const size_t min_put_size_;
+};
+
+// Intention of this is to put and erase key-val to fill up sectors. It's a
+// helper function in testing how KVS handles cases where flash sector is full
+// or near full.
+void FillKvs(const char* key, size_t size_to_fill) {
+ constexpr size_t kTestDataSize = 8;
+ KvsAttributes kvs_attr(std::strlen(key), kTestDataSize);
+ const size_t kMaxPutSize =
+ buffer.size() + kvs_attr.ChunkHeaderSize() + kvs_attr.KeySize();
+
+ CHECK(size_to_fill >= kvs_attr.MinPutSize() + kvs_attr.EraseSize());
+
+ // Saving enough space to perform erase after loop
+ size_to_fill -= kvs_attr.EraseSize();
+ // We start with possible small chunk to prevent too small of a Kvs.Put() at
+ // the end.
+ size_t chunk_len =
+ std::max(kvs_attr.MinPutSize(), size_to_fill % buffer.size());
+ memset(buffer.data(), 0, buffer.size());
+ while (size_to_fill > 0) {
+ buffer[0]++; // Changing buffer value so put actually does something
+ ASSERT_OK(
+ kvs.Put(key,
+ buffer.data(),
+ chunk_len - kvs_attr.ChunkHeaderSize() - kvs_attr.KeySize()));
+ size_to_fill -= chunk_len;
+ chunk_len = std::min(size_to_fill, kMaxPutSize);
+ }
+ ASSERT_OK(kvs.Erase(key));
+}
+
+uint16_t CalcKvsCrc(const char* key, const void* data, size_t data_len) {
+ CcittCrc16 crc;
+ static constexpr size_t kChunkKeyLengthMax = 15;
+ crc.AppendBytes(
+ ConstBuffer(reinterpret_cast<const uint8_t*>(key),
+ pw::util::StringLength(key, kChunkKeyLengthMax)));
+ return crc.AppendBytes(
+ ConstBuffer(reinterpret_cast<const uint8_t*>(data), data_len));
+}
+
+uint16_t CalcTestPartitionCrc() {
+ uint8_t buf[16]; // Read as 16 byte chunks
+ CHECK_EQ(sizeof(buf) % test_partition.GetAlignmentBytes(), 0);
+ CHECK_EQ(test_partition.GetSizeBytes() % sizeof(buf), 0);
+ CcittCrc16 crc;
+ for (size_t i = 0; i < test_partition.GetSizeBytes(); i += sizeof(buf)) {
+ test_partition.Read(buf, i, sizeof(buf));
+ crc.AppendBytes(ConstBuffer(buf, sizeof(buf)));
+ }
+ return crc.CurrentValue();
+}
+} // namespace
+
+TEST_F(KeyValueStoreTest, FuzzTest) {
+ if (test_partition.GetSectorSizeBytes() < 4 * 1024 ||
+ test_partition.GetSectorCount() < 4) {
+ LOG_INFO("Sectors too small, skipping test.");
+ return; // TODO: Test could be generalized
+ }
+ // Erase
+ ASSERT_OK(test_partition.Erase(0, test_partition.GetSectorCount()));
+
+ // Reset KVS
+ kvs.Disable();
+ ASSERT_OK(kvs.Enable());
+ const char* key1 = "Buf1";
+ const char* key2 = "Buf2";
+ const size_t kLargestBufSize = 3 * 1024;
+ static uint8_t buf1[kLargestBufSize];
+ static uint8_t buf2[kLargestBufSize];
+ memset(buf1, 1, sizeof(buf1));
+ memset(buf2, 2, sizeof(buf2));
+
+ // Start with things in KVS
+ ASSERT_OK(kvs.Put(key1, buf1, sizeof(buf1)));
+ ASSERT_OK(kvs.Put(key2, buf2, sizeof(buf2)));
+ for (size_t j = 0; j < keys.size(); j++) {
+ ASSERT_OK(kvs.Put(keys[j], j));
+ }
+
+ for (size_t i = 0; i < 100; i++) {
+ // Vary two sizes
+ size_t size1 = (kLargestBufSize) / (i + 1);
+ size_t size2 = (kLargestBufSize) / (100 - i);
+ for (size_t j = 0; j < 50; j++) {
+ // Rewrite a single key many times, can fill up a sector
+ ASSERT_OK(kvs.Put("some_data", j));
+ }
+ // Delete and re-add everything
+ ASSERT_OK(kvs.Erase(key1));
+ ASSERT_OK(kvs.Put(key1, buf1, size1));
+ ASSERT_OK(kvs.Erase(key2));
+ ASSERT_OK(kvs.Put(key2, buf2, size2));
+ for (size_t j = 0; j < keys.size(); j++) {
+ ASSERT_OK(kvs.Erase(keys[j]));
+ ASSERT_OK(kvs.Put(keys[j], j));
+ }
+
+ // Re-enable and verify
+ kvs.Disable();
+ ASSERT_OK(kvs.Enable());
+ static uint8_t buf[4 * 1024];
+ ASSERT_OK(kvs.Get(key1, buf, size1));
+ ASSERT_EQ(memcmp(buf, buf1, size1), 0);
+ ASSERT_OK(kvs.Get(key2, buf, size2));
+ ASSERT_EQ(memcmp(buf2, buf2, size2), 0);
+ for (size_t j = 0; j < keys.size(); j++) {
+ size_t ret = 1000;
+ ASSERT_OK(kvs.Get(keys[j], &ret));
+ ASSERT_EQ(ret, j);
+ }
+ }
+}
+
+TEST_F(KeyValueStoreTest, Basic) {
+ // Erase
+ ASSERT_OK(test_partition.Erase(0, test_partition.GetSectorCount()));
+
+ // Reset KVS
+ kvs.Disable();
+ ASSERT_OK(kvs.Enable());
+
+ // Add some data
+ uint8_t value1 = 0xDA;
+ ASSERT_OK(kvs.Put(keys[0], &value1, sizeof(value1)));
+
+ uint32_t value2 = 0xBAD0301f;
+ ASSERT_OK(
+ kvs.Put(keys[1], reinterpret_cast<uint8_t*>(&value2), sizeof(value2)));
+
+ // Verify data
+ uint32_t test2;
+ EXPECT_OK(
+ kvs.Get(keys[1], reinterpret_cast<uint8_t*>(&test2), sizeof(test2)));
+ uint8_t test1;
+ ASSERT_OK(
+ kvs.Get(keys[0], reinterpret_cast<uint8_t*>(&test1), sizeof(test1)));
+
+ EXPECT_EQ(test1, value1);
+ EXPECT_EQ(test2, value2);
+
+ // Erase a key
+ EXPECT_OK(kvs.Erase(keys[0]));
+
+ // Verify it was erased
+ EXPECT_EQ(kvs.Get(keys[0], reinterpret_cast<uint8_t*>(&test1), sizeof(test1))
+ .code(),
+ Status::NOT_FOUND);
+ test2 = 0;
+ ASSERT_OK(
+ kvs.Get(keys[1], reinterpret_cast<uint8_t*>(&test2), sizeof(test2)));
+ EXPECT_EQ(test2, value2);
+
+ // Erase other key
+ kvs.Erase(keys[1]);
+
+ // Verify it was erased
+ EXPECT_EQ(kvs.KeyCount(), 0);
+}
+
+TEST_F(KeyValueStoreTest, MaxKeyLength) {
+ // Erase
+ ASSERT_OK(test_partition.Erase(0, test_partition.GetSectorCount()));
+
+ // Reset KVS
+ kvs.Disable();
+ ASSERT_OK(kvs.Enable());
+
+ // Add some data
+ char key[16] = "123456789abcdef"; // key length 15 (without \0)
+ int value = 1;
+ ASSERT_OK(kvs.Put(key, value));
+
+ // Verify data
+ int test = 0;
+ ASSERT_OK(kvs.Get(key, &test));
+ EXPECT_EQ(test, value);
+
+ // Erase a key
+ kvs.Erase(key);
+
+ // Verify it was erased
+ EXPECT_EQ(kvs.Get(key, &test).code(), Status::NOT_FOUND);
+}
+
+TEST_F(KeyValueStoreTest, LargeBuffers) {
+ test_partition.Erase(0, test_partition.GetSectorCount());
+
+ // Note this assumes that no other keys larger then key0
+ static_assert(sizeof(keys[0]) >= sizeof(keys[1]) &&
+ sizeof(keys[0]) >= sizeof(keys[2]));
+ KvsAttributes kvs_attr(std::strlen(keys[0]), buffer.size());
+
+ // Verify the data will fit in this test partition. This checks that all the
+ // keys chunks will fit and a header for each sector will fit. It requires 1
+ // empty sector also.
+ const size_t kAllChunkSize = kvs_attr.MinPutSize() * keys.size();
+ const size_t kAllSectorHeaderSizes =
+ kvs_attr.SectorHeaderSize() * (test_partition.GetSectorCount() - 1);
+ const size_t kMinSize = kAllChunkSize + kAllSectorHeaderSizes;
+ const size_t kAvailSectorSpace = test_partition.GetSectorSizeBytes() *
+ (test_partition.GetSectorCount() - 1);
+ if (kAvailSectorSpace < kMinSize) {
+ LOG_INFO("KVS too small, skipping test.");
+ return;
+ }
+ // Reset KVS
+ kvs.Disable();
+ ASSERT_OK(kvs.Enable());
+
+ // Add and verify
+ for (unsigned add_idx = 0; add_idx < keys.size(); add_idx++) {
+ memset(buffer.data(), add_idx, buffer.size());
+ ASSERT_OK(kvs.Put(keys[add_idx], buffer.data(), buffer.size()));
+ EXPECT_EQ(kvs.KeyCount(), add_idx + 1);
+ for (unsigned verify_idx = 0; verify_idx <= add_idx; verify_idx++) {
+ memset(buffer.data(), 0, buffer.size());
+ ASSERT_OK(kvs.Get(keys[verify_idx], buffer.data(), buffer.size()));
+ for (unsigned i = 0; i < buffer.size(); i++) {
+ EXPECT_EQ(buffer[i], verify_idx);
+ }
+ }
+ }
+
+ // Erase and verify
+ for (unsigned erase_idx = 0; erase_idx < keys.size(); erase_idx++) {
+ ASSERT_OK(kvs.Erase(keys[erase_idx]));
+ EXPECT_EQ(kvs.KeyCount(), keys.size() - erase_idx - 1);
+ for (unsigned verify_idx = 0; verify_idx < keys.size(); verify_idx++) {
+ memset(buffer.data(), 0, buffer.size());
+ if (verify_idx <= erase_idx) {
+ ASSERT_EQ(
+ kvs.Get(keys[verify_idx], buffer.data(), buffer.size()).code(),
+ Status::NOT_FOUND);
+ } else {
+ ASSERT_OK(kvs.Get(keys[verify_idx], buffer.data(), buffer.size()));
+ for (uint32_t i = 0; i < buffer.size(); i++) {
+ EXPECT_EQ(buffer[i], verify_idx);
+ }
+ }
+ }
+ }
+}
+
+TEST_F(KeyValueStoreTest, Enable) {
+ test_partition.Erase(0, test_partition.GetSectorCount());
+
+ KvsAttributes kvs_attr(std::strlen(keys[0]), buffer.size());
+
+ // Verify the data will fit in this test partition. This checks that all the
+ // keys chunks will fit and a header for each sector will fit. It requires 1
+ // empty sector also.
+ const size_t kAllChunkSize = kvs_attr.MinPutSize() * keys.size();
+ const size_t kAllSectorHeaderSizes =
+ kvs_attr.SectorHeaderSize() * (test_partition.GetSectorCount() - 1);
+ const size_t kMinSize = kAllChunkSize + kAllSectorHeaderSizes;
+ const size_t kAvailSectorSpace = test_partition.GetSectorSizeBytes() *
+ (test_partition.GetSectorCount() - 1);
+ if (kAvailSectorSpace < kMinSize) {
+ LOG_INFO("KVS too small, skipping test.");
+ return;
+ }
+
+ // Reset KVS
+ kvs.Disable();
+ ASSERT_OK(kvs.Enable());
+
+ // Add some items
+ for (unsigned add_idx = 0; add_idx < keys.size(); add_idx++) {
+ memset(buffer.data(), add_idx, buffer.size());
+ ASSERT_OK(kvs.Put(keys[add_idx], buffer.data(), buffer.size()));
+ EXPECT_EQ(kvs.KeyCount(), add_idx + 1);
+ }
+
+ // Enable different KVS which should be able to properly setup the same map
+ // from what is stored in flash.
+ ASSERT_OK(kvs_local_.Enable());
+ EXPECT_EQ(kvs_local_.KeyCount(), keys.size());
+
+ // Ensure adding to new KVS works
+ uint8_t value = 0xDA;
+ const char* key = "new_key";
+ ASSERT_OK(kvs_local_.Put(key, &value, sizeof(value)));
+ uint8_t test;
+ ASSERT_OK(kvs_local_.Get(key, &test, sizeof(test)));
+ EXPECT_EQ(value, test);
+ EXPECT_EQ(kvs_local_.KeyCount(), keys.size() + 1);
+
+ // Verify previous data
+ for (unsigned verify_idx = 0; verify_idx < keys.size(); verify_idx++) {
+ memset(buffer.data(), 0, buffer.size());
+ ASSERT_OK(kvs_local_.Get(keys[verify_idx], buffer.data(), buffer.size()));
+ for (uint32_t i = 0; i < buffer.size(); i++) {
+ EXPECT_EQ(buffer[i], verify_idx);
+ }
+ }
+}
+
+TEST_F(KeyValueStoreTest, MultiSector) {
+ test_partition.Erase(0, test_partition.GetSectorCount());
+
+ // Reset KVS
+ kvs.Disable();
+ ASSERT_OK(kvs.Enable());
+
+ // Calculate number of elements to ensure multiple sectors are required.
+ uint16_t add_count =
+ (test_partition.GetSectorSizeBytes() / buffer.size()) + 1;
+
+ if (kvs.GetMaxKeys() < add_count) {
+ LOG_INFO("Sector size too large, skipping test.");
+ return; // this chip has very large sectors, test won't work
+ }
+ if (test_partition.GetSectorCount() < 3) {
+ LOG_INFO("Not enough sectors, skipping test.");
+ return; // need at least 3 sectors for multi-sector test
+ }
+
+ char key[20];
+ for (unsigned add_idx = 0; add_idx < add_count; add_idx++) {
+ memset(buffer.data(), add_idx, buffer.size());
+ snprintf(key, sizeof(key), "key_%u", add_idx);
+ ASSERT_OK(kvs.Put(key, buffer.data(), buffer.size()));
+ EXPECT_EQ(kvs.KeyCount(), add_idx + 1);
+ }
+
+ for (unsigned verify_idx = 0; verify_idx < add_count; verify_idx++) {
+ memset(buffer.data(), 0, buffer.size());
+ snprintf(key, sizeof(key), "key_%u", verify_idx);
+ ASSERT_OK(kvs.Get(key, buffer.data(), buffer.size()));
+ for (uint32_t i = 0; i < buffer.size(); i++) {
+ EXPECT_EQ(buffer[i], verify_idx);
+ }
+ }
+
+ // Check erase
+ for (unsigned erase_idx = 0; erase_idx < add_count; erase_idx++) {
+ snprintf(key, sizeof(key), "key_%u", erase_idx);
+ ASSERT_OK(kvs.Erase(key));
+ EXPECT_EQ(kvs.KeyCount(), add_count - erase_idx - 1);
+ }
+}
+
+TEST_F(KeyValueStoreTest, RewriteValue) {
+ test_partition.Erase(0, test_partition.GetSectorCount());
+
+ // Reset KVS
+ kvs.Disable();
+ ASSERT_OK(kvs.Enable());
+
+ // Write first value
+ const uint8_t kValue1 = 0xDA;
+ const uint8_t kValue2 = 0x12;
+ const char* key = "the_key";
+ ASSERT_OK(kvs.Put(key, &kValue1, sizeof(kValue1)));
+
+ // Verify
+ uint8_t value;
+ ASSERT_OK(kvs.Get(key, reinterpret_cast<uint8_t*>(&value), sizeof(value)));
+ EXPECT_EQ(kValue1, value);
+
+ // Write new value for key
+ ASSERT_OK(kvs.Put(key, &kValue2, sizeof(kValue2)));
+
+ // Verify
+ ASSERT_OK(kvs.Get(key, reinterpret_cast<uint8_t*>(&value), sizeof(value)));
+ EXPECT_EQ(kValue2, value);
+
+ // Verify only 1 element exists
+ EXPECT_EQ(kvs.KeyCount(), 1);
+}
+
+TEST_F(KeyValueStoreTest, OffsetRead) {
+ test_partition.Erase(0, test_partition.GetSectorCount());
+
+ // Reset KVS
+ kvs.Disable();
+ ASSERT_OK(kvs.Enable());
+
+ const char* key = "the_key";
+ constexpr uint8_t kReadSize = 16; // needs to be a multiple of alignment
+ constexpr uint8_t kTestBufferSize = kReadSize * 10;
+ CHECK_GT(buffer.size(), kTestBufferSize);
+ CHECK_LE(kTestBufferSize, 0xFF);
+
+ // Write the entire buffer
+ for (uint8_t i = 0; i < kTestBufferSize; i++) {
+ buffer[i] = i;
+ }
+ ASSERT_OK(kvs.Put(key, buffer.data(), kTestBufferSize));
+ EXPECT_EQ(kvs.KeyCount(), 1);
+
+ // Read in small chunks and verify
+ for (int i = 0; i < kTestBufferSize / kReadSize; i++) {
+ memset(buffer.data(), 0, buffer.size());
+ ASSERT_OK(kvs.Get(key, buffer.data(), kReadSize, i * kReadSize));
+ for (unsigned j = 0; j < kReadSize; j++) {
+ ASSERT_EQ(buffer[j], j + i * kReadSize);
+ }
+ }
+}
+
+TEST_F(KeyValueStoreTest, MultipleRewrite) {
+ // Write many large buffers to force moving to new sector.
+ test_partition.Erase(0, test_partition.GetSectorCount());
+
+ // Reset KVS
+ kvs.Disable();
+ ASSERT_OK(kvs.Enable());
+
+ // Calculate number of elements to ensure multiple sectors are required.
+ unsigned add_count =
+ (test_partition.GetSectorSizeBytes() / buffer.size()) + 1;
+
+ const char* key = "the_key";
+ constexpr uint8_t kGoodVal = 0x60;
+ constexpr uint8_t kBadVal = 0xBA;
+ memset(buffer.data(), kBadVal, buffer.size());
+ for (unsigned add_idx = 0; add_idx < add_count; add_idx++) {
+ if (add_idx == add_count - 1) { // last value
+ memset(buffer.data(), kGoodVal, buffer.size());
+ }
+ ASSERT_OK(kvs.Put(key, buffer.data(), buffer.size()));
+ EXPECT_EQ(kvs.KeyCount(), 1);
+ }
+
+ // Verify
+ memset(buffer.data(), 0, buffer.size());
+ ASSERT_OK(kvs.Get(key, buffer.data(), buffer.size()));
+ for (uint32_t i = 0; i < buffer.size(); i++) {
+ ASSERT_EQ(buffer[i], kGoodVal);
+ }
+}
+
+TEST_F(KeyValueStoreTest, FillSector) {
+ // Write key[0], Write/erase Key[2] multiple times to fill sector check
+ // everything makes sense after.
+ test_partition.Erase(0, test_partition.GetSectorCount());
+
+ // Reset KVS
+ kvs.Disable();
+ ASSERT_OK(kvs.Enable());
+
+ ASSERT_EQ(std::strlen(keys[0]), 8U); // Easier for alignment
+ ASSERT_EQ(std::strlen(keys[2]), 8U); // Easier for alignment
+ constexpr size_t kTestDataSize = 8;
+ KvsAttributes kvs_attr(std::strlen(keys[2]), kTestDataSize);
+ int bytes_remaining =
+ test_partition.GetSectorSizeBytes() - kvs_attr.SectorHeaderSize();
+ constexpr uint8_t kKey0Pattern = 0xBA;
+
+ memset(buffer.data(), kKey0Pattern, kvs_attr.DataSize());
+ ASSERT_OK(kvs.Put(keys[0], buffer.data(), kvs_attr.DataSize()));
+ bytes_remaining -= kvs_attr.MinPutSize();
+ memset(buffer.data(), 1, kvs_attr.DataSize());
+ ASSERT_OK(kvs.Put(keys[2], buffer.data(), kvs_attr.DataSize()));
+ bytes_remaining -= kvs_attr.MinPutSize();
+ EXPECT_EQ(kvs.KeyCount(), 2);
+ ASSERT_OK(kvs.Erase(keys[2]));
+ bytes_remaining -= kvs_attr.EraseSize();
+ EXPECT_EQ(kvs.KeyCount(), 1);
+
+ // Intentionally adding erase size to trigger sector cleanup
+ bytes_remaining += kvs_attr.EraseSize();
+ FillKvs(keys[2], bytes_remaining);
+
+ // Verify key[0]
+ memset(buffer.data(), 0, kvs_attr.DataSize());
+ ASSERT_OK(kvs.Get(keys[0], buffer.data(), kvs_attr.DataSize()));
+ for (uint32_t i = 0; i < kvs_attr.DataSize(); i++) {
+ EXPECT_EQ(buffer[i], kKey0Pattern);
+ }
+}
+
+TEST_F(KeyValueStoreTest, Interleaved) {
+ test_partition.Erase(0, test_partition.GetSectorCount());
+
+ // Reset KVS
+ kvs.Disable();
+ ASSERT_OK(kvs.Enable());
+
+ const uint8_t kValue1 = 0xDA;
+ const uint8_t kValue2 = 0x12;
+ uint8_t value[1];
+ ASSERT_OK(kvs.Put(keys[0], &kValue1, sizeof(kValue1)));
+ EXPECT_EQ(kvs.KeyCount(), 1);
+ ASSERT_OK(kvs.Erase(keys[0]));
+ EXPECT_EQ(kvs.Get(keys[0], value, sizeof(value)), Status::NOT_FOUND);
+ ASSERT_OK(kvs.Put(keys[1], &kValue1, sizeof(kValue1)));
+ ASSERT_OK(kvs.Put(keys[2], &kValue2, sizeof(kValue2)));
+ ASSERT_OK(kvs.Erase(keys[1]));
+ EXPECT_OK(kvs.Get(keys[2], value, sizeof(value)));
+ EXPECT_EQ(kValue2, value[0]);
+
+ EXPECT_EQ(kvs.KeyCount(), 1);
+}
+
+TEST_F(KeyValueStoreTest, BadCrc) {
+ static constexpr uint32_t kTestPattern = 0xBAD0301f;
+ // clang-format off
+ // There is a top and bottom because for each because we don't want to write
+ // the erase 0xFF, especially on encrypted flash.
+ static constexpr uint8_t kKvsTestDataAligned1Top[] = {
+ 0xCD, 0xAB, 0x03, 0x00, 0x01, 0x00, 0xFF, 0xFF, // Sector Header
+ };
+ static constexpr uint8_t kKvsTestDataAligned1Bottom[] = {
+ 0xAA, 0x55, 0xBA, 0xDD, 0x00, 0x00, 0x18, 0x00, // header (BAD CRC)
+ 0x54, 0x65, 0x73, 0x74, 0x4B, 0x65, 0x79, 0x31, // Key (keys[0])
+ 0xDA, // Value
+ 0xAA, 0x55, 0xB5, 0x87, 0x00, 0x00, 0x44, 0x00, // Header (GOOD CRC)
+ 0x4B, 0x65, 0x79, 0x32, // Key (keys[1])
+ 0x1F, 0x30, 0xD0, 0xBA}; // Value
+ static constexpr uint8_t kKvsTestDataAligned2Top[] = {
+ 0xCD, 0xAB, 0x03, 0x00, 0x02, 0x00, 0xFF, 0xFF, // Sector Header
+ };
+ static constexpr uint8_t kKvsTestDataAligned2Bottom[] = {
+ 0xAA, 0x55, 0xBA, 0xDD, 0x00, 0x00, 0x18, 0x00, // header (BAD CRC)
+ 0x54, 0x65, 0x73, 0x74, 0x4B, 0x65, 0x79, 0x31, // Key (keys[0])
+ 0xDA, 0x00, // Value + padding
+ 0xAA, 0x55, 0xB5, 0x87, 0x00, 0x00, 0x44, 0x00, // Header (GOOD CRC)
+ 0x4B, 0x65, 0x79, 0x32, // Key (keys[1])
+ 0x1F, 0x30, 0xD0, 0xBA}; // Value
+ static constexpr uint8_t kKvsTestDataAligned8Top[] = {
+ 0xCD, 0xAB, 0x03, 0x00, 0x08, 0x00, 0xFF, 0xFF, // Sector Header
+ };
+ static constexpr uint8_t kKvsTestDataAligned8Bottom[] = {
+ 0xAA, 0x55, 0xBA, 0xDD, 0x00, 0x00, 0x18, 0x00, // header (BAD CRC)
+ 0x54, 0x65, 0x73, 0x74, 0x4B, 0x65, 0x79, 0x31, // Key (keys[0])
+ 0xDA, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // Value + padding
+ 0xAA, 0x55, 0xB5, 0x87, 0x00, 0x00, 0x44, 0x00, // header (GOOD CRC)
+ 0x4B, 0x65, 0x79, 0x32, 0x00, 0x00, 0x00, 0x00, // Key (keys[1])
+ 0x1F, 0x30, 0xD0, 0xBA, 0x00, 0x00, 0x00, 0x00, // Value + padding
+ };
+ static constexpr uint8_t kKvsTestDataAligned16Top[] = {
+ 0xCD, 0xAB, 0x03, 0x00, 0x10, 0x00, 0xFF, 0xFF, // Sector Header
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // Alignment to 16
+ };
+ static constexpr uint8_t kKvsTestDataAligned16Bottom[] = {
+ 0xAA, 0x55, 0xBA, 0xDD, 0x00, 0x00, 0x18, 0x00, // header (BAD CRC)
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // Alignment to 16
+ 0x54, 0x65, 0x73, 0x74, 0x4B, 0x65, 0x79, 0x31, // Key (keys[0])
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // Alignment to 16
+ 0xDA, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // Value + padding
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // Alignment to 16
+ 0xAA, 0x55, 0xB5, 0x87, 0x00, 0x00, 0x44, 0x00, // header (GOOD CRC)
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // Alignment to 16
+ 0x4B, 0x65, 0x79, 0x32, 0x00, 0x00, 0x00, 0x00, // Key (keys[1])
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // Alignment to 16
+ 0x1F, 0x30, 0xD0, 0xBA, 0x00, 0x00, 0x00, 0x00, // Value + padding
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // Alignment to 16
+ };
+ // clang-format on
+ ASSERT_OK(test_partition.Erase(0, test_partition.GetSectorCount()));
+
+ // We don't actually care about the size values provided, since we are only
+ // using kvs_attr to get Sector Size
+ KvsAttributes kvs_attr(8, 8);
+ if (test_partition.GetAlignmentBytes() == 1) {
+ ASSERT_OK(test_partition.Write(
+ 0, kKvsTestDataAligned1Top, sizeof(kKvsTestDataAligned1Top)));
+ ASSERT_OK(test_partition.Write(kvs_attr.SectorHeaderSize(),
+ kKvsTestDataAligned1Bottom,
+ sizeof(kKvsTestDataAligned1Bottom)));
+ } else if (test_partition.GetAlignmentBytes() == 2) {
+ ASSERT_OK(test_partition.Write(
+ 0, kKvsTestDataAligned2Top, sizeof(kKvsTestDataAligned2Top)));
+ ASSERT_OK(test_partition.Write(kvs_attr.SectorHeaderSize(),
+ kKvsTestDataAligned2Bottom,
+ sizeof(kKvsTestDataAligned2Bottom)));
+ } else if (test_partition.GetAlignmentBytes() == 8) {
+ ASSERT_OK(test_partition.Write(
+ 0, kKvsTestDataAligned8Top, sizeof(kKvsTestDataAligned8Top)));
+ ASSERT_OK(test_partition.Write(kvs_attr.SectorHeaderSize(),
+ kKvsTestDataAligned8Bottom,
+ sizeof(kKvsTestDataAligned8Bottom)));
+ } else if (test_partition.GetAlignmentBytes() == 16) {
+ ASSERT_OK(test_partition.Write(
+ 0, kKvsTestDataAligned16Top, sizeof(kKvsTestDataAligned16Top)));
+ ASSERT_OK(test_partition.Write(kvs_attr.SectorHeaderSize(),
+ kKvsTestDataAligned16Bottom,
+ sizeof(kKvsTestDataAligned16Bottom)));
+ } else {
+ LOG_ERROR("Test only supports 1, 2, 8 and 16 byte alignments.");
+ ASSERT_OK(false);
+ }
+
+ EXPECT_EQ(kvs_local_.Enable(), Status::OK);
+ EXPECT_TRUE(kvs_local_.IsEnabled());
+
+ EXPECT_EQ(kvs_local_.Get(keys[0], buffer.data(), 1), Status::DATA_LOSS);
+
+ // Value with correct CRC should still be available.
+ uint32_t test2 = 0;
+ ASSERT_OK(kvs_local_.Get(
+ keys[1], reinterpret_cast<uint8_t*>(&test2), sizeof(test2)));
+ EXPECT_EQ(kTestPattern, test2);
+
+ // Test rewriting over corrupted data.
+ ASSERT_OK(kvs_local_.Put(keys[0], kTestPattern));
+ test2 = 0;
+ EXPECT_OK(kvs_local_.Get(keys[0], &test2));
+ EXPECT_EQ(kTestPattern, test2);
+
+ // Check correct when re-enabled
+ kvs_local_.Disable();
+ EXPECT_EQ(kvs_local_.Enable(), Status::OK);
+ test2 = 0;
+ EXPECT_OK(kvs_local_.Get(keys[0], &test2));
+ EXPECT_EQ(kTestPattern, test2);
+}
+
+TEST_F(KeyValueStoreTest, TestVersion2) {
+ static constexpr uint32_t kTestPattern = 0xBAD0301f;
+ // Since this test is not run on encypted flash, we can write the clean
+ // pending flag for just this test.
+ static constexpr uint8_t kKvsTestDataAligned1[] = {
+ 0xCD, 0xAB, 0x02, 0x00, 0x00, 0x00, 0xFF, 0xFF, // Sector Header
+ 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // Clean pending flag
+ 0xAA, 0x55, 0xB5, 0x87, 0x00, 0x00, 0x44, 0x00, // Header (GOOD CRC)
+ 0x4B, 0x65, 0x79, 0x32, // Key (keys[1])
+ 0x1F, 0x30, 0xD0, 0xBA}; // Value
+
+ if (test_partition.GetAlignmentBytes() == 1) {
+ // Test only runs on 1 byte alignment partitions
+ test_partition.Erase(0, test_partition.GetSectorCount());
+ test_partition.Write(0, kKvsTestDataAligned1, sizeof(kKvsTestDataAligned1));
+ EXPECT_OK(kvs_local_.Enable());
+ uint32_t test2 = 0;
+ ASSERT_OK(kvs_local_.Get(
+ keys[1], reinterpret_cast<uint8_t*>(&test2), sizeof(test2)));
+ EXPECT_EQ(kTestPattern, test2);
+ }
+}
+
+TEST_F(KeyValueStoreTest, ReEnable) {
+ test_partition.Erase(0, test_partition.GetSectorCount());
+
+ // Reset KVS
+ kvs.Disable();
+ ASSERT_OK(kvs.Enable());
+ kvs.Disable();
+
+ EXPECT_OK(kvs_local_.Enable());
+ // Write value
+ const uint8_t kValue = 0xDA;
+ ASSERT_OK(kvs_local_.Put(keys[0], &kValue, sizeof(kValue)));
+ uint8_t value;
+ ASSERT_OK(kvs_local_.Get(
+ keys[0], reinterpret_cast<uint8_t*>(&value), sizeof(value)));
+
+ // Verify
+ EXPECT_EQ(kValue, value);
+}
+
+TEST_F(KeyValueStoreTest, Erase) {
+ test_partition.Erase(0, test_partition.GetSectorCount());
+
+ // Reset KVS
+ kvs.Disable();
+ ASSERT_OK(kvs.Enable());
+
+ // Write value
+ const uint8_t kValue = 0xDA;
+ ASSERT_OK(kvs.Put(keys[0], &kValue, sizeof(kValue)));
+
+ ASSERT_OK(kvs.Erase(keys[0]));
+ uint8_t value[1];
+ ASSERT_EQ(kvs.Get(keys[0], value, sizeof(value)), Status::NOT_FOUND);
+
+ // Reset KVS, ensure captured at enable
+ kvs.Disable();
+ ASSERT_OK(kvs.Enable());
+
+ ASSERT_EQ(kvs.Get(keys[0], value, sizeof(value)), Status::NOT_FOUND);
+}
+
+TEST_F(KeyValueStoreTest, TemplatedPutAndGet) {
+ test_partition.Erase(0, test_partition.GetSectorCount());
+ // Reset KVS
+ kvs.Disable();
+ ASSERT_OK(kvs.Enable());
+ // Store a value with the convenience method.
+ const uint32_t kValue = 0x12345678;
+ ASSERT_OK(kvs.Put(keys[0], kValue));
+
+ // Read it back with the other convenience method.
+ uint32_t value;
+ ASSERT_OK(kvs.Get(keys[0], &value));
+ ASSERT_EQ(kValue, value);
+
+ // Make sure we cannot get something where size isn't what we expect
+ const uint8_t kSmallValue = 0xBA;
+ uint8_t small_value = kSmallValue;
+ ASSERT_EQ(kvs.Get(keys[0], &small_value), Status::INVALID_ARGUMENT);
+ ASSERT_EQ(small_value, kSmallValue);
+}
+
+TEST_F(KeyValueStoreTest, SameValueRewrite) {
+ static constexpr uint32_t kTestPattern = 0xBAD0301f;
+ // clang-format off
+ static constexpr uint8_t kKvsTestDataAligned1Top[] = {
+ 0xCD, 0xAB, 0x02, 0x00, 0x00, 0x00, 0xFF, 0xFF, // Sector Header
+ };
+ static constexpr uint8_t kKvsTestDataAligned1Bottom[] = {
+ 0xAA, 0x55, 0xB5, 0x87, 0x00, 0x00, 0x44, 0x00, // Header (GOOD CRC)
+ 0x4B, 0x65, 0x79, 0x32, // Key (keys[1])
+ 0x1F, 0x30, 0xD0, 0xBA}; // Value
+ static constexpr uint8_t kKvsTestDataAligned2Top[] = {
+ 0xCD, 0xAB, 0x03, 0x00, 0x02, 0x00, 0xFF, 0xFF, // Sector Header
+ };
+ static constexpr uint8_t kKvsTestDataAligned2Bottom[] = {
+ 0xAA, 0x55, 0xB5, 0x87, 0x00, 0x00, 0x44, 0x00, // Header (GOOD CRC)
+ 0x4B, 0x65, 0x79, 0x32, // Key (keys[1])
+ 0x1F, 0x30, 0xD0, 0xBA}; // Value
+ static constexpr uint8_t kKvsTestDataAligned8Top[] = {
+ 0xCD, 0xAB, 0x03, 0x00, 0x08, 0x00, 0xFF, 0xFF, // Sector Header
+ };
+ static constexpr uint8_t kKvsTestDataAligned8Bottom[] = {
+ 0xAA, 0x55, 0xB5, 0x87, 0x00, 0x00, 0x44, 0x00, // header (GOOD CRC)
+ 0x4B, 0x65, 0x79, 0x32, 0x00, 0x00, 0x00, 0x00, // Key (keys[1])
+ 0x1F, 0x30, 0xD0, 0xBA, 0x00, 0x00, 0x00, 0x00, // Value + padding
+ };
+ static constexpr uint8_t kKvsTestDataAligned16Top[] = {
+ 0xCD, 0xAB, 0x03, 0x00, 0x10, 0x00, 0xFF, 0xFF, // Sector Header
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // Alignment to 16
+ };
+ static constexpr uint8_t kKvsTestDataAligned16Bottom[] = {
+ 0xAA, 0x55, 0xB5, 0x87, 0x00, 0x00, 0x44, 0x00, // header (GOOD CRC)
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // Alignment to 16
+ 0x4B, 0x65, 0x79, 0x32, 0x00, 0x00, 0x00, 0x00, // Key (keys[1])
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // Alignment to 16
+ 0x1F, 0x30, 0xD0, 0xBA, 0x00, 0x00, 0x00, 0x00, // Value + padding
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // Alignment to 16
+ };
+ // clang-format on
+
+ ASSERT_OK(test_partition.Erase(0, test_partition.GetSectorCount()));
+ // We don't actually care about the size values provided, since we are only
+ // using kvs_attr to get Sector Size
+ KvsAttributes kvs_attr(8, 8);
+ pw::FlashPartition::Address address = kvs_attr.SectorHeaderSize();
+ if (test_partition.GetAlignmentBytes() == 1) {
+ ASSERT_OK(test_partition.Write(
+ 0, kKvsTestDataAligned1Top, sizeof(kKvsTestDataAligned1Top)));
+ ASSERT_OK(test_partition.Write(address,
+ kKvsTestDataAligned1Bottom,
+ sizeof(kKvsTestDataAligned1Bottom)));
+ address += sizeof(kKvsTestDataAligned1Bottom);
+ } else if (test_partition.GetAlignmentBytes() == 2) {
+ ASSERT_OK(test_partition.Write(
+ 0, kKvsTestDataAligned2Top, sizeof(kKvsTestDataAligned2Top)));
+ ASSERT_OK(test_partition.Write(address,
+ kKvsTestDataAligned2Bottom,
+ sizeof(kKvsTestDataAligned2Bottom)));
+ address += sizeof(kKvsTestDataAligned2Bottom);
+ } else if (test_partition.GetAlignmentBytes() == 8) {
+ ASSERT_OK(test_partition.Write(
+ 0, kKvsTestDataAligned8Top, sizeof(kKvsTestDataAligned8Top)));
+ ASSERT_OK(test_partition.Write(address,
+ kKvsTestDataAligned8Bottom,
+ sizeof(kKvsTestDataAligned8Bottom)));
+ address += sizeof(kKvsTestDataAligned8Bottom);
+ } else if (test_partition.GetAlignmentBytes() == 16) {
+ ASSERT_OK(test_partition.Write(
+ 0, kKvsTestDataAligned16Top, sizeof(kKvsTestDataAligned16Top)));
+ ASSERT_OK(test_partition.Write(address,
+ kKvsTestDataAligned16Bottom,
+ sizeof(kKvsTestDataAligned16Bottom)));
+ address += sizeof(kKvsTestDataAligned16Bottom);
+ } else {
+ LOG_ERROR("Test only supports 1, 2, 8 and 16 byte alignments.");
+ ASSERT_EQ(true, false);
+ }
+
+ ASSERT_OK(kvs_local_.Enable());
+ EXPECT_TRUE(kvs_local_.IsEnabled());
+
+ // Put in same key/value pair
+ ASSERT_OK(kvs_local_.Put(keys[1], kTestPattern));
+
+ bool is_erased = false;
+ ASSERT_OK(test_partition.IsChunkErased(
+ address, test_partition.GetAlignmentBytes(), &is_erased));
+ EXPECT_EQ(is_erased, true);
+}
+
+// This test is derived from bug that was discovered. Testing this corner case
+// relies on creating a new key-value just under the size that is left over in
+// the sector.
+TEST_F(KeyValueStoreTest, FillSector2) {
+ if (test_partition.GetSectorCount() < 3) {
+ LOG_INFO("Not enough sectors, skipping test.");
+ return; // need at least 3 sectors
+ }
+
+ // Reset KVS
+ kvs.Disable();
+ test_partition.Erase(0, test_partition.GetSectorCount());
+ ASSERT_OK(kvs.Enable());
+
+ // Start of by filling flash sector to near full
+ constexpr int kHalfBufferSize = buffer.size() / 2;
+ const int kSizeToFill = test_partition.GetSectorSizeBytes() - kHalfBufferSize;
+ constexpr size_t kTestDataSize = 8;
+ KvsAttributes kvs_attr(std::strlen(keys[2]), kTestDataSize);
+
+ FillKvs(keys[2], kSizeToFill);
+
+ // Find out how much space is remaining for new key-value and confirm it
+ // makes sense.
+ size_t new_keyvalue_size = 0;
+ size_t alignment = test_partition.GetAlignmentBytes();
+ // Starts on second sector since it will try to keep first sector free
+ pw::FlashPartition::Address read_address =
+ 2 * test_partition.GetSectorSizeBytes() - alignment;
+ for (; read_address > 0; read_address -= alignment) {
+ bool is_erased = false;
+ ASSERT_OK(
+ test_partition.IsChunkErased(read_address, alignment, &is_erased));
+ if (is_erased) {
+ new_keyvalue_size += alignment;
+ } else {
+ break;
+ }
+ }
+
+ size_t expected_remaining = test_partition.GetSectorSizeBytes() -
+ kvs_attr.SectorHeaderSize() - kSizeToFill;
+ ASSERT_EQ(new_keyvalue_size, expected_remaining);
+
+ const char* kNewKey = "NewKey";
+ constexpr size_t kValueLessThanChunkHeaderSize = 2;
+ constexpr uint8_t kTestPattern = 0xBA;
+ new_keyvalue_size -= kValueLessThanChunkHeaderSize;
+ memset(buffer.data(), kTestPattern, new_keyvalue_size);
+ ASSERT_OK(kvs.Put(kNewKey, buffer.data(), new_keyvalue_size));
+
+ // In failed corner case, adding new key is deceptively successful. It isn't
+ // until KVS is disabled and reenabled that issue can be detected.
+ kvs.Disable();
+ ASSERT_OK(kvs.Enable());
+
+ // Might as well check that new key-value is what we expect it to be
+ ASSERT_OK(kvs.Get(kNewKey, buffer.data(), new_keyvalue_size));
+ for (size_t i = 0; i < new_keyvalue_size; i++) {
+ EXPECT_EQ(buffer[i], kTestPattern);
+ }
+}
+
+TEST_F(KeyValueStoreTest, GetValueSizeTests) {
+ constexpr uint16_t kSizeOfValueToFill = 20U;
+ constexpr uint8_t kKey0Pattern = 0xBA;
+ uint16_t value_size = 0;
+ // Start off with disabled KVS
+ kvs.Disable();
+
+ // Try getting value when KVS is disabled, expect failure
+ EXPECT_EQ(kvs.GetValueSize(keys[0], &value_size),
+ Status::FAILED_PRECONDITION);
+
+ // Reset KVS
+ test_partition.Erase(0, test_partition.GetSectorCount());
+ ASSERT_OK(kvs.Enable());
+
+ // Try some case that are expected to fail
+ ASSERT_EQ(kvs.GetValueSize(keys[0], &value_size), Status::NOT_FOUND);
+ ASSERT_EQ(kvs.GetValueSize(nullptr, &value_size), Status::INVALID_ARGUMENT);
+ ASSERT_EQ(kvs.GetValueSize(keys[0], nullptr), Status::INVALID_ARGUMENT);
+
+ // Add key[0] and test we get the right value size for it.
+ memset(buffer.data(), kKey0Pattern, kSizeOfValueToFill);
+ ASSERT_OK(kvs.Put(keys[0], buffer.data(), kSizeOfValueToFill));
+ ASSERT_OK(kvs.GetValueSize(keys[0], &value_size));
+ ASSERT_EQ(value_size, kSizeOfValueToFill);
+
+ // Verify after erase key is not found
+ ASSERT_OK(kvs.Erase(keys[0]));
+ ASSERT_EQ(kvs.GetValueSize(keys[0], &value_size), Status::NOT_FOUND);
+}
+
+TEST_F(KeyValueStoreTest, CanFitEntryTests) {
+ // Reset KVS
+ kvs.Disable();
+ test_partition.Erase(0, test_partition.GetSectorCount());
+ ASSERT_OK(kvs.Enable());
+
+ // Get exactly the number of bytes that can fit in the space remaining for
+ // a large value, accounting for alignment.
+ constexpr uint16_t kTestKeySize = 2;
+ size_t space_remaining =
+ test_partition.GetSectorSizeBytes() //
+ - RoundUpForAlignment(KeyValueStore::kHeaderSize) // Sector Header
+ - RoundUpForAlignment(KeyValueStore::kHeaderSize) // Cleaning Header
+ - RoundUpForAlignment(KeyValueStore::kHeaderSize) // Chunk Header
+ - RoundUpForAlignment(kTestKeySize);
+ space_remaining -= test_partition.GetAlignmentBytes() / 2;
+ space_remaining = RoundUpForAlignment(space_remaining);
+
+ EXPECT_TRUE(kvs.CanFitEntry(kTestKeySize, space_remaining));
+ EXPECT_FALSE(kvs.CanFitEntry(kTestKeySize, space_remaining + 1));
+}
+
+TEST_F(KeyValueStoreTest, DifferentValueSameCrc16) {
+ test_partition.Erase(0, test_partition.GetSectorCount());
+ const char kKey[] = "k";
+ // With the key and our CRC16 algorithm these both have CRC of 0x82AE
+ // Given they are the same size and same key, the KVS will need to check
+ // the actual bits to know they are different.
+ const char kValue1[] = {'d', 'a', 't'};
+ const char kValue2[] = {'u', 'c', 'd'};
+
+ // Verify the CRC matches
+ ASSERT_EQ(CalcKvsCrc(kKey, kValue1, sizeof(kValue1)),
+ CalcKvsCrc(kKey, kValue2, sizeof(kValue2)));
+
+ // Reset KVS
+ kvs.Disable();
+ ASSERT_OK(kvs.Enable());
+ ASSERT_OK(kvs.Put(kKey, kValue1));
+
+ // Now try to rewrite with the similar value.
+ ASSERT_OK(kvs.Put(kKey, kValue2));
+
+ // Read it back and check it is correct
+ char value[3];
+ ASSERT_OK(kvs.Get(kKey, value, sizeof(value)));
+ ASSERT_EQ(memcmp(value, kValue2, sizeof(value)), 0);
+}
+
+TEST_F(KeyValueStoreTest, CallingEraseTwice) {
+ test_partition.Erase(0, test_partition.GetSectorCount());
+
+ // Reset KVS
+ kvs.Disable();
+ ASSERT_OK(kvs.Enable());
+
+ const uint8_t kValue = 0xDA;
+ ASSERT_OK(kvs.Put(keys[0], &kValue, sizeof(kValue)));
+ ASSERT_OK(kvs.Erase(keys[0]));
+ uint16_t crc = CalcTestPartitionCrc();
+ EXPECT_EQ(kvs.Erase(keys[0]), Status::NOT_FOUND);
+ // Verify the flash has not changed
+ EXPECT_EQ(crc, CalcTestPartitionCrc());
+}
+
+void __attribute__((noinline)) StackHeavyPartialClean() {
+ CHECK_GE(test_partition.GetSectorCount(), 2);
+ FlashSubPartition test_partition_sector1(&test_partition, 0, 1);
+ FlashSubPartition test_partition_sector2(&test_partition, 1, 1);
+
+ KeyValueStore kvs1(&test_partition_sector1);
+ KeyValueStore kvs2(&test_partition_sector2);
+
+ test_partition.Erase(0, test_partition.GetSectorCount());
+
+ ASSERT_OK(kvs1.Enable());
+ ASSERT_OK(kvs2.Enable());
+
+ int values1[3] = {100, 101, 102};
+ ASSERT_OK(kvs1.Put(keys[0], values1[0]));
+ ASSERT_OK(kvs1.Put(keys[1], values1[1]));
+ ASSERT_OK(kvs1.Put(keys[2], values1[2]));
+
+ int values2[3] = {200, 201, 202};
+ ASSERT_OK(kvs2.Put(keys[0], values2[0]));
+ ASSERT_OK(kvs2.Put(keys[1], values2[1]));
+ ASSERT_OK(kvs2.Erase(keys[1]));
+
+ kvs1.Disable();
+ kvs2.Disable();
+
+ // Key 0 is value1 in first sector, value 2 in second
+ // Key 1 is value1 in first sector, erased in second
+ // key 2 is only in first sector
+
+ uint64_t mark_clean_count = 5;
+ ASSERT_OK(PaddedWrite(&test_partition_sector1,
+ RoundUpForAlignment(KeyValueStore::kHeaderSize),
+ reinterpret_cast<uint8_t*>(&mark_clean_count),
+ sizeof(uint64_t)));
+
+ // Reset KVS
+ kvs.Disable();
+ ASSERT_OK(kvs.Enable());
+ int value;
+ ASSERT_OK(kvs.Get(keys[0], &value));
+ ASSERT_EQ(values2[0], value);
+ ASSERT_EQ(kvs.Get(keys[1], &value), Status::NOT_FOUND);
+ ASSERT_OK(kvs.Get(keys[2], &value));
+ ASSERT_EQ(values1[2], value);
+
+ if (test_partition.GetSectorCount() == 2) {
+ EXPECT_EQ(kvs.PendingCleanCount(), 0u);
+ // Has forced a clean, mark again for next test
+ return; // Not enough sectors to test 2 partial cleans.
+ } else {
+ EXPECT_EQ(kvs.PendingCleanCount(), 1u);
+ }
+
+ mark_clean_count--;
+ ASSERT_OK(PaddedWrite(&test_partition_sector2,
+ RoundUpForAlignment(KeyValueStore::kHeaderSize),
+ reinterpret_cast<uint8_t*>(&mark_clean_count),
+ sizeof(uint64_t)));
+ // Reset KVS
+ kvs.Disable();
+ ASSERT_OK(kvs.Enable());
+ EXPECT_EQ(kvs.PendingCleanCount(), 2u);
+ ASSERT_OK(kvs.Get(keys[0], &value));
+ ASSERT_EQ(values1[0], value);
+ ASSERT_OK(kvs.Get(keys[1], &value));
+ ASSERT_EQ(values1[1], value);
+ ASSERT_OK(kvs.Get(keys[2], &value));
+ ASSERT_EQ(values1[2], value);
+}
+
+TEST_F(KeyValueStoreTest, PartialClean) {
+ if (CurrentTaskStackFree() < sizeof(KeyValueStore) * 2) {
+ LOG_ERROR("Not enough stack for test, skipping");
+ return;
+ }
+ StackHeavyPartialClean();
+}
+
+void __attribute__((noinline)) StackHeavyCleanAll() {
+ CHECK_GE(test_partition.GetSectorCount(), 2);
+ FlashSubPartition test_partition_sector1(&test_partition, 0, 1);
+
+ KeyValueStore kvs1(&test_partition_sector1);
+ test_partition.Erase(0, test_partition.GetSectorCount());
+
+ ASSERT_OK(kvs1.Enable());
+
+ int values1[3] = {100, 101, 102};
+ ASSERT_OK(kvs1.Put(keys[0], values1[0]));
+ ASSERT_OK(kvs1.Put(keys[1], values1[1]));
+ ASSERT_OK(kvs1.Put(keys[2], values1[2] - 100)); // force a rewrite
+ ASSERT_OK(kvs1.Put(keys[2], values1[2]));
+
+ kvs1.Disable();
+
+ uint64_t mark_clean_count = 5;
+ ASSERT_OK(PaddedWrite(&test_partition_sector1,
+ RoundUpForAlignment(KeyValueStore::kHeaderSize),
+ reinterpret_cast<uint8_t*>(&mark_clean_count),
+ sizeof(uint64_t)));
+
+ // Reset KVS
+ kvs.Disable();
+ ASSERT_OK(kvs.Enable());
+ int value;
+ EXPECT_EQ(kvs.PendingCleanCount(), 1u);
+ ASSERT_OK(kvs.CleanAll());
+ EXPECT_EQ(kvs.PendingCleanCount(), 0u);
+ ASSERT_OK(kvs.Get(keys[0], &value));
+ ASSERT_EQ(values1[0], value);
+ ASSERT_OK(kvs.Get(keys[1], &value));
+ ASSERT_EQ(values1[1], value);
+ ASSERT_OK(kvs.Get(keys[2], &value));
+ ASSERT_EQ(values1[2], value);
+}
+
+TEST_F(KeyValueStoreTest, CleanAll) {
+ if (CurrentTaskStackFree() < sizeof(KeyValueStore) * 1) {
+ LOG_ERROR("Not enough stack for test, skipping");
+ return;
+ }
+ StackHeavyCleanAll();
+}
+
+void __attribute__((noinline)) StackHeavyPartialCleanLargeCounts() {
+ CHECK_GE(test_partition.GetSectorCount(), 2);
+ FlashSubPartition test_partition_sector1(&test_partition, 0, 1);
+ FlashSubPartition test_partition_sector2(&test_partition, 1, 1);
+
+ KeyValueStore kvs1(&test_partition_sector1);
+ KeyValueStore kvs2(&test_partition_sector2);
+
+ test_partition.Erase(0, test_partition.GetSectorCount());
+
+ ASSERT_OK(kvs1.Enable());
+ ASSERT_OK(kvs2.Enable());
+
+ int values1[3] = {100, 101, 102};
+ ASSERT_OK(kvs1.Put(keys[0], values1[0]));
+ ASSERT_OK(kvs1.Put(keys[1], values1[1]));
+ ASSERT_OK(kvs1.Put(keys[2], values1[2]));
+
+ int values2[3] = {200, 201, 202};
+ ASSERT_OK(kvs2.Put(keys[0], values2[0]));
+ ASSERT_OK(kvs2.Put(keys[1], values2[1]));
+ ASSERT_OK(kvs2.Erase(keys[1]));
+
+ kvs1.Disable();
+ kvs2.Disable();
+ kvs.Disable();
+
+ // Key 0 is value1 in first sector, value 2 in second
+ // Key 1 is value1 in first sector, erased in second
+ // key 2 is only in first sector
+
+ uint64_t mark_clean_count = 4569877515;
+ ASSERT_OK(PaddedWrite(&test_partition_sector1,
+ RoundUpForAlignment(KeyValueStore::kHeaderSize),
+ reinterpret_cast<uint8_t*>(&mark_clean_count),
+ sizeof(uint64_t)));
+
+ // Reset KVS
+ ASSERT_OK(kvs.Enable());
+ int value;
+ ASSERT_OK(kvs.Get(keys[0], &value));
+ ASSERT_EQ(values2[0], value);
+ ASSERT_EQ(kvs.Get(keys[1], &value), Status::NOT_FOUND);
+ ASSERT_OK(kvs.Get(keys[2], &value));
+ ASSERT_EQ(values1[2], value);
+
+ if (test_partition.GetSectorCount() == 2) {
+ EXPECT_EQ(kvs.PendingCleanCount(), 0u);
+ // Has forced a clean, mark again for next test
+ // Has forced a clean, mark again for next test
+ return; // Not enough sectors to test 2 partial cleans.
+ } else {
+ EXPECT_EQ(kvs.PendingCleanCount(), 1u);
+ }
+ kvs.Disable();
+
+ mark_clean_count--;
+ ASSERT_OK(PaddedWrite(&test_partition_sector2,
+ RoundUpForAlignment(KeyValueStore::kHeaderSize),
+ reinterpret_cast<uint8_t*>(&mark_clean_count),
+ sizeof(uint64_t)));
+ // Reset KVS
+ ASSERT_OK(kvs.Enable());
+ EXPECT_EQ(kvs.PendingCleanCount(), 2u);
+ ASSERT_OK(kvs.Get(keys[0], &value));
+ ASSERT_EQ(values1[0], value);
+ ASSERT_OK(kvs.Get(keys[1], &value));
+ ASSERT_EQ(values1[1], value);
+ ASSERT_OK(kvs.Get(keys[2], &value));
+ ASSERT_EQ(values1[2], value);
+}
+
+TEST_F(KeyValueStoreTest, PartialCleanLargeCounts) {
+ if (CurrentTaskStackFree() < sizeof(KeyValueStore) * 2) {
+ LOG_ERROR("Not enough stack for test, skipping");
+ return;
+ }
+ StackHeavyPartialCleanLargeCounts();
+}
+
+void __attribute__((noinline)) StackHeavyRecoverNoFreeSectors() {
+ CHECK_GE(test_partition.GetSectorCount(), 2);
+ FlashSubPartition test_partition_sector1(&test_partition, 0, 1);
+ FlashSubPartition test_partition_sector2(&test_partition, 1, 1);
+ FlashSubPartition test_partition_both(&test_partition, 0, 2);
+
+ KeyValueStore kvs1(&test_partition_sector1);
+ KeyValueStore kvs2(&test_partition_sector2);
+ KeyValueStore kvs_both(&test_partition_both);
+
+ test_partition.Erase(0, test_partition.GetSectorCount());
+
+ ASSERT_OK(kvs1.Enable());
+ ASSERT_OK(kvs2.Enable());
+
+ int values[3] = {100, 101};
+ ASSERT_OK(kvs1.Put(keys[0], values[0]));
+ ASSERT_FALSE(kvs1.HasEmptySector());
+ ASSERT_OK(kvs2.Put(keys[1], values[1]));
+ ASSERT_FALSE(kvs2.HasEmptySector());
+
+ kvs1.Disable();
+ kvs2.Disable();
+
+ // Reset KVS
+ ASSERT_OK(kvs_both.Enable());
+ ASSERT_TRUE(kvs_both.HasEmptySector());
+ int value;
+ ASSERT_OK(kvs_both.Get(keys[0], &value));
+ ASSERT_EQ(values[0], value);
+ ASSERT_OK(kvs_both.Get(keys[1], &value));
+ ASSERT_EQ(values[1], value);
+}
+
+TEST_F(KeyValueStoreTest, RecoverNoFreeSectors) {
+ if (CurrentTaskStackFree() < sizeof(KeyValueStore) * 3) {
+ LOG_ERROR("Not enough stack for test, skipping");
+ return;
+ }
+ StackHeavyRecoverNoFreeSectors();
+}
+
+void __attribute__((noinline)) StackHeavyCleanOneSector() {
+ CHECK_GE(test_partition.GetSectorCount(), 2);
+ FlashSubPartition test_partition_sector1(&test_partition, 0, 1);
+ FlashSubPartition test_partition_sector2(&test_partition, 1, 1);
+
+ KeyValueStore kvs1(&test_partition_sector1);
+
+ test_partition.Erase(0, test_partition.GetSectorCount());
+
+ ASSERT_OK(kvs1.Enable());
+
+ int values[3] = {100, 101, 102};
+ ASSERT_OK(kvs1.Put(keys[0], values[0]));
+ ASSERT_OK(kvs1.Put(keys[1], values[1]));
+ ASSERT_OK(kvs1.Put(keys[2], values[2]));
+
+ kvs1.Disable();
+ kvs.Disable();
+
+ uint64_t mark_clean_count = 1;
+ ASSERT_OK(PaddedWrite(&test_partition_sector1,
+ RoundUpForAlignment(KeyValueStore::kHeaderSize),
+ reinterpret_cast<uint8_t*>(&mark_clean_count),
+ sizeof(uint64_t)));
+
+ // Reset KVS
+ ASSERT_OK(kvs.Enable());
+
+ EXPECT_EQ(kvs.PendingCleanCount(), 1u);
+
+ bool all_sectors_have_been_cleaned = false;
+ ASSERT_OK(kvs.CleanOneSector(&all_sectors_have_been_cleaned));
+ EXPECT_EQ(all_sectors_have_been_cleaned, true);
+ EXPECT_EQ(kvs.PendingCleanCount(), 0u);
+ ASSERT_OK(kvs.CleanOneSector(&all_sectors_have_been_cleaned));
+ EXPECT_EQ(all_sectors_have_been_cleaned, true);
+ int value;
+ ASSERT_OK(kvs.Get(keys[0], &value));
+ ASSERT_EQ(values[0], value);
+ ASSERT_OK(kvs.Get(keys[1], &value));
+ ASSERT_EQ(values[1], value);
+ ASSERT_OK(kvs.Get(keys[2], &value));
+ ASSERT_EQ(values[2], value);
+}
+
+TEST_F(KeyValueStoreTest, CleanOneSector) {
+ if (CurrentTaskStackFree() < sizeof(KeyValueStore)) {
+ LOG_ERROR("Not enough stack for test, skipping");
+ return;
+ }
+ StackHeavyCleanOneSector();
+}
+
+#if USE_MEMORY_BUFFER
+
+TEST_F(KeyValueStoreTest, LargePartition) {
+ if (CurrentTaskStackFree() < sizeof(KeyValueStore)) {
+ LOG_ERROR("Not enough stack for test, skipping");
+ return;
+ }
+ large_test_partition.Erase(0, large_test_partition.GetSectorCount());
+ KeyValueStore large_kvs(&large_test_partition);
+ // Reset KVS
+ large_kvs.Disable();
+ ASSERT_OK(large_kvs.Enable());
+
+ const uint8_t kValue1 = 0xDA;
+ const uint8_t kValue2 = 0x12;
+ uint8_t value[1];
+ ASSERT_OK(large_kvs.Put(keys[0], &kValue1, sizeof(kValue1)));
+ EXPECT_EQ(large_kvs.KeyCount(), 1);
+ ASSERT_OK(large_kvs.Erase(keys[0]));
+ EXPECT_EQ(large_kvs.Get(keys[0], value, sizeof(value)), Status::NOT_FOUND);
+ ASSERT_OK(large_kvs.Put(keys[1], &kValue1, sizeof(kValue1)));
+ ASSERT_OK(large_kvs.Put(keys[2], &kValue2, sizeof(kValue2)));
+ ASSERT_OK(large_kvs.Erase(keys[1]));
+ EXPECT_OK(large_kvs.Get(keys[2], value, sizeof(value)));
+ EXPECT_EQ(kValue2, value[0]);
+ ASSERT_EQ(large_kvs.Get(keys[1], &value), Status::NOT_FOUND);
+ EXPECT_EQ(large_kvs.KeyCount(), 1);
+}
+#endif // USE_MEMORY_BUFFER
+
+} // namespace pw
diff --git a/pw_kvs/public/pw_kvs/flash.h b/pw_kvs/public/pw_kvs/flash.h
new file mode 100644
index 0000000..5cb80cd
--- /dev/null
+++ b/pw_kvs/public/pw_kvs/flash.h
@@ -0,0 +1,33 @@
+// 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 "pw_kvs/devices/flash_memory.h"
+
+namespace pw {
+
+// Writes a buffer which is not guaranteed to be aligned, pads remaining
+// bytes with 0.
+Status PaddedWrite(FlashPartition* partition,
+ FlashPartition::Address address,
+ const uint8_t* buffer,
+ uint16_t size);
+
+// Read into a buffer when size is not guaranteed to be aligned.
+Status UnalignedRead(FlashPartition* partition,
+ uint8_t* buffer,
+ FlashPartition::Address address,
+ uint16_t size);
+
+} // namespace pw
diff --git a/pw_kvs/public/pw_kvs/flash_memory.h b/pw_kvs/public/pw_kvs/flash_memory.h
new file mode 100644
index 0000000..707f128
--- /dev/null
+++ b/pw_kvs/public/pw_kvs/flash_memory.h
@@ -0,0 +1,377 @@
+// 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 <algorithm>
+
+#include "pw_kvs/assert.h"
+#include "pw_kvs/logging.h"
+#include "pw_kvs/peripherals/partition_table_entry.h"
+#include "pw_kvs/status.h"
+#include "pw_kvs/status_macros.h"
+
+namespace pw {
+class FlashMemory {
+ public:
+ // The flash address is in the range of: 0 to FlashSize.
+ typedef uint32_t Address;
+ constexpr FlashMemory(uint32_t sector_size,
+ uint32_t sector_count,
+ uint8_t alignment,
+ uint32_t start_address = 0,
+ uint32_t sector_start = 0,
+ uint8_t erased_memory_content = 0xFF)
+ : sector_size_(sector_size),
+ flash_sector_count_(sector_count),
+ alignment_(alignment),
+ start_address_(start_address),
+ sector_start_(sector_start),
+ erased_memory_content_(erased_memory_content) {}
+
+ virtual Status Enable() = 0;
+ virtual Status Disable() = 0;
+ virtual bool IsEnabled() const = 0;
+ virtual Status SelfTest() { return Status::UNIMPLEMENTED; }
+
+ // Erase num_sectors starting at a given address. Blocking call.
+ // Address should be on a sector boundary.
+ // Returns: OK, on success.
+ // TIMEOUT, on timeout.
+ // INVALID_ARGUMENT, if address or sector count is invalid.
+ // UNKNOWN, on HAL error
+ virtual Status Erase(Address flash_address, uint32_t num_sectors) = 0;
+
+ // Reads bytes from flash into buffer. Blocking call.
+ // Returns: OK, on success.
+ // TIMEOUT, on timeout.
+ // INVALID_ARGUMENT, if address or length is invalid.
+ // UNKNOWN, on HAL error
+ virtual Status Read(uint8_t* destination_ram_address,
+ Address source_flash_address,
+ uint32_t len) = 0;
+
+ // Writes bytes to flash. Blocking call.
+ // Returns: OK, on success.
+ // TIMEOUT, on timeout.
+ // INVALID_ARGUMENT, if address or length is invalid.
+ // UNKNOWN, on HAL error
+ virtual Status Write(Address destination_flash_address,
+ const uint8_t* source_ram_address,
+ uint32_t len) = 0;
+
+ // Convert an Address to an MCU pointer, this can be used for memory
+ // mapped reads. Return NULL if the memory is not memory mapped.
+ virtual uint8_t* FlashAddressToMcuAddress(Address address) const {
+ return nullptr;
+ }
+
+ // GetStartSector() is useful for FlashMemory instances where the
+ // sector start is not 0. (ex.: cases where there are portions of flash
+ // that should be handled independently).
+ constexpr uint32_t GetStartSector() const { return sector_start_; }
+ constexpr uint32_t GetSectorSizeBytes() const { return sector_size_; }
+ constexpr uint32_t GetSectorCount() const { return flash_sector_count_; }
+ constexpr uint8_t GetAlignmentBytes() const { return alignment_; }
+ constexpr uint32_t GetSizeBytes() const {
+ return sector_size_ * flash_sector_count_;
+ }
+ // Address of the start of flash (the address of sector 0)
+ constexpr uint32_t GetStartAddress() const { return start_address_; }
+ constexpr uint8_t GetErasedMemoryContent() const {
+ return erased_memory_content_;
+ }
+
+ private:
+ const uint32_t sector_size_;
+ const uint32_t flash_sector_count_;
+ const uint8_t alignment_;
+ const uint32_t start_address_;
+ const uint32_t sector_start_;
+ const uint8_t erased_memory_content_;
+};
+
+// Exposes a sub-sector sized region of flash memory that cannot be erased.
+// It can be thought of as one pseudo-sector that is sized exactly as provided.
+//
+// TODO(b/117553777): This makes a little more sense as a SubSectorPartition,
+// but PartitionTableEntry currently assumes all partitions fill entire sectors.
+// Revisit when PartitionTable is refactored.
+class FlashMemorySubSector : public FlashMemory {
+ public:
+ constexpr FlashMemorySubSector(FlashMemory* flash,
+ uint32_t start_address,
+ uint32_t size)
+ : FlashMemory(size,
+ 1, // Round up to "1" sector.
+ flash->GetAlignmentBytes(),
+ start_address,
+ // Calculate the sector for this start address.
+ flash->GetStartSector() +
+ ((start_address - flash->GetStartAddress()) /
+ flash->GetSectorSizeBytes())),
+ flash_(*CHECK_NOTNULL(flash)),
+ base_offset_(start_address - flash->GetStartAddress()) {
+ // Make sure we're not specifying a region of flash larger than
+ // that which the underlying FlashMemory supports.
+ CHECK(start_address >= flash->GetStartAddress());
+ CHECK(size <= flash->GetSectorSizeBytes());
+ CHECK(start_address + size <=
+ flash->GetStartAddress() + flash->GetSizeBytes());
+ CHECK_EQ(0, start_address % flash->GetAlignmentBytes());
+ CHECK_EQ(0, size % flash->GetAlignmentBytes());
+ }
+
+ Status Enable() override { return flash_.Enable(); }
+ Status Disable() override { return flash_.Disable(); }
+ bool IsEnabled() const override { return flash_.IsEnabled(); }
+ Status SelfTest() override { return flash_.SelfTest(); }
+
+ Status Erase(Address flash_address, uint32_t num_sectors) override {
+ return Status::UNIMPLEMENTED;
+ }
+
+ Status Read(uint8_t* destination_ram_address,
+ Address source_flash_address,
+ uint32_t len) override {
+ return flash_.Read(destination_ram_address, source_flash_address, len);
+ }
+
+ Status Write(Address destination_flash_address,
+ const uint8_t* source_ram_address,
+ uint32_t len) override {
+ return flash_.Write(destination_flash_address, source_ram_address, len);
+ }
+
+ uint8_t* FlashAddressToMcuAddress(Address address) const override {
+ return flash_.FlashAddressToMcuAddress(base_offset_ + address);
+ }
+
+ private:
+ FlashMemory& flash_;
+ // Value to add to addresses to get to the underlying flash_ address.
+ const Address base_offset_;
+};
+
+class FlashPartition {
+ public:
+ // The flash address is in the range of: 0 to PartitionSize.
+ typedef uint32_t Address;
+
+ constexpr FlashPartition(
+ FlashMemory* flash,
+ uint32_t start_sector_index,
+ uint32_t sector_count,
+ PartitionPermission permission = PartitionPermission::kReadAndWrite)
+ : flash_(*flash),
+ start_sector_index_(start_sector_index),
+ sector_count_(sector_count),
+ permission_(permission) {}
+
+ constexpr FlashPartition(FlashMemory* flash, PartitionTableEntry entry)
+ : flash_(*flash),
+ start_sector_index_(entry.partition_start_sector_index),
+ sector_count_(entry.partition_end_sector_index -
+ entry.partition_start_sector_index + 1),
+ permission_(entry.partition_permission) {}
+
+ // Erase num_sectors starting at a given address. Blocking call.
+ // Address should be on a sector boundary.
+ // Returns: OK, on success.
+ // TIMEOUT, on timeout.
+ // INVALID_ARGUMENT, if address or sector count is invalid.
+ // PERMISSION_DENIED, if partition is read only.
+ // UNKNOWN, on HAL error
+ virtual Status Erase(Address address, uint32_t num_sectors) {
+ RETURN_STATUS_IF(permission_ == PartitionPermission::kReadOnly,
+ Status::PERMISSION_DENIED);
+ RETURN_IF_ERROR(CheckBounds(address, num_sectors * GetSectorSizeBytes()));
+ return flash_.Erase(PartitionToFlashAddress(address), num_sectors);
+ }
+
+ // Reads bytes from flash into buffer. Blocking call.
+ // Returns: OK, on success.
+ // TIMEOUT, on timeout.
+ // INVALID_ARGUMENT, if address or length is invalid.
+ // UNKNOWN, on HAL error
+ virtual Status Read(uint8_t* destination_ram_address,
+ Address source_flash_address,
+ uint32_t len) {
+ RETURN_IF_ERROR(CheckBounds(source_flash_address, len));
+ return flash_.Read(destination_ram_address,
+ PartitionToFlashAddress(source_flash_address),
+ len);
+ }
+
+ // Writes bytes to flash. Blocking call.
+ // Returns: OK, on success.
+ // TIMEOUT, on timeout.
+ // INVALID_ARGUMENT, if address or length is invalid.
+ // PERMISSION_DENIED, if partition is read only.
+ // UNKNOWN, on HAL error
+ virtual Status Write(Address destination_flash_address,
+ const uint8_t* source_ram_address,
+ uint32_t len) {
+ RETURN_STATUS_IF(permission_ == PartitionPermission::kReadOnly,
+ Status::PERMISSION_DENIED);
+ RETURN_IF_ERROR(CheckBounds(destination_flash_address, len));
+ return flash_.Write(PartitionToFlashAddress(destination_flash_address),
+ source_ram_address,
+ len);
+ }
+
+ // Check to see if chunk of flash memory is erased. Address and len need to
+ // be aligned with FlashMemory.
+ // Returns: OK, on success.
+ // TIMEOUT, on timeout.
+ // INVALID_ARGUMENT, if address or length is invalid.
+ // UNKNOWN, on HAL error
+ virtual Status IsChunkErased(Address source_flash_address,
+ uint32_t len,
+ bool* is_erased) {
+ // Max alignment is artifical to keep the stack usage low for this
+ // function. Using 16 because it's the alignment of encrypted flash.
+ const uint8_t kMaxAlignment = 16;
+ // Relying on Read() to check address and len arguments.
+ RETURN_STATUS_IF(!is_erased, Status::INVALID_ARGUMENT);
+ uint8_t alignment = GetAlignmentBytes();
+ RETURN_STATUS_IF(alignment > kMaxAlignment, Status::INVALID_ARGUMENT);
+ RETURN_STATUS_IF(kMaxAlignment % alignment, Status::INVALID_ARGUMENT);
+ RETURN_STATUS_IF(len % alignment, Status::INVALID_ARGUMENT);
+
+ uint8_t buffer[kMaxAlignment];
+ uint8_t erased_pattern_buffer[kMaxAlignment];
+ size_t offset = 0;
+ memset(erased_pattern_buffer,
+ flash_.GetErasedMemoryContent(),
+ sizeof(erased_pattern_buffer));
+ *is_erased = false;
+ while (len > 0) {
+ // Check earlier that len is aligned, no need to round up
+ uint16_t read_size = std::min(static_cast<uint32_t>(sizeof(buffer)), len);
+ RETURN_IF_ERROR(Read(buffer, source_flash_address + offset, read_size));
+ if (memcmp(buffer, erased_pattern_buffer, read_size)) {
+ // Detected memory chunk is not entirely erased
+ return Status::OK;
+ }
+ offset += read_size;
+ len -= read_size;
+ }
+ *is_erased = true;
+ return Status::OK;
+ }
+
+ constexpr uint32_t GetSectorSizeBytes() const {
+ return flash_.GetSectorSizeBytes();
+ }
+
+ uint32_t GetSizeBytes() const {
+ return GetSectorCount() * GetSectorSizeBytes();
+ }
+
+ virtual uint8_t GetAlignmentBytes() const {
+ return flash_.GetAlignmentBytes();
+ }
+
+ virtual uint32_t GetSectorCount() const { return sector_count_; }
+
+ // Convert a FlashMemory::Address to an MCU pointer, this can be used for
+ // memory mapped reads. Return NULL if the memory is not memory mapped.
+ uint8_t* PartitionAddressToMcuAddress(Address address) const {
+ return flash_.FlashAddressToMcuAddress(PartitionToFlashAddress(address));
+ }
+
+ FlashMemory::Address PartitionToFlashAddress(Address address) const {
+ return flash_.GetStartAddress() +
+ (start_sector_index_ - flash_.GetStartSector()) *
+ GetSectorSizeBytes() +
+ address;
+ }
+
+ protected:
+ Status CheckBounds(Address address, uint32_t len) const {
+ if (address + len > GetSizeBytes()) {
+ LOG(ERROR) << "Attempted out-of-bound flash memory access (address:"
+ << address << " length:" << len << ")";
+ return Status::INVALID_ARGUMENT;
+ }
+ return Status::OK;
+ }
+
+ private:
+ FlashMemory& flash_;
+ const uint32_t start_sector_index_;
+ const uint32_t sector_count_;
+ const PartitionPermission permission_;
+};
+
+// FlashSubPartition defines a new partition which maps itself as a smaller
+// piece of another partition. This can used when a partition has special
+// behaviours (for example encrypted flash).
+// For example, this will be the first sector of test_partition:
+// FlashSubPartition test_partition_sector1(&test_partition, 0, 1);
+class FlashSubPartition : public FlashPartition {
+ public:
+ constexpr FlashSubPartition(FlashPartition* parent_partition,
+ uint32_t start_sector_index,
+ uint32_t sector_count)
+ : FlashPartition(*parent_partition),
+ partition_(parent_partition),
+ start_sector_index_(start_sector_index),
+ sector_count_(sector_count) {}
+
+ Status Erase(Address address, uint32_t num_sectors) override {
+ RETURN_IF_ERROR(CheckBounds(address, num_sectors * GetSectorSizeBytes()));
+ return partition_->Erase(ParentAddress(address), num_sectors);
+ }
+
+ Status Read(uint8_t* destination_ram_address,
+ Address source_flash_address,
+ uint32_t len) override {
+ RETURN_IF_ERROR(CheckBounds(source_flash_address, len));
+ return partition_->Read(
+ destination_ram_address, ParentAddress(source_flash_address), len);
+ }
+
+ Status Write(Address destination_flash_address,
+ const uint8_t* source_ram_address,
+ uint32_t len) override {
+ RETURN_IF_ERROR(CheckBounds(destination_flash_address, len));
+ return partition_->Write(
+ ParentAddress(destination_flash_address), source_ram_address, len);
+ }
+
+ Status IsChunkErased(Address source_flash_address,
+ uint32_t len,
+ bool* is_erased) override {
+ RETURN_IF_ERROR(CheckBounds(source_flash_address, len));
+ return partition_->IsChunkErased(
+ ParentAddress(source_flash_address), len, is_erased);
+ }
+
+ uint8_t GetAlignmentBytes() const override {
+ return partition_->GetAlignmentBytes();
+ }
+
+ uint32_t GetSectorCount() const override { return sector_count_; }
+
+ private:
+ Address ParentAddress(Address address) const {
+ return address + start_sector_index_ * partition_->GetSectorSizeBytes();
+ }
+ FlashPartition* partition_;
+ const uint32_t start_sector_index_;
+ const uint32_t sector_count_;
+};
+
+} // namespace pw
diff --git a/pw_kvs/public/pw_kvs/in_memory_fake_flash.h b/pw_kvs/public/pw_kvs/in_memory_fake_flash.h
new file mode 100644
index 0000000..a67c770
--- /dev/null
+++ b/pw_kvs/public/pw_kvs/in_memory_fake_flash.h
@@ -0,0 +1,92 @@
+// 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 <array>
+
+#include "pw_kvs/devices/flash_memory.h"
+#include "pw_kvs/status.h"
+#include "pw_kvs/status_macros.h"
+
+namespace pw {
+
+// This creates a buffer which mimics the behaviour of flash (requires erase,
+// before write, checks alignments, and is addressed in sectors).
+template <uint32_t kSectorSize, uint16_t kSectorCount>
+class InMemoryFakeFlash : public FlashMemory {
+ public:
+ InMemoryFakeFlash(uint8_t alignment = 1) // default 8 bit alignment
+ : FlashMemory(kSectorSize, kSectorCount, alignment) {}
+
+ // Always enabled
+ Status Enable() override { return Status::OK; }
+ Status Disable() override { return Status::OK; }
+ bool IsEnabled() const override { return true; }
+
+ // Erase num_sectors starting at a given address. Blocking call.
+ // Address should be on a sector boundary.
+ // Returns: OK, on success.
+ // INVALID_ARGUMENT, if address or sector count is invalid.
+ // UNKNOWN, on HAL error
+ Status Erase(Address addr, uint32_t num_sectors) override {
+ RETURN_STATUS_IF(addr % GetSectorSizeBytes() != 0,
+ Status::INVALID_ARGUMENT);
+ RETURN_STATUS_IF(
+ addr / GetSectorSizeBytes() + num_sectors > GetSectorCount(),
+ Status::UNKNOWN);
+ RETURN_STATUS_IF(addr % GetAlignmentBytes() != 0, Status::INVALID_ARGUMENT);
+ memset(&buffer_[addr], 0xFF, GetSectorSizeBytes() * num_sectors);
+ return Status::OK;
+ }
+
+ // Reads bytes from flash into buffer. Blocking call.
+ // Returns: OK, on success.
+ // INVALID_ARGUMENT, if address or length is invalid.
+ // UNKNOWN, on HAL error
+ Status Read(uint8_t* dest_ram_addr,
+ Address source_flash_addr,
+ uint32_t len) override {
+ RETURN_STATUS_IF(
+ (source_flash_addr + len) >= GetSectorCount() * GetSizeBytes(),
+ Status::INVALID_ARGUMENT);
+ memcpy(dest_ram_addr, &buffer_[source_flash_addr], len);
+ return Status::OK;
+ }
+
+ // Writes bytes to flash. Blocking call.
+ // Returns: OK, on success.
+ // INVALID_ARGUMENT, if address or length is invalid.
+ // UNKNOWN, on HAL error
+ Status Write(Address dest_flash_addr,
+ const uint8_t* source_ram_addr,
+ uint32_t len) override {
+ RETURN_STATUS_IF(
+ (dest_flash_addr + len) >= GetSectorCount() * GetSizeBytes(),
+ Status::INVALID_ARGUMENT);
+ RETURN_STATUS_IF(dest_flash_addr % GetAlignmentBytes() != 0,
+ Status::INVALID_ARGUMENT);
+ RETURN_STATUS_IF(len % GetAlignmentBytes() != 0, Status::INVALID_ARGUMENT);
+ // Check in erased state
+ for (unsigned i = 0; i < len; i++) {
+ RETURN_STATUS_IF(buffer_[dest_flash_addr + i] != 0xFF, Status::UNKNOWN);
+ }
+ memcpy(&buffer_[dest_flash_addr], source_ram_addr, len);
+ return Status::OK;
+ }
+
+ private:
+ std::array<uint8_t, kSectorCount * kSectorSize> buffer_;
+};
+
+} // namespace pw
diff --git a/pw_kvs/public/pw_kvs/key_value_store.h b/pw_kvs/public/pw_kvs/key_value_store.h
new file mode 100644
index 0000000..dca6507
--- /dev/null
+++ b/pw_kvs/public/pw_kvs/key_value_store.h
@@ -0,0 +1,366 @@
+// 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 <algorithm>
+#include <type_traits>
+
+#include "pw_kvs/devices/flash_memory.h"
+#include "pw_kvs/logging.h"
+#include "pw_kvs/os/mutex.h"
+#include "pw_kvs/status.h"
+
+namespace pw {
+
+// This object is very large and should not be placed on the stack.
+class KeyValueStore {
+ public:
+ KeyValueStore(FlashPartition* partition) : partition_(*partition) {}
+
+ // 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;
+ }
+ os::MutexLock lock(&lock_);
+ 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 char* 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 char* key, void* value, uint16_t size, 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 char* 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");
+ uint16_t value_size = 0;
+ RETURN_IF_ERROR(GetValueSize(key, &value_size));
+ RETURN_STATUS_IF(value_size != sizeof(T), Status::INVALID_ARGUMENT);
+ return Get(key, value, sizeof(T));
+ }
+
+ // Writes the key value store to the partition. If the key already exists it
+ // will be deleted before writing the new value.
+ // key is a null terminated c string.
+ // Returns OK if successful
+ // RESOURCE_EXHAUSTED if there is not enough available space
+ Status Put(const char* key, const void* value, uint16_t size);
+
+ // Writes the value to the partition. If the key already exists it will be
+ // deleted before writing the new value.
+ // key is a null terminated c string.
+ // Returns OK if successful
+ // RESOURCE_EXHAUSTED if there is not enough available space
+ template <typename T>
+ Status Put(const char* key, const T& value) {
+ static_assert(std::is_trivially_copyable<T>(), "KVS values must copyable");
+ static_assert(!std::is_pointer<T>(), "KVS values cannot be pointers");
+ return Put(key, &value, sizeof(T));
+ }
+
+ // 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.
+ Status GetValueSize(const char* key, uint16_t* value_size);
+
+ // 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() {
+ os::MutexLock lock(&lock_);
+ return CleanAllInternal();
+ }
+ size_t PendingCleanCount() {
+ os::MutexLock lock(&lock_);
+ 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.
+ uint8_t KeyCount() const;
+ const char* GetKey(uint8_t idx) const;
+ uint16_t GetValueSize(uint8_t idx) const;
+ size_t GetMaxKeys() const { return kListCapacityMax; }
+ bool HasEmptySector() const { return HaveEmptySectorImpl(); }
+
+ static constexpr size_t kHeaderSize = 8; // Sector and KVS Header size
+ static constexpr uint16_t MaxValueLength() { return kChunkValueLengthMax; }
+ KeyValueStore(const KeyValueStore&) = delete;
+ KeyValueStore& operator=(const KeyValueStore&) = delete;
+
+ private:
+ using KeyIndex = uint8_t;
+ using SectorIndex = uint32_t;
+
+ static constexpr uint16_t kVersion = 4;
+ 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;
+
+ // TODO: Use BitPacker if/when have more flags.
+ 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.
+ } __attribute__((__packed__));
+ 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;
+ } __attribute__((__packed__));
+ static_assert(sizeof(KvsHeader) == kHeaderSize, "Invalid KvsHeader size");
+
+ struct KeyMap {
+ char key[kChunkKeyLengthMax + 1]; // + 1 for null terminator
+ FlashPartition::Address address;
+ uint16_t chunk_len;
+ bool is_erased;
+ };
+
+ // 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(uint16_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 uint8_t* value,
+ uint16_t size,
+ bool is_erased = false);
+
+ bool ValueMatches(KeyIndex key_index,
+ const uint8_t* value,
+ uint16_t size,
+ bool is_erased);
+
+ // ResetSector erases the sector and writes the sector header.
+ Status ResetSector(SectorIndex sector_index);
+ Status WriteKeyValue(FlashPartition::Address address,
+ const char* key,
+ const uint8_t* value,
+ uint16_t size,
+ bool is_erased = false);
+ uint32_t SectorSpaceRemaining(SectorIndex sector_index) const;
+
+ // Returns idx if key is found, otherwise kListCapacityMax.
+ KeyIndex FindKeyInMap(const char* key) const;
+ bool IsKeyInMap(const char* key) const {
+ return FindKeyInMap(key) != kListCapacityMax;
+ }
+
+ void RemoveFromMap(KeyIndex key_index);
+ Status AppendToMap(const char* key,
+ FlashPartition::Address address,
+ uint16_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 char* key,
+ uint16_t key_size,
+ const uint8_t* value,
+ uint16_t value_size) const;
+
+ // Calculates the CRC by reading the value from flash in chunks.
+ Status CalculateCrcFromValueAddress(const char* key,
+ uint16_t key_size,
+ 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.
+ uint16_t ChunkSize(uint16_t key_len, uint16_t chunk_len) const {
+ return RoundUpForAlignment(sizeof(KvsHeader)) +
+ RoundUpForAlignment(key_len) + RoundUpForAlignment(chunk_len);
+ }
+
+ // 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 HaveEmptySectorImpl(SectorIndex skip_sector = kSectorInvalid) 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() &&
+ !HaveEmptySectorImpl(AddressToSectorIndex(address));
+ }
+
+ FlashPartition& partition_;
+ os::Mutex lock_;
+ 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] = {{{0}}};
+ KeyIndex map_size_ = 0;
+
+ // +1 for nul-terminator since keys are stored as Length + Value and no nul
+ // termination but we are using them as nul-terminated strings through
+ // loading-up and comparing the keys.
+ char temp_key_buffer_[kChunkKeyLengthMax + 1u] = {0};
+ uint8_t temp_buffer_[cfg::kKvsBufferSize] = {0};
+};
+
+} // namespace pw
diff --git a/pw_kvs/public/pw_kvs/partition_table_entry.h b/pw_kvs/public/pw_kvs/partition_table_entry.h
new file mode 100644
index 0000000..9aa111d
--- /dev/null
+++ b/pw_kvs/public/pw_kvs/partition_table_entry.h
@@ -0,0 +1,51 @@
+// 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 <cstdint>
+
+namespace pw {
+
+namespace partition_table {
+
+enum DataType { kRawData, kKvs };
+
+} // namespace partition_table
+
+enum class PartitionPermission : bool {
+ kReadOnly = true,
+ kReadAndWrite = false,
+};
+
+struct PartitionTableEntry {
+ constexpr PartitionTableEntry(
+ const uint32_t start_sector_index,
+ const uint32_t end_sector_index,
+ const uint8_t partition_identifier,
+ partition_table::DataType datatype,
+ PartitionPermission permission = PartitionPermission::kReadAndWrite)
+ : partition_start_sector_index(start_sector_index),
+ partition_end_sector_index(end_sector_index),
+ partition_id(partition_identifier),
+ data_type(datatype),
+ partition_permission(permission) {}
+
+ const uint32_t partition_start_sector_index;
+ const uint32_t partition_end_sector_index;
+ const uint8_t partition_id;
+ const partition_table::DataType data_type;
+ const PartitionPermission partition_permission;
+};
+
+} // namespace pw