Add an attribute persistence class to centralize attribute I/O. (#40311)
* Add an attribute persistence class to centralize attribute I/O.
This adds type-safe methods around the AttributePersistenceProvider
so that we work without raw bytes.
This was extracted out of the BasicInformation cluster updates as
a stand-alone PR. No flash cost yet because unused, but first
cluster(s) to use these methods will start paying the cost.
* Typo fixing
* Split out code a bit, make it handle nulls better and separate out char and byte pascal string bits
* Add/update unit tests
* Use _span more
* Use arithmetic instead of fundamental
* Add test of value size not preserved for persistence
* Restyle
* Bug fix and add extra validation for strings being the right size
* Fix some bugs, refactor and add tests
* Restyle
* Fix typo and add one more test
* Fix comment
* Fix doxy comments
* Code simplification
* Comment update
* Remove 2021 from copyright - that was probably a wrong copy and paste
* Fix includes
* Correct the pascal string readability
* Restyle
* Update comments
* Fix includes
* Update src/app/persistence/AttributePersistence.cpp
Co-authored-by: Tennessee Carmel-Veilleux <tennessee.carmelveilleux@gmail.com>
* move tests to use the test persistent storage delegate
* Remove all references to pascal string I/O in attribute persistence. I was asked to approach this differently
* Start adding a String class that can be used to read/write values to persistent storage
* Some comment updates
* Unit test updates
* Restyle
* Review update
* Remove assignment/copy for Strings
* Add some casts
* Update src/app/persistence/AttributePersistence.cpp
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
* Update src/app/persistence/String.h
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
* Update src/app/persistence/String.h
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
* Add additional check
* Restyled by clang-format
* Fix minimal condition
* Update condition
* Update comment
* Update comments
* Add null termination after read
* Update unit tests
* Update src/app/persistence/AttributePersistence.h
Co-authored-by: Boris Zbarsky <bzbarsky@apple.com>
* Update src/app/persistence/AttributePersistence.h
Co-authored-by: Boris Zbarsky <bzbarsky@apple.com>
* Address some review comments
* Update src/app/persistence/PascalString.h
Co-authored-by: Boris Zbarsky <bzbarsky@apple.com>
* Update src/app/persistence/String.h
Co-authored-by: Boris Zbarsky <bzbarsky@apple.com>
* Update src/app/persistence/String.h
Co-authored-by: Boris Zbarsky <bzbarsky@apple.com>
* Drop IsValidContent
* More review comments
* one more review comment
* Update src/app/persistence/AttributePersistence.h
Co-authored-by: Boris Zbarsky <bzbarsky@apple.com>
* Update src/app/persistence/String.h
Co-authored-by: Boris Zbarsky <bzbarsky@apple.com>
* Update src/app/persistence/String.h
Co-authored-by: Boris Zbarsky <bzbarsky@apple.com>
* Update src/app/persistence/AttributePersistence.h
Co-authored-by: Boris Zbarsky <bzbarsky@apple.com>
* Update src/app/persistence/AttributePersistence.h
Co-authored-by: Boris Zbarsky <bzbarsky@apple.com>
* Adapter renames for stringio
* Fix include path
* Fix include path
---------
Co-authored-by: Andrei Litvin <andreilitvin@google.com>
Co-authored-by: Tennessee Carmel-Veilleux <tennessee.carmelveilleux@gmail.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Restyled.io <commits@restyled.io>
Co-authored-by: Boris Zbarsky <bzbarsky@apple.com>
diff --git a/src/app/persistence/AttributePersistence.cpp b/src/app/persistence/AttributePersistence.cpp
new file mode 100644
index 0000000..aaf50b1
--- /dev/null
+++ b/src/app/persistence/AttributePersistence.cpp
@@ -0,0 +1,83 @@
+/*
+ * Copyright (c) 2025 Project CHIP 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
+ *
+ * http://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 <app/persistence/AttributePersistence.h>
+
+#include <app/ConcreteAttributePath.h>
+#include <app/data-model/Nullable.h>
+#include <app/persistence/AttributePersistenceProvider.h>
+#include <app/persistence/String.h>
+#include <lib/core/CHIPError.h>
+#include <lib/support/Span.h>
+
+namespace chip::app {
+
+namespace {
+
+bool VerifySuccessLogOnFailure(const ConcreteAttributePath & path, CHIP_ERROR err)
+{
+ VerifyOrReturnValue(err != CHIP_NO_ERROR, true);
+
+ // Value not found is typical. Not an error worth logging.
+ VerifyOrReturnValue(err != CHIP_ERROR_PERSISTED_STORAGE_VALUE_NOT_FOUND, false);
+
+ ChipLogError(Zcl, "Failed to load attribute %u/" ChipLogFormatMEI "/" ChipLogFormatMEI ": %" CHIP_ERROR_FORMAT,
+ path.mEndpointId, ChipLogValueMEI(path.mClusterId), ChipLogValueMEI(path.mAttributeId), err.Format());
+ return false;
+}
+
+} // namespace
+
+bool AttributePersistence::InternalRawLoadNativeEndianValue(const ConcreteAttributePath & path, void * data,
+ const void * valueOnLoadFailure, size_t size)
+{
+ MutableByteSpan rawBytes(reinterpret_cast<uint8_t *>(data), size);
+ if (!VerifySuccessLogOnFailure(path, mProvider.ReadValue(path, rawBytes)))
+ {
+ // in case of failure, set the default value
+ memcpy(data, valueOnLoadFailure, size);
+ return false;
+ }
+
+ if (rawBytes.size() != size)
+ {
+ // short read: the value is not valid
+ memcpy(data, valueOnLoadFailure, size);
+ return false;
+ }
+
+ return true;
+}
+
+bool AttributePersistence::LoadString(const ConcreteAttributePath & path, Storage::Internal::ShortString & value)
+{
+ Storage::Internal::ShortStringInputAdapter io(value);
+ MutableByteSpan rawBytes = io.ReadBuffer();
+
+ if (!VerifySuccessLogOnFailure(path, mProvider.ReadValue(path, rawBytes)))
+ {
+ value.SetContent(""_span);
+ return false;
+ }
+ return io.FinalizeRead(rawBytes);
+}
+
+CHIP_ERROR AttributePersistence::StoreString(const ConcreteAttributePath & path, const Storage::Internal::ShortString & value)
+{
+ Storage::Internal::ShortStringOutputAdapter io(value);
+ return mProvider.WriteValue(path, io.ContentWithPrefix());
+}
+
+} // namespace chip::app
diff --git a/src/app/persistence/AttributePersistence.h b/src/app/persistence/AttributePersistence.h
new file mode 100644
index 0000000..4762eec
--- /dev/null
+++ b/src/app/persistence/AttributePersistence.h
@@ -0,0 +1,90 @@
+/*
+ * Copyright (c) 2025 Project CHIP 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
+ *
+ * http://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 <app/AttributeValueDecoder.h>
+#include <app/ConcreteAttributePath.h>
+#include <app/data-model-provider/ActionReturnStatus.h>
+#include <app/persistence/AttributePersistenceProvider.h>
+#include <app/persistence/String.h>
+
+#include <type_traits>
+
+namespace chip::app {
+
+/// Provides functionality for handling attribute persistence via
+/// an AttributePersistenceProvider.
+///
+/// AttributePersistenceProvider works with raw bytes, however attributes
+/// have known (strong) types and their load/decode logic is often
+/// similar and reusable. This class implements the logic of handling
+/// such attributes, so that it can be reused across cluster implementations.
+class AttributePersistence
+{
+public:
+ AttributePersistence(AttributePersistenceProvider & provider) : mProvider(provider) {}
+
+ /// Loads a native-endianness stored value of type `T` into `value` from the persistence provider.
+ ///
+ /// If load fails, `false` is returned and data is filled with `valueOnLoadFailure`.
+ ///
+ /// Error reason for load failure is logged (or nothing logged in case "Value not found" is the
+ /// reason for the load failure).
+ template <typename T, typename std::enable_if_t<std::is_arithmetic_v<T>> * = nullptr>
+ bool LoadNativeEndianValue(const ConcreteAttributePath & path, T & value, const T & valueOnLoadFailure)
+ {
+ return InternalRawLoadNativeEndianValue(path, &value, &valueOnLoadFailure, sizeof(T));
+ }
+
+ /// Performs all the steps of:
+ /// - decode the given raw data
+ /// - write to storage
+ template <typename T, typename std::enable_if_t<std::is_arithmetic_v<T>> * = nullptr>
+ CHIP_ERROR DecodeAndStoreNativeEndianValue(const ConcreteAttributePath & path, AttributeValueDecoder & decoder, T & value)
+ {
+ ReturnErrorOnFailure(decoder.Decode(value));
+ return mProvider.WriteValue(path, { reinterpret_cast<const uint8_t *>(&value), sizeof(value) });
+ }
+
+ /// Load the given string from concrete storage.
+ ///
+ /// NOTE: `value` is take as an internal short string to avoid the templates that Storage::String
+ /// implies, however callers are generally expected to pass in a `Storage::String` value and
+ /// not use internal classes directly.
+ ///
+ /// Returns true on success, false on failure. On failure the string is reset to empty.
+ bool LoadString(const ConcreteAttributePath & path, Storage::Internal::ShortString & value);
+
+ /// Store the given string in persistent storage.
+ ///
+ /// NOTE: `value` is take as an internal short string to avoid the templates that Storage::String
+ /// implies, however callers are generally expected to pass in a `Storage::String` value and
+ /// not use internal classes directly.
+ CHIP_ERROR StoreString(const ConcreteAttributePath & path, const Storage::Internal::ShortString & value);
+
+private:
+ AttributePersistenceProvider & mProvider;
+
+ /// Loads a raw value of size `size` into the memory pointed to by `data`.
+ /// If load fails, `false` is returned and data is filled with `valueOnLoadFailure`.
+ ///
+ /// Error reason for load failure is logged (or nothing logged in case "Value not found" is the
+ /// reason for the load failure).
+ bool InternalRawLoadNativeEndianValue(const ConcreteAttributePath & path, void * data, const void * valueOnLoadFailure,
+ size_t size);
+};
+
+} // namespace chip::app
diff --git a/src/app/persistence/BUILD.gn b/src/app/persistence/BUILD.gn
index 333ac2d..96869e0 100644
--- a/src/app/persistence/BUILD.gn
+++ b/src/app/persistence/BUILD.gn
@@ -16,12 +16,18 @@
source_set("persistence") {
sources = [
+ "AttributePersistence.cpp",
+ "AttributePersistence.h",
"AttributePersistenceProvider.h",
"PascalString.h",
+ "String.cpp",
+ "String.h",
]
public_deps = [
+ "${chip_root}/src/app:attribute-access",
"${chip_root}/src/app:paths",
+ "${chip_root}/src/app/data-model-provider",
"${chip_root}/src/lib/support:span",
]
}
diff --git a/src/app/persistence/PascalString.h b/src/app/persistence/PascalString.h
index 25fb92a..8b33d0d 100644
--- a/src/app/persistence/PascalString.h
+++ b/src/app/persistence/PascalString.h
@@ -75,8 +75,12 @@
{
public:
using LengthType = typename PascalPrefixOperations<PREFIX_LEN>::LengthType;
+ using ValueType = Span<const T>;
static constexpr LengthType kInvalidLength = PascalPrefixOperations<PREFIX_LEN>::kInvalidLength;
+ /// How many bytes of buffer are needed to store a max `charCount` sized buffer.
+ static constexpr size_t BufferSizeFor(size_t charCount) { return PREFIX_LEN + charCount; }
+
static_assert(sizeof(T) == 1);
PascalBuffer(PascalBuffer &&) = default;
@@ -89,6 +93,11 @@
static_assert(N <= kInvalidLength);
}
+ /// Allocates a pascal buffer of the given size.
+ ///
+ /// buffer_size includes the prefix and must be at least PREFIX_LEN.
+ PascalBuffer(Span<T> data) : mData(data.data()), mMaxSize(static_cast<LengthType>(data.size() - PREFIX_LEN)) {}
+
/// Returns the content of the pascal string.
/// Uses the prefix size information
Span<T> Content() { return { mData + PREFIX_LEN, GetContentLength() }; }
@@ -155,7 +164,7 @@
/// Checks if the given span is a valid Pascal string: i.e. its size prefix
/// is either Invalid (i.e. null marker) or it has a size that fits in the buffer
- static bool IsValid(Span<const T> span)
+ static bool IsValid(ByteSpan span)
{
VerifyOrReturnValue(span.size() >= PREFIX_LEN, false);
LengthType len = PascalPrefixOperations<PREFIX_LEN>::GetContentLength(span.data());
diff --git a/src/app/persistence/String.cpp b/src/app/persistence/String.cpp
new file mode 100644
index 0000000..2741bf5
--- /dev/null
+++ b/src/app/persistence/String.cpp
@@ -0,0 +1,46 @@
+/*
+ * Copyright (c) 2025 Project CHIP 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
+ *
+ * http://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 <app/persistence/String.h>
+
+namespace chip::app::Storage::Internal {
+
+bool ShortString::SetContent(CharSpan value)
+{
+
+ ShortPascalString view = AsPascal();
+
+ if (!view.SetValue(value))
+ {
+ view.SetValue(""_span);
+ NullTerminate();
+ return false;
+ }
+ NullTerminate();
+ return true;
+}
+
+bool ShortStringInputAdapter::FinalizeRead(ByteSpan actuallyRead)
+{
+ if (!ShortPascalString::IsValid(actuallyRead))
+ {
+ mValue.SetContent(""_span);
+ return false;
+ }
+ mValue.NullTerminate();
+ return true;
+}
+
+} // namespace chip::app::Storage::Internal
diff --git a/src/app/persistence/String.h b/src/app/persistence/String.h
new file mode 100644
index 0000000..ad226f1
--- /dev/null
+++ b/src/app/persistence/String.h
@@ -0,0 +1,145 @@
+/**
+ * Copyright (c) 2025 Project CHIP 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
+ *
+ * http://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 <app/persistence/AttributePersistenceProvider.h>
+#include <app/persistence/PascalString.h>
+#include <lib/support/Span.h>
+
+#include <cstddef>
+#include <cstdint>
+
+namespace chip::app::Storage {
+
+namespace Internal {
+
+/// Represents a string that can be read/written to storage.
+///
+/// This is currently implemented for SHORT strings only (< 255 bytes)
+/// that are NOT nullable
+class ShortString
+{
+public:
+ /// Use the input buffer as a short pascal string.
+ ///
+ /// The input buffer is assumed to have:
+ /// - size for a PascalString (i.e. generally pascal string max length + prefix == max length + 1 given we use
+ /// short pascal strings here)
+ /// - an extra +1 bytes for a null terminator so that c_str works.
+ ///
+ /// This means that buffer_size should be `N+2` where N is the expected maximum string length.
+ ///
+ /// This class is considered Internal, use chip::app::Storage::String<MAX_LENGTH> in application code.
+ ShortString(char * buffer, size_t buffer_size) : mBuffer(buffer), mPascalSize(static_cast<uint8_t>(buffer_size - 1))
+ {
+ // for a buffer to be usable we need 1 byte for size, 1 byte for content and 1 byte for null terminator.
+ // Strings without any size make no sense.
+ VerifyOrDie(buffer_size >= 3);
+ VerifyOrDie(buffer_size <= 1 + 254 + 1); // prefix + 254 length string + null terminator
+ }
+
+ /// Returns the content as a character span
+ CharSpan Content() const { return AsPascal().Content(); }
+
+ /// Returns the content as a null terminated string
+ const char * c_str() const { return Content().data(); /* ALWAYS null terminated*/ }
+
+ /// sets the internal value of the string to the given value.
+ /// If the set fails, the value is set to empty string and false is returned.
+ bool SetContent(CharSpan value);
+
+ friend class ShortStringOutputAdapter;
+ friend class ShortStringInputAdapter;
+
+private:
+ char * mBuffer;
+ uint8_t mPascalSize;
+
+ ShortPascalString AsPascal() { return Span{ mBuffer, mPascalSize }; }
+ ShortConstPascalString AsPascal() const { return CharSpan{ mBuffer, mPascalSize }; }
+
+ /// Places a null terminator after the pascal string content, so that c_str() works
+ void NullTerminate() { mBuffer[AsPascal().ContentWithLenPrefix().size()] = 0; }
+};
+
+/// Provides internal access and processing to a ShortString class.
+///
+/// The class is intended to read raw data into a short string: it provides
+/// access to the internal read buffer and allows a `FinalizeRead` call to
+/// perform final validation of the read.
+///
+/// Meant for internal use only, this is NOT public API outside the SDK code itself.
+class ShortStringInputAdapter
+{
+public:
+ ShortStringInputAdapter(ShortString & value) : mValue(value) {}
+ MutableByteSpan ReadBuffer() { return mValue.AsPascal().RawFullBuffer(); }
+
+ /// Method to be called once data has been read into ReadBuffer
+ ///
+ /// Validates that the read span is valid, sets the value to empty string on failure.
+ /// ByteSpan MUST be a subset of the buffer inside of the mValue.AsPascal().
+ bool FinalizeRead(ByteSpan actuallyRead);
+
+private:
+ ShortString & mValue;
+};
+
+/// Provides internal access and processing to a ShortString class.
+///
+/// The class is intended to provide access to a serializable view of the
+/// short string, specifically access to its content with length.
+///
+/// Meant for internal use only, this is NOT public API outside the SDK code itself.
+class ShortStringOutputAdapter
+{
+public:
+ ShortStringOutputAdapter(const ShortString & value) : mValue(value) {}
+ ByteSpan ContentWithPrefix() const { return mValue.AsPascal().ContentWithLenPrefix(); }
+
+private:
+ const ShortString & mValue;
+};
+
+} // namespace Internal
+
+/// Represents a string of maximum length of `N` characters
+///
+/// The string storage will be formatted to support persistent storage I/O,
+/// specifically stored as a length-prefixed string and it also maintains a null terminator
+/// so that both char-span and null terminated views are usable. The storage
+/// overhead of this string is 2 bytes (one for length prefix, one for null terminator).
+template <size_t N, typename = std::enable_if_t<(N < 255) && (N > 0)>>
+class String : public Internal::ShortString
+{
+public:
+ String() : Internal::ShortString(mBuffer, sizeof(mBuffer)) { SetContent(""_span); }
+
+ // internal shortstring contains self-referencing pointers. That cannot be copied, so we assume no copy for now
+ // These could be implemented, however for now we assume people should just use the underlying Span() to set
+ // the values.
+ String(String &&) = delete;
+ String & operator=(String &&) = delete;
+ String(const String &) = delete;
+ String & operator=(const String &) = delete;
+
+private:
+ // +1 byte to null-terminate to allow for a c_str() implementation
+ // as that seems very convenient.
+ char mBuffer[ShortPascalString::BufferSizeFor(N) + 1] = { 0 };
+};
+
+} // namespace chip::app::Storage
diff --git a/src/app/persistence/tests/BUILD.gn b/src/app/persistence/tests/BUILD.gn
index 0f04f5d..b8f975e 100644
--- a/src/app/persistence/tests/BUILD.gn
+++ b/src/app/persistence/tests/BUILD.gn
@@ -20,10 +20,16 @@
chip_test_suite("tests") {
output_name = "appPersistenceTests"
- test_sources = [ "TestPascalString.cpp" ]
+ test_sources = [
+ "TestAttributePersistence.cpp",
+ "TestPascalString.cpp",
+ "TestString.cpp",
+ ]
public_deps = [
+ "${chip_root}/src/app/data-model-provider/tests:encode-decode",
"${chip_root}/src/app/persistence",
+ "${chip_root}/src/app/persistence:default",
"${chip_root}/src/lib/core:string-builder-adapters",
"${chip_root}/src/lib/support:testing",
]
diff --git a/src/app/persistence/tests/TestAttributePersistence.cpp b/src/app/persistence/tests/TestAttributePersistence.cpp
new file mode 100644
index 0000000..28f3821
--- /dev/null
+++ b/src/app/persistence/tests/TestAttributePersistence.cpp
@@ -0,0 +1,234 @@
+/*
+ * Copyright (c) 2025 Project CHIP 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
+ *
+ * http://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_unit_test/framework.h>
+
+#include <app/AttributeValueDecoder.h>
+#include <app/ConcreteAttributePath.h>
+#include <app/data-model-provider/tests/WriteTesting.h>
+#include <app/persistence/AttributePersistence.h>
+#include <app/persistence/DefaultAttributePersistenceProvider.h>
+#include <app/persistence/String.h>
+#include <lib/core/CHIPError.h>
+#include <lib/core/StringBuilderAdapters.h>
+#include <lib/support/DefaultStorageKeyAllocator.h>
+#include <lib/support/Span.h>
+#include <lib/support/TestPersistentStorageDelegate.h>
+#include <unistd.h>
+
+namespace {
+
+using namespace chip;
+using namespace chip::app;
+using namespace chip::app::Testing;
+
+TEST(TestAttributePersistence, TestLoadAndDecodeAndStoreNativeEndian)
+{
+ TestPersistentStorageDelegate storageDelegate;
+ DefaultAttributePersistenceProvider ramProvider;
+ ASSERT_EQ(ramProvider.Init(&storageDelegate), CHIP_NO_ERROR);
+
+ AttributePersistence persistence(ramProvider);
+
+ const ConcreteAttributePath path(1, 2, 3);
+ const ConcreteAttributePath wrongPath(1, 2, 4);
+ constexpr uint32_t kValueToStore = 42;
+ constexpr uint32_t kOtherValue = 99;
+
+ // Store a fake value
+ {
+ const uint32_t value = kValueToStore;
+ EXPECT_EQ(storageDelegate.SyncSetKeyValue(
+ DefaultStorageKeyAllocator::AttributeValue(path.mEndpointId, path.mClusterId, path.mAttributeId).KeyName(),
+ &value, sizeof(value)),
+ CHIP_NO_ERROR);
+ }
+
+ // Test loading a value
+ {
+ uint32_t valueRead = 0;
+
+ ASSERT_TRUE(persistence.LoadNativeEndianValue(path, valueRead, kOtherValue));
+ ASSERT_EQ(valueRead, kValueToStore);
+ }
+
+ // Test loading a non-existent value
+ {
+ uint32_t valueRead = 0;
+
+ ASSERT_FALSE(persistence.LoadNativeEndianValue(wrongPath, valueRead, kOtherValue));
+ ASSERT_EQ(valueRead, kOtherValue);
+ }
+
+ // Test loading a removed value
+ {
+ EXPECT_EQ(storageDelegate.SyncDeleteKeyValue(
+ DefaultStorageKeyAllocator::AttributeValue(path.mEndpointId, path.mClusterId, path.mAttributeId).KeyName()),
+ CHIP_NO_ERROR);
+
+ uint32_t valueRead = 0;
+ ASSERT_FALSE(persistence.LoadNativeEndianValue(path, valueRead, kOtherValue));
+ ASSERT_EQ(valueRead, kOtherValue);
+ }
+}
+
+TEST(TestAttributePersistence, TestNativeRawValueViaDecoder)
+{
+ TestPersistentStorageDelegate storageDelegate;
+ DefaultAttributePersistenceProvider ramProvider;
+ ASSERT_EQ(ramProvider.Init(&storageDelegate), CHIP_NO_ERROR);
+
+ AttributePersistence persistence(ramProvider);
+
+ const ConcreteAttributePath path(1, 2, 3);
+ const ConcreteAttributePath wrongPath(1, 2, 4);
+ constexpr uint32_t kValueToStore = 0x12345678;
+ constexpr uint32_t kOtherValue = 0x99887766;
+ uint32_t valueRead = 0;
+
+ // Store a value using an encoder (these are a PAIN to create, so use data model provider helpers)
+ {
+ WriteOperation writeOp(path);
+ AttributeValueDecoder decoder = writeOp.DecoderFor(kValueToStore);
+ EXPECT_EQ(persistence.DecodeAndStoreNativeEndianValue(path, decoder, valueRead), CHIP_NO_ERROR);
+ EXPECT_EQ(valueRead, kValueToStore);
+ }
+
+ {
+ valueRead = 0;
+ ASSERT_TRUE(persistence.LoadNativeEndianValue(path, valueRead, kOtherValue));
+ ASSERT_EQ(valueRead, kValueToStore);
+ }
+
+ // Try to read non-compatible types (note that size-wise compatible types will work ... wrongly (like u32 and float))
+ // this extra check is best-effort
+ {
+ uint16_t smallValue = 0;
+ const uint16_t kOther = 123u;
+
+ ASSERT_FALSE(persistence.LoadNativeEndianValue(path, smallValue, kOther));
+ ASSERT_EQ(smallValue, kOther);
+ }
+ {
+ uint64_t largeValue = 0;
+ const uint64_t kOther = 0x1122334455667788ull;
+
+ ASSERT_FALSE(persistence.LoadNativeEndianValue(path, largeValue, kOther));
+ ASSERT_EQ(largeValue, kOther);
+ }
+}
+
+TEST(TestAttributePersistence, TestStrings)
+{
+ TestPersistentStorageDelegate storageDelegate;
+ DefaultAttributePersistenceProvider ramProvider;
+ ASSERT_EQ(ramProvider.Init(&storageDelegate), CHIP_NO_ERROR);
+
+ AttributePersistence persistence(ramProvider);
+
+ const ConcreteAttributePath path(1, 2, 3);
+ const ConcreteAttributePath wrongPath(1, 2, 4);
+
+ {
+ Storage::String<8> testString;
+
+ ASSERT_TRUE(testString.SetContent("foo"_span));
+ ASSERT_EQ(persistence.StoreString(path, testString), CHIP_NO_ERROR);
+ }
+
+ {
+ Storage::String<16> readString;
+ ASSERT_TRUE(persistence.LoadString(path, readString));
+ ASSERT_TRUE(readString.Content().data_equal("foo"_span));
+ ASSERT_STREQ(readString.c_str(), "foo");
+ }
+
+ // fits exactly. Load should succeed
+ {
+ Storage::String<3> readString;
+ ASSERT_TRUE(persistence.LoadString(path, readString));
+ ASSERT_TRUE(readString.Content().data_equal("foo"_span));
+ ASSERT_STREQ(readString.c_str(), "foo");
+ }
+
+ // no space: data is cleared on load error
+ {
+ Storage::String<2> readString;
+ ASSERT_FALSE(persistence.LoadString(path, readString));
+ ASSERT_TRUE(readString.Content().empty());
+
+ ASSERT_TRUE(readString.SetContent("xy"_span));
+ ASSERT_FALSE(readString.Content().empty());
+ ASSERT_FALSE(persistence.LoadString(path, readString));
+ ASSERT_TRUE(readString.Content().empty());
+ ASSERT_STREQ(readString.c_str(), "");
+ }
+
+ // wrong path: data is cleared on load error
+ {
+ Storage::String<16> readString;
+
+ ASSERT_TRUE(readString.SetContent("xy"_span));
+ ASSERT_FALSE(readString.Content().empty());
+ ASSERT_FALSE(persistence.LoadString(wrongPath, readString));
+ ASSERT_TRUE(readString.Content().empty());
+ ASSERT_STREQ(readString.c_str(), "");
+ }
+
+ // empty string can be stored and loaded
+ {
+ Storage::String<8> testString;
+ Storage::String<16> readString;
+
+ ASSERT_TRUE(testString.SetContent(""_span));
+ ASSERT_EQ(persistence.StoreString(path, testString), CHIP_NO_ERROR);
+
+ ASSERT_TRUE(readString.SetContent("some value"_span));
+ ASSERT_TRUE(persistence.LoadString(path, readString));
+ ASSERT_TRUE(readString.Content().empty());
+ ASSERT_STREQ(readString.c_str(), "");
+ }
+}
+
+TEST(TestAttributePersistence, TestInvalidPascalLengthStored)
+{
+ TestPersistentStorageDelegate storageDelegate;
+ DefaultAttributePersistenceProvider ramProvider;
+ ASSERT_EQ(ramProvider.Init(&storageDelegate), CHIP_NO_ERROR);
+
+ AttributePersistence persistence(ramProvider);
+ const ConcreteAttributePath path(1, 2, 3);
+
+ // This string is invalid as stored
+ {
+ uint8_t buffer[] = { 10, 'h', 'e', 'l', 'l', 'o' }; // length 10, but only 5 chars
+ EXPECT_EQ(storageDelegate.SyncSetKeyValue(
+ DefaultStorageKeyAllocator::AttributeValue(path.mEndpointId, path.mClusterId, path.mAttributeId).KeyName(),
+ buffer, sizeof(buffer)),
+ CHIP_NO_ERROR);
+ }
+
+ // Load into a buffer that COULD contain the string, but
+ // stored string is invalid
+ {
+ Storage::String<16> readString;
+
+ ASSERT_TRUE(readString.SetContent("some value"_span));
+ ASSERT_FALSE(persistence.LoadString(path, readString));
+ ASSERT_TRUE(readString.Content().empty());
+ }
+}
+
+} // namespace
diff --git a/src/app/persistence/tests/TestString.cpp b/src/app/persistence/tests/TestString.cpp
new file mode 100644
index 0000000..0e97713
--- /dev/null
+++ b/src/app/persistence/tests/TestString.cpp
@@ -0,0 +1,75 @@
+/*
+ * Copyright (c) 2025 Project CHIP 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
+ *
+ * http://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_unit_test/framework.h>
+
+#include <app/persistence/String.h>
+#include <lib/core/StringBuilderAdapters.h>
+#include <lib/support/Span.h>
+
+namespace {
+
+using namespace chip;
+using namespace chip::app;
+using namespace chip::app::Storage;
+
+TEST(TestString, TestSetContent)
+{
+ String<10> testString;
+
+ // Expect empty initialization
+ EXPECT_TRUE(testString.Content().data_equal(""_span));
+ EXPECT_STREQ(testString.c_str(), "");
+
+ // basic test
+ EXPECT_TRUE(testString.SetContent("hello"_span));
+ EXPECT_TRUE(testString.Content().data_equal("hello"_span));
+ EXPECT_STREQ(testString.c_str(), "hello");
+
+ // Exactly fitting
+ EXPECT_TRUE(testString.SetContent("0123456789"_span));
+ EXPECT_TRUE(testString.Content().data_equal("0123456789"_span));
+ EXPECT_STREQ(testString.c_str(), "0123456789");
+
+ // Too large
+ EXPECT_FALSE(testString.SetContent("0123456789a"_span));
+ EXPECT_TRUE(testString.Content().empty());
+ EXPECT_STREQ(testString.c_str(), "");
+
+ // empty content
+ EXPECT_TRUE(testString.SetContent(""_span));
+ EXPECT_TRUE(testString.Content().empty());
+ EXPECT_STREQ(testString.c_str(), "");
+
+ // C-str works after truncation
+ EXPECT_TRUE(testString.SetContent("0123456789"_span));
+ EXPECT_TRUE(testString.SetContent("01"_span));
+ EXPECT_TRUE(testString.Content().data_equal("01"_span));
+ EXPECT_STREQ(testString.c_str(), "01");
+}
+
+TEST(TestString, TestCStr)
+{
+ String<5> testString;
+
+ EXPECT_TRUE(testString.SetContent("abc"_span));
+ EXPECT_STREQ(testString.c_str(), "abc");
+
+ // c_str() should work even if we do not have spare characters
+ EXPECT_TRUE(testString.SetContent("abcde"_span));
+ EXPECT_STREQ(testString.c_str(), "abcde");
+}
+
+} // namespace