pw_kvs: Expand Entry tests; fix issues
- Expand the tests for the KVS entry class.
- Fix a few minor issues found by the tests.
Change-Id: I92b5dd614d2a726c912b7b1bf1f9ebd47a702be4
diff --git a/pw_kvs/BUILD.gn b/pw_kvs/BUILD.gn
index c2552c8..205496c 100644
--- a/pw_kvs/BUILD.gn
+++ b/pw_kvs/BUILD.gn
@@ -50,6 +50,7 @@
friend = [
":entry_test",
":key_value_store_test",
+ ":key_value_store_map_test",
]
}
diff --git a/pw_kvs/debug_cli.cc b/pw_kvs/debug_cli.cc
index 8f5baeb..58eeac8 100644
--- a/pw_kvs/debug_cli.cc
+++ b/pw_kvs/debug_cli.cc
@@ -68,11 +68,10 @@
char value[64] = {};
if (StatusWithSize result = entry.Get(as_writable_bytes(span(value)));
result.ok()) {
- printf("%2d: %s='%s'\n", ++i, entry.key().data(), value);
+ printf("%2d: %s='%s'\n", ++i, entry.key(), value);
} else {
- printf("FAILED to Get key %s: %s\n",
- entry.key().data(),
- result.status().str());
+ printf(
+ "FAILED to Get key %s: %s\n", entry.key(), result.status().str());
}
}
printf("---------------------------------------------- END CONTENTS\n");
diff --git a/pw_kvs/entry.cc b/pw_kvs/entry.cc
index c0cc9e6..458a12c 100644
--- a/pw_kvs/entry.cc
+++ b/pw_kvs/entry.cc
@@ -28,23 +28,28 @@
Status Entry::Read(FlashPartition& partition, Address address, Entry* entry) {
EntryHeader header;
TRY(partition.Read(address, sizeof(header), &header));
- *entry = Entry(&partition, address, header);
if (partition.AppearsErased(as_bytes(span(&header.magic, 1)))) {
return Status::NOT_FOUND;
}
+ if (header.key_length_bytes > kMaxKeyLength) {
+ return Status::DATA_LOSS;
+ }
+
+ *entry = Entry(&partition, address, header);
return Status::OK;
}
-StatusWithSize Entry::ReadKey(FlashPartition& partition,
- Address address,
- size_t key_length,
- KeyBuffer& key) {
+Status Entry::ReadKey(FlashPartition& partition,
+ Address address,
+ size_t key_length,
+ char* key) {
if (key_length == 0u || key_length > kMaxKeyLength) {
- return StatusWithSize(Status::DATA_LOSS);
+ return Status::DATA_LOSS;
}
- return partition.Read(address + sizeof(EntryHeader), key_length, key.data());
+ return partition.Read(address + sizeof(EntryHeader), key_length, key)
+ .status();
}
Entry::Entry(FlashPartition& partition,
@@ -131,7 +136,7 @@
}
if (algorithm == nullptr) {
- return Status::OK;
+ return checksum() == 0 ? Status::OK : Status::DATA_LOSS;
}
// The checksum is calculated as if the header's checksum field were 0.
diff --git a/pw_kvs/entry_test.cc b/pw_kvs/entry_test.cc
index 26c44a3..00a5bd9 100644
--- a/pw_kvs/entry_test.cc
+++ b/pw_kvs/entry_test.cc
@@ -14,45 +14,48 @@
#include "pw_kvs_private/entry.h"
+#include <string_view>
+
#include "gtest/gtest.h"
#include "pw_kvs/alignment.h"
+#include "pw_kvs/crc16_checksum.h"
#include "pw_kvs/flash_memory.h"
#include "pw_kvs/in_memory_fake_flash.h"
+#include "pw_kvs_private/byte_utils.h"
+#include "pw_span/span.h"
namespace pw::kvs {
namespace {
-// TODO(hepler): expand these tests
-
-FakeFlashBuffer<128, 4> test_flash(16);
-FlashPartition test_partition(&test_flash, 0, test_flash.sector_count());
+using std::byte;
+using std::string_view;
TEST(Entry, Size_RoundsUpToAlignment) {
+ FakeFlashBuffer<64, 2> flash(16);
+ FlashPartition partition(&flash, 0, flash.sector_count());
+
for (size_t alignment_bytes = 1; alignment_bytes <= 4096; ++alignment_bytes) {
const size_t align = AlignUp(alignment_bytes, Entry::kMinAlignmentBytes);
for (size_t value : {size_t(0), align - 1, align, align + 1, 2 * align}) {
- Entry entry = Entry::Valid(test_partition,
- 0,
- 9,
- nullptr,
- "k",
- {nullptr, value},
- alignment_bytes,
- 0);
+ Entry entry = Entry::Valid(
+ partition, 0, 9, nullptr, "k", {nullptr, value}, alignment_bytes, 0);
ASSERT_EQ(AlignUp(sizeof(EntryHeader) + 1 /* key */ + value, align),
entry.size());
}
- Entry entry = Entry::Tombstone(
- test_partition, 0, 9, nullptr, "k", alignment_bytes, 0);
+ Entry entry =
+ Entry::Tombstone(partition, 0, 9, nullptr, "k", alignment_bytes, 0);
ASSERT_EQ(AlignUp(sizeof(EntryHeader) + 1 /* key */, align), entry.size());
}
}
TEST(Entry, Construct_ValidEntry) {
+ FakeFlashBuffer<64, 2> flash(16);
+ FlashPartition partition(&flash, 0, flash.sector_count());
+
auto entry = Entry::Valid(
- test_partition, 1, 9, nullptr, "k", as_bytes(span("123")), 1, 9876);
+ partition, 1, 9, nullptr, "k", as_bytes(span("123")), 1, 9876);
EXPECT_FALSE(entry.deleted());
EXPECT_EQ(entry.magic(), 9u);
@@ -61,7 +64,10 @@
}
TEST(Entry, Construct_Tombstone) {
- auto entry = Entry::Tombstone(test_partition, 1, 99, nullptr, "key", 1, 123);
+ FakeFlashBuffer<64, 2> flash(16);
+ FlashPartition partition(&flash, 0, flash.sector_count());
+
+ auto entry = Entry::Tombstone(partition, 1, 99, nullptr, "key", 1, 123);
EXPECT_TRUE(entry.deleted());
EXPECT_EQ(entry.magic(), 99u);
@@ -69,5 +75,192 @@
EXPECT_EQ(entry.key_version(), 123u);
}
+constexpr auto kHeader1 = ByteStr(
+ "\x0d\xf0\x0d\x60" // magic
+ "\xC5\x65\x00\x00" // checksum (CRC16)
+ "\x01" // alignment
+ "\x05" // key length
+ "\x06\x00" // value size
+ "\x99\x98\x97\x96" // version
+);
+
+constexpr auto kKey1 = ByteStr("key45");
+constexpr auto kValue1 = ByteStr("VALUE!");
+constexpr auto kPadding1 = ByteStr("\0\0\0\0\0");
+
+constexpr auto kEntry1 = AsBytes(kHeader1, kKey1, kValue1, kPadding1);
+
+class ValidEntryInFlash : public ::testing::Test {
+ protected:
+ ValidEntryInFlash() : flash_(kEntry1), partition_(&flash_) {
+ EXPECT_EQ(Status::OK, Entry::Read(partition_, 0, &entry_));
+ }
+
+ FakeFlashBuffer<1024, 4> flash_;
+ FlashPartition partition_;
+ Entry entry_;
+};
+
+TEST_F(ValidEntryInFlash, PassesChecksumVerification) {
+ ChecksumCrc16 checksum;
+ EXPECT_EQ(Status::OK, entry_.VerifyChecksumInFlash(&checksum));
+ EXPECT_EQ(Status::OK, entry_.VerifyChecksum(&checksum, "key45", kValue1));
+}
+
+TEST_F(ValidEntryInFlash, HeaderContents) {
+ EXPECT_EQ(entry_.magic(), 0x600DF00Du);
+ EXPECT_EQ(entry_.key_length(), 5u);
+ EXPECT_EQ(entry_.value_size(), 6u);
+ EXPECT_EQ(entry_.key_version(), 0x96979899u);
+ EXPECT_FALSE(entry_.deleted());
+}
+
+TEST_F(ValidEntryInFlash, ReadKey) {
+ Entry::KeyBuffer key = {};
+ auto result = entry_.ReadKey(key);
+
+ ASSERT_EQ(Status::OK, result.status());
+ EXPECT_EQ(result.size(), entry_.key_length());
+ EXPECT_STREQ(key.data(), "key45");
+}
+
+TEST_F(ValidEntryInFlash, ReadValue) {
+ char value[32] = {};
+ auto result = entry_.ReadValue(as_writable_bytes(span(value)));
+
+ ASSERT_EQ(Status::OK, result.status());
+ EXPECT_EQ(result.size(), entry_.value_size());
+ EXPECT_STREQ(value, "VALUE!");
+}
+
+TEST_F(ValidEntryInFlash, ReadValue_ResourceExhaustedIfBufferFills) {
+ char value[3] = {};
+ auto result = entry_.ReadValue(as_writable_bytes(span(value)));
+
+ ASSERT_EQ(Status::RESOURCE_EXHAUSTED, result.status());
+ EXPECT_EQ(3u, result.size());
+ EXPECT_EQ(value[0], 'V');
+ EXPECT_EQ(value[1], 'A');
+ EXPECT_EQ(value[2], 'L');
+}
+
+TEST(ValidEntry, Write) {
+ FakeFlashBuffer<1024, 4> flash;
+ FlashPartition partition(&flash);
+ ChecksumCrc16 checksum;
+
+ Entry entry = Entry::Valid(
+ partition, 53, 0x600DF00Du, &checksum, "key45", kValue1, 32, 0x96979899u);
+
+ auto result = entry.Write("key45", kValue1);
+ EXPECT_EQ(Status::OK, result.status());
+ EXPECT_EQ(32u, result.size());
+ EXPECT_EQ(std::memcmp(&flash.buffer()[53], kEntry1.data(), kEntry1.size()),
+ 0);
+}
+
+constexpr auto kHeader2 = ByteStr(
+ "\x0d\xf0\x0d\x60" // magic
+ "\xd5\xf5\x00\x00" // checksum (CRC16)
+ "\x00" // alignment
+ "\x01" // key length
+ "\xff\xff" // value size
+ "\x00\x01\x02\x03" // key version
+);
+
+constexpr auto kKeyAndPadding2 = ByteStr("K\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0");
+
+class TombstoneEntryInFlash : public ::testing::Test {
+ protected:
+ TombstoneEntryInFlash()
+ : flash_(AsBytes(kHeader2, kKeyAndPadding2)), partition_(&flash_) {
+ EXPECT_EQ(Status::OK, Entry::Read(partition_, 0, &entry_));
+ }
+
+ FakeFlashBuffer<1024, 4> flash_;
+ FlashPartition partition_;
+ Entry entry_;
+};
+
+TEST_F(TombstoneEntryInFlash, PassesChecksumVerification) {
+ ChecksumCrc16 checksum;
+ EXPECT_EQ(Status::OK, entry_.VerifyChecksumInFlash(&checksum));
+ EXPECT_EQ(Status::OK, entry_.VerifyChecksum(&checksum, "K", {}));
+}
+
+TEST_F(TombstoneEntryInFlash, HeaderContents) {
+ EXPECT_EQ(entry_.magic(), 0x600DF00Du);
+ EXPECT_EQ(entry_.key_length(), 1u);
+ EXPECT_EQ(entry_.value_size(), 0u);
+ EXPECT_EQ(entry_.key_version(), 0x03020100u);
+ EXPECT_TRUE(entry_.deleted());
+}
+
+TEST_F(TombstoneEntryInFlash, ReadKey) {
+ Entry::KeyBuffer key = {};
+ auto result = entry_.ReadKey(key);
+
+ ASSERT_EQ(Status::OK, result.status());
+ EXPECT_EQ(result.size(), entry_.key_length());
+ EXPECT_STREQ(key.data(), "K");
+}
+
+TEST_F(TombstoneEntryInFlash, ReadValue) {
+ char value[32] = {};
+ auto result = entry_.ReadValue(as_writable_bytes(span(value)));
+
+ ASSERT_EQ(Status::OK, result.status());
+ EXPECT_EQ(0u, result.size());
+}
+
+TEST(TombstoneEntry, Write) {
+ FakeFlashBuffer<1024, 4> flash;
+ FlashPartition partition(&flash);
+ ChecksumCrc16 checksum;
+
+ Entry entry = Entry::Tombstone(
+ partition, 16, 0x600DF00Du, &checksum, "K", 16, 0x03020100);
+
+ auto result = entry.Write("K", {});
+ EXPECT_EQ(Status::OK, result.status());
+ EXPECT_EQ(32u, result.size());
+ EXPECT_EQ(std::memcmp(&flash.buffer()[16],
+ AsBytes(kHeader2, kKeyAndPadding2).data(),
+ kEntry1.size()),
+ 0);
+}
+
+TEST(Entry, Checksum_NoChecksumRequiresZero) {
+ FakeFlashBuffer<1024, 4> flash(kEntry1);
+ FlashPartition partition(&flash);
+ Entry entry;
+ ASSERT_EQ(Status::OK, Entry::Read(partition, 0, &entry));
+
+ EXPECT_EQ(Status::DATA_LOSS, entry.VerifyChecksumInFlash(nullptr));
+ EXPECT_EQ(Status::DATA_LOSS, entry.VerifyChecksum(nullptr, {}, {}));
+
+ std::memset(&flash.buffer()[4], 0, 4); // set the checksum field to 0
+ ASSERT_EQ(Status::OK, Entry::Read(partition, 0, &entry));
+ EXPECT_EQ(Status::OK, entry.VerifyChecksumInFlash(nullptr));
+ EXPECT_EQ(Status::OK, entry.VerifyChecksum(nullptr, {}, {}));
+}
+
+TEST(Entry, Checksum_ChecksPadding) {
+ FakeFlashBuffer<1024, 4> flash(
+ AsBytes(kHeader1, kKey1, kValue1, ByteStr("\0\0\0\0\1")));
+ FlashPartition partition(&flash);
+ Entry entry;
+ ASSERT_EQ(Status::OK, Entry::Read(partition, 0, &entry));
+
+ // Last byte in padding is a 1; should fail.
+ ChecksumCrc16 checksum;
+ EXPECT_EQ(Status::DATA_LOSS, entry.VerifyChecksumInFlash(&checksum));
+
+ // The in-memory verification fills in 0s for the padding.
+ EXPECT_EQ(Status::OK, entry.VerifyChecksum(&checksum, "key45", kValue1));
+
+ flash.buffer()[kEntry1.size() - 1] = byte{0};
+ EXPECT_EQ(Status::OK, entry.VerifyChecksumInFlash(&checksum));
+}
} // namespace
} // namespace pw::kvs
diff --git a/pw_kvs/key_value_store.cc b/pw_kvs/key_value_store.cc
index 29fa778..b18d4f2 100644
--- a/pw_kvs/key_value_store.cc
+++ b/pw_kvs/key_value_store.cc
@@ -25,10 +25,17 @@
#include "pw_log/log.h"
namespace pw::kvs {
+namespace {
using std::byte;
using std::string_view;
+constexpr bool InvalidKey(std::string_view key) {
+ return key.empty() || (key.size() > Entry::kMaxKeyLength);
+}
+
+} // namespace
+
KeyValueStore::KeyValueStore(FlashPartition* partition,
const EntryHeaderFormat& format,
const Options& options)
@@ -148,7 +155,7 @@
}
// Read the key from flash & validate the entry (which reads the value).
- KeyBuffer key_buffer;
+ Entry::KeyBuffer key_buffer;
TRY_ASSIGN(size_t key_length, entry.ReadKey(key_buffer));
const string_view key(key_buffer.data(), key_length);
@@ -393,13 +400,13 @@
//
Status KeyValueStore::FindKeyDescriptor(string_view key,
const KeyDescriptor** result) const {
- Entry::KeyBuffer key_buffer;
const uint32_t hash = HashKey(key);
+ Entry::KeyBuffer key_buffer;
for (auto& descriptor : key_descriptors_) {
if (descriptor.key_hash == hash) {
TRY(Entry::ReadKey(
- partition_, descriptor.address, key.size(), key_buffer));
+ partition_, descriptor.address, key.size(), key_buffer.data()));
if (key == string_view(key_buffer.data(), key.size())) {
DBG("Found match for key hash 0x%08" PRIx32, hash);
diff --git a/pw_kvs/key_value_store_map_test.cc b/pw_kvs/key_value_store_map_test.cc
index 542493d..43da122 100644
--- a/pw_kvs/key_value_store_map_test.cc
+++ b/pw_kvs/key_value_store_map_test.cc
@@ -30,6 +30,7 @@
#include "pw_kvs/crc16_checksum.h"
#include "pw_kvs/in_memory_fake_flash.h"
#include "pw_kvs/key_value_store.h"
+#include "pw_kvs_private/entry.h"
#include "pw_log/log.h"
#include "pw_span/span.h"
@@ -114,8 +115,7 @@
// Either add a new key or replace an existing one.
if (empty() || random_int() % 2 == 0) {
- key =
- random_string(random_int() % (KeyValueStore::kMaxKeyLength + 1));
+ key = random_string(random_int() % (Entry::kMaxKeyLength + 1));
} else {
key = RandomPresentKey();
}
@@ -197,7 +197,7 @@
auto map_entry = map_.find(std::string(item.key()));
if (map_entry == map_.end()) {
- PW_LOG_CRITICAL("Entry %s missing from map", item.key().data());
+ PW_LOG_CRITICAL("Entry %s missing from map", item.key());
} else if (map_entry != map_.end()) {
EXPECT_EQ(map_entry->first, item.key());
@@ -218,7 +218,7 @@
Status result = kvs_.Put(key, as_bytes(span(value)));
- if (key.empty() || key.size() > KeyValueStore::kMaxKeyLength) {
+ if (key.empty() || key.size() > Entry::kMaxKeyLength) {
EXPECT_EQ(Status::INVALID_ARGUMENT, result);
} else if (map_.size() == KeyValueStore::kMaxEntries) {
EXPECT_EQ(Status::RESOURCE_EXHAUSTED, result);
@@ -248,7 +248,7 @@
Status result = kvs_.Delete(key);
- if (key.empty() || key.size() > KeyValueStore::kMaxKeyLength) {
+ if (key.empty() || key.size() > Entry::kMaxKeyLength) {
EXPECT_EQ(Status::INVALID_ARGUMENT, result);
} else if (map_.count(key) == 0) {
EXPECT_EQ(Status::NOT_FOUND, result);
@@ -270,18 +270,18 @@
void StartOperation(const std::string& operation, const std::string& key) {
count_ += 1;
- PW_LOG_CRITICAL(
+ PW_LOG_DEBUG(
"[%3u] START %s for '%s'", count_, operation.c_str(), key.c_str());
}
void FinishOperation(const std::string& operation,
Status result,
const std::string& key) {
- PW_LOG_CRITICAL("[%3u] FINISH %s <%s> for '%s'",
- count_,
- operation.c_str(),
- result.str(),
- key.c_str());
+ PW_LOG_DEBUG("[%3u] FINISH %s <%s> for '%s'",
+ count_,
+ operation.c_str(),
+ result.str(),
+ key.c_str());
}
bool empty() const { return map_.empty(); }
diff --git a/pw_kvs/key_value_store_test.cc b/pw_kvs/key_value_store_test.cc
index 4b00ed5..1644e7c 100644
--- a/pw_kvs/key_value_store_test.cc
+++ b/pw_kvs/key_value_store_test.cc
@@ -374,7 +374,7 @@
ASSERT_EQ(Status::OK, kvs_.Put("kEy", as_bytes(span(value))));
for (KeyValueStore::Item entry : kvs_) {
- EXPECT_STREQ(entry.key().data(), "kEy"); // Make sure null-terminated.
+ EXPECT_STREQ(entry.key(), "kEy"); // Make sure null-terminated.
char buffer[sizeof(value)] = {};
EXPECT_EQ(Status::OK, entry.Get(&buffer));
diff --git a/pw_kvs/public/pw_kvs/key_value_store.h b/pw_kvs/public/pw_kvs/key_value_store.h
index b29058d..b47041e 100644
--- a/pw_kvs/public/pw_kvs/key_value_store.h
+++ b/pw_kvs/public/pw_kvs/key_value_store.h
@@ -90,11 +90,6 @@
static constexpr size_t kMaxUsableSectors = 256;
static constexpr size_t kWorkingBufferSizeBytes = (4 * 1024);
- // TODO: Remove the duplicate kMaxKeyLength and KeyBuffer definitions. These
- // should be provided by the Entry class.
- static constexpr size_t kMaxKeyLength = 0b111111;
- using KeyBuffer = std::array<char, kMaxKeyLength + 1>;
-
// In the future, will be able to provide additional EntryHeaderFormats for
// backwards compatibility.
KeyValueStore(FlashPartition* partition,
@@ -157,8 +152,8 @@
class Item {
public:
- // Guaranteed to be null-terminated
- std::string_view key() const { return key_buffer_.data(); }
+ // The key as a null-terminated string.
+ const char* key() const { return key_buffer_.data(); }
StatusWithSize Get(span<std::byte> value_buffer) const {
return kvs_.Get(key(), value_buffer);
@@ -178,7 +173,13 @@
constexpr Item(const KeyValueStore& kvs) : kvs_(kvs), key_buffer_{} {}
const KeyValueStore& kvs_;
- KeyBuffer key_buffer_;
+
+ // TODO: Remove the duplicate kMaxKeyLength definition. This should be
+ // provided by the Entry class.
+ static constexpr size_t kMaxKeyLength = 0b111111;
+
+ // Buffer large enough for a null-terminated version of any valid key.
+ std::array<char, kMaxKeyLength + 1> key_buffer_;
};
class iterator {
@@ -280,10 +281,6 @@
Status CheckOperation(std::string_view key) const;
- static constexpr bool InvalidKey(std::string_view key) {
- return key.empty() || (key.size() > kMaxKeyLength);
- }
-
Status FindKeyDescriptor(std::string_view key,
const KeyDescriptor** result) const;
diff --git a/pw_kvs/pw_kvs_private/entry.h b/pw_kvs/pw_kvs_private/entry.h
index c272886..4cae4bb 100644
--- a/pw_kvs/pw_kvs_private/entry.h
+++ b/pw_kvs/pw_kvs_private/entry.h
@@ -64,20 +64,22 @@
using Address = FlashPartition::Address;
- // Buffer capable of holding a null-terminated version of any valid key.
- using KeyBuffer = std::array<char, kMaxKeyLength + 1>;
+ // Buffer capable of holding any valid key (without a null terminator);
+ using KeyBuffer = std::array<char, kMaxKeyLength>;
// Returns flash partition Read error codes, or one of the following:
//
// OK: successfully read the header and initialized the Entry
// NOT_FOUND: read the header, but the data appears to be erased
+ // DATA_LOSS: read the header, but it contained invalid data
//
static Status Read(FlashPartition& partition, Address address, Entry* entry);
- static StatusWithSize ReadKey(FlashPartition& partition,
- Address address,
- size_t key_length,
- KeyBuffer& key);
+ // Reads a key into a buffer, which must be at least key_length bytes.
+ static Status ReadKey(FlashPartition& partition,
+ Address address,
+ size_t key_length,
+ char* key);
// Creates a new Entry for a valid (non-deleted) entry.
static Entry Valid(FlashPartition& partition,
@@ -123,8 +125,14 @@
StatusWithSize Write(std::string_view key, span<const std::byte> value) const;
- StatusWithSize ReadKey(KeyBuffer& key) const {
- return ReadKey(partition(), address_, key_length(), key);
+ // Reads a key into a buffer, which must be large enough for a max-length key.
+ // If successful, the size is returned in the StatusWithSize. The key is not
+ // null terminated.
+ template <size_t kSize>
+ StatusWithSize ReadKey(std::array<char, kSize>& key) const {
+ static_assert(kSize >= kMaxKeyLength);
+ return StatusWithSize(
+ ReadKey(partition(), address_, key_length(), key.data()), key_length());
}
StatusWithSize ReadValue(span<std::byte> value) const;
@@ -149,6 +157,9 @@
// Total size of this entry, including padding.
size_t size() const { return AlignUp(content_size(), alignment_bytes()); }
+ // The length of the key in bytes. Keys are not null terminated.
+ size_t key_length() const { return header_.key_length_bytes; }
+
// The size of the value, without padding. The size is 0 if this is a
// tombstone entry.
size_t value_size() const {
@@ -173,9 +184,6 @@
uint32_t checksum() const { return header_.checksum; }
- // The length of the key in bytes. Keys are not null terminated.
- size_t key_length() const { return header_.key_length_bytes; }
-
size_t alignment_bytes() const { return (header_.alignment_units + 1) * 16; }
// The total size of the entry, excluding padding.