| // 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. |
| |
| #define PW_LOG_MODULE_NAME "KVS" |
| |
| #include "pw_kvs/internal/entry.h" |
| |
| #include <cinttypes> |
| #include <cstring> |
| |
| #include "pw_kvs_private/config.h" |
| #include "pw_kvs_private/macros.h" |
| #include "pw_log/log.h" |
| |
| namespace pw::kvs::internal { |
| |
| static_assert( |
| kMaxFlashAlignment >= Entry::kMinAlignmentBytes, |
| "Flash alignment is required to be at least Entry::kMinAlignmentBytes"); |
| |
| constexpr size_t kWriteBufferSize = |
| std::max(kMaxFlashAlignment, 4 * Entry::kMinAlignmentBytes); |
| |
| using std::byte; |
| using std::string_view; |
| |
| Status Entry::Read(FlashPartition& partition, |
| Address address, |
| const internal::EntryFormats& formats, |
| Entry* entry) { |
| EntryHeader header; |
| TRY(partition.Read(address, sizeof(header), &header)); |
| |
| if (partition.AppearsErased(std::as_bytes(std::span(&header.magic, 1)))) { |
| return Status::NOT_FOUND; |
| } |
| if (header.key_length_bytes > kMaxKeyLength) { |
| return Status::DATA_LOSS; |
| } |
| |
| const EntryFormat* format = formats.Find(header.magic); |
| if (format == nullptr) { |
| PW_LOG_ERROR("Found corrupt magic: %" PRIx32 " at address %u", |
| header.magic, |
| unsigned(address)); |
| return Status::DATA_LOSS; |
| } |
| |
| *entry = Entry(&partition, address, *format, header); |
| return Status::OK; |
| } |
| |
| Status Entry::ReadKey(FlashPartition& partition, |
| Address address, |
| size_t key_length, |
| char* key) { |
| if (key_length == 0u || key_length > kMaxKeyLength) { |
| return Status::DATA_LOSS; |
| } |
| |
| return partition.Read(address + sizeof(EntryHeader), key_length, key) |
| .status(); |
| } |
| |
| Entry::Entry(FlashPartition& partition, |
| Address address, |
| const EntryFormat& format, |
| string_view key, |
| std::span<const byte> value, |
| uint16_t value_size_bytes, |
| uint32_t transaction_id) |
| : Entry(&partition, |
| address, |
| format, |
| {.magic = format.magic, |
| .checksum = 0, |
| .alignment_units = |
| alignment_bytes_to_units(partition.alignment_bytes()), |
| .key_length_bytes = static_cast<uint8_t>(key.size()), |
| .value_size_bytes = value_size_bytes, |
| .transaction_id = transaction_id}) { |
| if (checksum_algo_ != nullptr) { |
| std::span<const byte> checksum = CalculateChecksum(key, value); |
| std::memcpy(&header_.checksum, |
| checksum.data(), |
| std::min(checksum.size(), sizeof(header_.checksum))); |
| } |
| } |
| |
| StatusWithSize Entry::Write(string_view key, |
| std::span<const byte> value) const { |
| FlashPartition::Output flash(partition(), address_); |
| return AlignedWrite<kWriteBufferSize>(flash, |
| alignment_bytes(), |
| {std::as_bytes(std::span(&header_, 1)), |
| std::as_bytes(std::span(key)), |
| value}); |
| } |
| |
| Status Entry::Update(const EntryFormat& new_format, |
| uint32_t new_transaction_id) { |
| checksum_algo_ = new_format.checksum; |
| header_.magic = new_format.magic; |
| header_.alignment_units = |
| alignment_bytes_to_units(partition_->alignment_bytes()); |
| header_.transaction_id = new_transaction_id; |
| |
| // If we could write the header last, we could avoid reading the entry twice |
| // when moving an entry. However, to support alignments greater than the |
| // header size, we first read the entire value to calculate the new checksum, |
| // then write the full entry in WriteFrom. |
| return CalculateChecksumFromFlash(); |
| } |
| |
| StatusWithSize Entry::Copy(Address new_address) const { |
| PW_LOG_DEBUG("Copying entry from %u to %u as ID %" PRIu32, |
| unsigned(address()), |
| unsigned(new_address), |
| transaction_id()); |
| |
| FlashPartition::Output output(partition(), new_address); |
| AlignedWriterBuffer<kWriteBufferSize> writer(alignment_bytes(), output); |
| |
| // Use this object's header rather than the header in flash of flash, since |
| // this Entry may have been updated. |
| TRY_WITH_SIZE(writer.Write(&header_, sizeof(header_))); |
| |
| // Write only the key and value from the original entry. |
| FlashPartition::Input input(partition(), address() + sizeof(EntryHeader)); |
| TRY_WITH_SIZE(writer.Write(input, key_length() + value_size())); |
| return writer.Flush(); |
| } |
| |
| StatusWithSize Entry::ReadValue(std::span<byte> buffer, |
| size_t offset_bytes) const { |
| if (offset_bytes > value_size()) { |
| return StatusWithSize::OUT_OF_RANGE; |
| } |
| |
| const size_t remaining_bytes = value_size() - offset_bytes; |
| const size_t read_size = std::min(buffer.size(), remaining_bytes); |
| |
| StatusWithSize result = partition().Read( |
| address_ + sizeof(EntryHeader) + key_length() + offset_bytes, |
| buffer.subspan(0, read_size)); |
| TRY_WITH_SIZE(result); |
| |
| if (read_size != remaining_bytes) { |
| return StatusWithSize(Status::RESOURCE_EXHAUSTED, read_size); |
| } |
| return StatusWithSize(read_size); |
| } |
| |
| Status Entry::ValueMatches(std::span<const std::byte> value) const { |
| if (value_size() != value.size_bytes()) { |
| return Status::NOT_FOUND; |
| } |
| |
| Address address = address_ + sizeof(EntryHeader) + key_length(); |
| Address end = address + value_size(); |
| const std::byte* value_ptr = value.data(); |
| |
| std::array<std::byte, 2 * kMinAlignmentBytes> buffer; |
| while (address < end) { |
| const size_t read_size = std::min(size_t(end - address), buffer.size()); |
| TRY(partition_->Read(address, std::span(buffer).first(read_size))); |
| |
| if (std::memcmp(buffer.data(), value_ptr, read_size) != 0) { |
| return Status::NOT_FOUND; |
| } |
| |
| address += read_size; |
| value_ptr += read_size; |
| } |
| |
| return Status::OK; |
| } |
| |
| Status Entry::VerifyChecksum(string_view key, |
| std::span<const byte> value) const { |
| if (checksum_algo_ == nullptr) { |
| return header_.checksum == 0 ? Status::OK : Status::DATA_LOSS; |
| } |
| CalculateChecksum(key, value); |
| return checksum_algo_->Verify(checksum_bytes()); |
| } |
| |
| Status Entry::VerifyChecksumInFlash() const { |
| // Read the entire entry piece-by-piece into a small buffer. If the entry is |
| // 32 B or less, only one read is required. |
| union { |
| EntryHeader header_to_verify; |
| byte buffer[sizeof(EntryHeader) * 2]; |
| }; |
| |
| size_t bytes_to_read = size(); |
| size_t read_size = std::min(sizeof(buffer), bytes_to_read); |
| |
| Address read_address = address_; |
| |
| // Read the first chunk, which includes the header, and compare the checksum. |
| TRY(partition().Read(read_address, read_size, buffer)); |
| |
| if (header_to_verify.checksum != header_.checksum) { |
| PW_LOG_ERROR("Expected checksum 0x%08" PRIx32 ", found 0x%08" PRIx32, |
| header_.checksum, |
| header_to_verify.checksum); |
| return Status::DATA_LOSS; |
| } |
| |
| if (checksum_algo_ == nullptr) { |
| return header_.checksum == 0 ? Status::OK : Status::DATA_LOSS; |
| } |
| |
| // The checksum is calculated as if the header's checksum field were 0. |
| header_to_verify.checksum = 0; |
| |
| checksum_algo_->Reset(); |
| |
| while (true) { |
| // Add the chunk in the buffer to the checksum. |
| checksum_algo_->Update(buffer, read_size); |
| |
| bytes_to_read -= read_size; |
| if (bytes_to_read == 0u) { |
| break; |
| } |
| |
| // Read the next chunk into the buffer. |
| read_address += read_size; |
| read_size = std::min(sizeof(buffer), bytes_to_read); |
| TRY(partition().Read(read_address, read_size, buffer)); |
| } |
| |
| checksum_algo_->Finish(); |
| return checksum_algo_->Verify(checksum_bytes()); |
| } |
| |
| void Entry::DebugLog() const { |
| PW_LOG_DEBUG("Entry [%s]: ", deleted() ? "tombstone" : "present"); |
| PW_LOG_DEBUG(" Address = 0x%x", unsigned(address_)); |
| PW_LOG_DEBUG(" Transaction = %u", unsigned(transaction_id())); |
| PW_LOG_DEBUG(" Magic = 0x%x", unsigned(magic())); |
| PW_LOG_DEBUG(" Checksum = 0x%x", unsigned(header_.checksum)); |
| PW_LOG_DEBUG(" Key length = 0x%x", unsigned(key_length())); |
| PW_LOG_DEBUG(" Value length = 0x%x", unsigned(value_size())); |
| PW_LOG_DEBUG(" Entry size = 0x%x", unsigned(size())); |
| PW_LOG_DEBUG(" Alignment = 0x%x", unsigned(alignment_bytes())); |
| } |
| |
| std::span<const byte> Entry::CalculateChecksum( |
| const string_view key, std::span<const byte> value) const { |
| checksum_algo_->Reset(); |
| |
| { |
| EntryHeader header_for_checksum = header_; |
| header_for_checksum.checksum = 0; |
| |
| checksum_algo_->Update(&header_for_checksum, sizeof(header_for_checksum)); |
| checksum_algo_->Update(std::as_bytes(std::span(key))); |
| checksum_algo_->Update(value); |
| } |
| |
| AddPaddingBytesToChecksum(); |
| |
| return checksum_algo_->Finish(); |
| } |
| |
| Status Entry::CalculateChecksumFromFlash() { |
| header_.checksum = 0; |
| |
| if (checksum_algo_ == nullptr) { |
| return Status::OK; |
| } |
| |
| checksum_algo_->Reset(); |
| checksum_algo_->Update(&header_, sizeof(header_)); |
| |
| Address address = address_ + sizeof(EntryHeader); |
| // To handle alignment changes, do not read the padding. The padding is added |
| // after checksumming the key and value from flash. |
| const Address end = address_ + content_size(); |
| |
| std::array<std::byte, 2 * kMinAlignmentBytes> buffer; |
| while (address < end) { |
| const size_t read_size = std::min(size_t(end - address), buffer.size()); |
| TRY(partition_->Read(address, std::span(buffer).first(read_size))); |
| |
| checksum_algo_->Update(buffer.data(), read_size); |
| address += read_size; |
| } |
| |
| AddPaddingBytesToChecksum(); |
| |
| std::span checksum = checksum_algo_->Finish(); |
| std::memcpy(&header_.checksum, |
| checksum.data(), |
| std::min(checksum.size(), sizeof(header_.checksum))); |
| return Status::OK; |
| } |
| |
| void Entry::AddPaddingBytesToChecksum() const { |
| constexpr byte padding[kMinAlignmentBytes - 1] = {}; |
| size_t padding_to_add = Padding(content_size(), alignment_bytes()); |
| |
| while (padding_to_add != 0u) { |
| const size_t chunk_size = std::min(padding_to_add, sizeof(padding)); |
| checksum_algo_->Update(padding, chunk_size); |
| padding_to_add -= chunk_size; |
| } |
| } |
| |
| } // namespace pw::kvs::internal |