diff --git a/src/app/BUILD.gn b/src/app/BUILD.gn index 9150bfa7857185..74d01091b8d791 100644 --- a/src/app/BUILD.gn +++ b/src/app/BUILD.gn @@ -201,6 +201,10 @@ static_library("app") { "TimerDelegates.h", "WriteClient.cpp", "WriteHandler.cpp", + "data-model/FabricScopedPreEncodedValue.cpp", + "data-model/FabricScopedPreEncodedValue.h", + "data-model/PreEncodedValue.cpp", + "data-model/PreEncodedValue.h", "reporting/Engine.cpp", "reporting/Engine.h", "reporting/ReportScheduler.h", diff --git a/src/app/data-model/FabricScopedPreEncodedValue.cpp b/src/app/data-model/FabricScopedPreEncodedValue.cpp new file mode 100644 index 00000000000000..30bd9a4f5101bd --- /dev/null +++ b/src/app/data-model/FabricScopedPreEncodedValue.cpp @@ -0,0 +1,103 @@ +/* + * Copyright (c) 2024 Project CHIP Authors + * All rights reserved. + * + * 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 "FabricScopedPreEncodedValue.h" +#include +#include + +#include + +namespace chip { +namespace app { +namespace DataModel { + +FabricScopedPreEncodedValue::FabricScopedPreEncodedValue(const ByteSpan & aData) : mData(aData) {} + +CHIP_ERROR FabricScopedPreEncodedValue::EncodeForRead(TLV::TLVWriter & aWriter, TLV::Tag aTag, FabricIndex aFabricIndex) const +{ + TLV::TLVReader reader; + reader.Init(mData); + + ReturnErrorOnFailure(reader.Next()); + + return aWriter.CopyElement(aTag, reader); +} + +FabricIndex FabricScopedPreEncodedValue::GetFabricIndex() const +{ + TLV::TLVReader reader; + reader.Init(mData); + CHIP_ERROR err = reader.Next(); + if (err != CHIP_NO_ERROR) + { + return kUndefinedFabricIndex; + } + + // We must have a struct here. + if (reader.GetType() != TLV::kTLVType_Structure) + { + return kUndefinedFabricIndex; + } + + TLV::TLVType structType; + err = reader.EnterContainer(structType); + if (err != CHIP_NO_ERROR) + { + return kUndefinedFabricIndex; + } + + // Now look for a single field with the right tag. + std::optional foundFabricIndex; + constexpr TLV::Tag kFabricIndexTag = TLV::ContextTag(254); + while ((err = reader.Next()) == CHIP_NO_ERROR) + { + if (reader.GetTag() != kFabricIndexTag) + { + continue; + } + + if (foundFabricIndex.has_value()) + { + // Two fabric indices? Just give up. Note that this will lead to + // errors encoding our value. + return kUndefinedFabricIndex; + } + + err = reader.Get(foundFabricIndex.emplace()); + if (err != CHIP_NO_ERROR) + { + return kUndefinedFabricIndex; + } + } + + if (err != CHIP_ERROR_END_OF_TLV) + { + return kUndefinedFabricIndex; + } + + err = reader.ExitContainer(structType); + if (err != CHIP_NO_ERROR) + { + return kUndefinedFabricIndex; + } + + return foundFabricIndex.value_or(kUndefinedFabricIndex); +} + +} // namespace DataModel +} // namespace app +} // namespace chip diff --git a/src/app/data-model/FabricScopedPreEncodedValue.h b/src/app/data-model/FabricScopedPreEncodedValue.h new file mode 100644 index 00000000000000..b95b99369c9b45 --- /dev/null +++ b/src/app/data-model/FabricScopedPreEncodedValue.h @@ -0,0 +1,58 @@ +/* + * Copyright (c) 2024 Project CHIP Authors + * All rights reserved. + * + * 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 +#include +#include +#include + +namespace chip { +namespace app { +namespace DataModel { + +/** + * FabridScopedPreEncodedValue represents the value of a single item in a list + * of fabric-scoped structs that has already been encoded as TLV. To enable + * reading these values successfully, the struct must have the fabric index it + * corresponds to encoded with the right tag. + * + * Note: Fabric-sensitive fields are currently not supported; there is no way to + * specify which fields are fabric-sensitive. + */ +class FabricScopedPreEncodedValue +{ +public: + /** + * The data buffer backing the ByteSpan must outlive the + * FabricScopedPreEncodedValue instance. + */ + FabricScopedPreEncodedValue(const ByteSpan & aData); + + /** + * Encodable object API implementation. + */ + static constexpr bool kIsFabricScoped = true; + CHIP_ERROR EncodeForRead(TLV::TLVWriter & aWriter, TLV::Tag aTag, FabricIndex aFabricIndex) const; + FabricIndex GetFabricIndex() const; + +private: + const ByteSpan mData; +}; + +} // namespace DataModel +} // namespace app +} // namespace chip diff --git a/src/app/data-model/PreEncodedValue.cpp b/src/app/data-model/PreEncodedValue.cpp new file mode 100644 index 00000000000000..3de3e1c98e8b11 --- /dev/null +++ b/src/app/data-model/PreEncodedValue.cpp @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2024 Project CHIP Authors + * All rights reserved. + * + * 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 "PreEncodedValue.h" +#include +#include + +namespace chip { +namespace app { +namespace DataModel { + +PreEncodedValue::PreEncodedValue(const ByteSpan & aData) : mData(aData) {} + +CHIP_ERROR PreEncodedValue::Encode(TLV::TLVWriter & aWriter, TLV::Tag aTag) const +{ + TLV::TLVReader reader; + reader.Init(mData); + + ReturnErrorOnFailure(reader.Next()); + + return aWriter.CopyElement(aTag, reader); +} + +} // namespace DataModel +} // namespace app +} // namespace chip diff --git a/src/app/data-model/PreEncodedValue.h b/src/app/data-model/PreEncodedValue.h new file mode 100644 index 00000000000000..052c3872adc9b9 --- /dev/null +++ b/src/app/data-model/PreEncodedValue.h @@ -0,0 +1,52 @@ +/* + * Copyright (c) 2024 Project CHIP Authors + * All rights reserved. + * + * 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 +#include +#include + +namespace chip { +namespace app { +namespace DataModel { + +/** + * PreEncodedValue represents the value of an attribute or the value of a single + * item in a list of non-fabric-scoped structs that has already been + * encoded as TLV. + */ +class PreEncodedValue +{ +public: + /** + * The data buffer backing the ByteSpan must outlive the PreEncodedValue + * instance. + */ + PreEncodedValue(const ByteSpan & aData); + + /** + * Encodable object API implementation. + */ + static constexpr bool kIsFabricScoped = false; + CHIP_ERROR Encode(TLV::TLVWriter & aWriter, TLV::Tag aTag) const; + +private: + const ByteSpan mData; +}; + +} // namespace DataModel +} // namespace app +} // namespace chip diff --git a/src/app/tests/TestAttributeValueEncoder.cpp b/src/app/tests/TestAttributeValueEncoder.cpp index 991f0ab0169307..bd4a59ebec5463 100644 --- a/src/app/tests/TestAttributeValueEncoder.cpp +++ b/src/app/tests/TestAttributeValueEncoder.cpp @@ -25,10 +25,16 @@ #include #include #include +#include +#include +#include +#include #include #include #include +#include + using namespace chip; using namespace chip::app; using namespace chip::TLV; @@ -43,6 +49,7 @@ constexpr ClusterId kRandomClusterId = 0xaa; constexpr AttributeId kRandomAttributeId = 0xcc; constexpr DataVersion kRandomDataVersion = 0x99; constexpr FabricIndex kTestFabricIndex = 1; +constexpr TLV::Tag kFabricIndexTag = TLV::ContextTag(254); template struct LimitedTestSetup @@ -553,6 +560,288 @@ void TestEncodeListChunking2(nlTestSuite * aSuite, void * aContext) } } +void TestEncodePreEncoded(nlTestSuite * aSuite, void * aContext) +{ + TestSetup test(aSuite); + + uint8_t buffer[128]; + TLV::TLVWriter writer; + writer.Init(buffer); + // Use a random tag that is not the right tag. + CHIP_ERROR err = writer.PutString(TLV::ProfileTag(0x1234abcd, 0x5678fedc), "hello"); + NL_TEST_ASSERT(aSuite, err == CHIP_NO_ERROR); + + err = writer.Finalize(); + NL_TEST_ASSERT(aSuite, err == CHIP_NO_ERROR); + + ByteSpan value(buffer, writer.GetLengthWritten()); + err = test.encoder.Encode(DataModel::PreEncodedValue(value)); + NL_TEST_ASSERT(aSuite, err == CHIP_NO_ERROR); + + const uint8_t expected[] = { + // clang-format off + 0x15, 0x36, 0x01, // Test overhead, Start Anonymous struct + Start 1 byte Tag Array + Tag (01) + 0x15, // Start anonymous struct + 0x35, 0x01, // Start 1 byte tag struct + Tag (01) + 0x24, 0x00, 0x99, // Tag (00) Value (1 byte uint) 0x99 (Attribute Version) + 0x37, 0x01, // Start 1 byte tag list + Tag (01) (Attribute Path) + 0x24, 0x02, 0x55, // Tag (02) Value (1 byte uint) 0x55 + 0x24, 0x03, 0xaa, // Tag (03) Value (1 byte uint) 0xaa + 0x24, 0x04, 0xcc, // Tag (04) Value (1 byte uint) 0xcc + 0x18, // End of container + 0x2C, 0x02, 0x05, 0x68, 0x65, 0x6C, 0x6C, 0x6F, // Tag (02) Value "hello", with 1-byte length of 0x05 (Attribute Value) + 0x18, // End of container + 0x18, // End of container + // clang-format on + }; + VERIFY_BUFFER_STATE(aSuite, test, expected); +} + +void TestEncodeListOfPreEncoded(nlTestSuite * aSuite, void * aContext) +{ + TestSetup test(aSuite); + + uint8_t buffers[2][128]; + std::optional values[2]; + + { + TLV::TLVWriter writer; + writer.Init(buffers[0]); + // Use a random tag that is not the right tag. + CHIP_ERROR err = writer.PutString(TLV::ProfileTag(0x1234abcd, 0x5678fedc), "hello"); + NL_TEST_ASSERT(aSuite, err == CHIP_NO_ERROR); + + err = writer.Finalize(); + NL_TEST_ASSERT(aSuite, err == CHIP_NO_ERROR); + + values[0].emplace(ByteSpan(buffers[0], writer.GetLengthWritten())); + } + + { + TLV::TLVWriter writer; + writer.Init(buffers[1]); + // Use a random tag that is not the right tag. + CHIP_ERROR err = writer.PutString(TLV::ProfileTag(0x1234abcd, 0x00010002), "bye"); + NL_TEST_ASSERT(aSuite, err == CHIP_NO_ERROR); + + err = writer.Finalize(); + NL_TEST_ASSERT(aSuite, err == CHIP_NO_ERROR); + + values[1].emplace(ByteSpan(buffers[1], writer.GetLengthWritten())); + } + + CHIP_ERROR err = test.encoder.EncodeList([&values](const auto & encoder) { + for (auto & value : values) + { + ReturnErrorOnFailure(encoder.Encode(value.value())); + } + return CHIP_NO_ERROR; + }); + NL_TEST_ASSERT(aSuite, err == CHIP_NO_ERROR); + + const uint8_t expected[] = { + // clang-format off + 0x15, 0x36, 0x01, // Test overhead, Start Anonymous struct + Start 1 byte Tag Array + Tag (01) + 0x15, // Start anonymous struct + 0x35, 0x01, // Start 1 byte tag struct + Tag (01) + 0x24, 0x00, 0x99, // Tag (00) Value (1 byte uint) 0x99 (Attribute Version) + 0x37, 0x01, // Start 1 byte tag list + Tag (01) (Attribute Path) + 0x24, 0x02, 0x55, // Tag (02) Value (1 byte uint) 0x55 + 0x24, 0x03, 0xaa, // Tag (03) Value (1 byte uint) 0xaa + 0x24, 0x04, 0xcc, // Tag (04) Value (1 byte uint) 0xcc + 0x18, // End of container + 0x36, 0x02, // Start 1 byte tag array + Tag (02) (Attribute Value) + 0x0C, 0x05, 0x68, 0x65, 0x6C, 0x6C, 0x6F, // "hello", with 1-byte length of 0x05 + 0x0C, 0x03, 0x62, 0x79, 0x65, // "bye", with 1-byte length of 0x03 + 0x18, // End of array + 0x18, // End of container + 0x18, // End of container + // clang-format on + }; + VERIFY_BUFFER_STATE(aSuite, test, expected); +} + +void TestEncodeListOfFabricScopedPreEncoded(nlTestSuite * aSuite, void * aContext) +{ + TestSetup test(aSuite); + + uint8_t buffers[2][128]; + std::optional values[2]; + + { + TLV::TLVWriter writer; + writer.Init(buffers[0]); + // Use a random tag that is not the right tag. + TLV::TLVType outerContainerType; + CHIP_ERROR err = + writer.StartContainer(TLV::ProfileTag(0x1234abcd, 0x5678fedc), TLV::kTLVType_Structure, outerContainerType); + NL_TEST_ASSERT(aSuite, err == CHIP_NO_ERROR); + + err = writer.PutString(TLV::ContextTag(7), "hello"); + NL_TEST_ASSERT(aSuite, err == CHIP_NO_ERROR); + + err = writer.Put(kFabricIndexTag, kTestFabricIndex); + NL_TEST_ASSERT(aSuite, err == CHIP_NO_ERROR); + + err = writer.EndContainer(outerContainerType); + NL_TEST_ASSERT(aSuite, err == CHIP_NO_ERROR); + + err = writer.Finalize(); + NL_TEST_ASSERT(aSuite, err == CHIP_NO_ERROR); + + values[0].emplace(ByteSpan(buffers[0], writer.GetLengthWritten())); + } + + { + TLV::TLVWriter writer; + writer.Init(buffers[1]); + // Use a random tag that is not the right tag. + TLV::TLVType outerContainerType; + CHIP_ERROR err = + writer.StartContainer(TLV::ProfileTag(0x1234abcd, 0x00010002), TLV::kTLVType_Structure, outerContainerType); + NL_TEST_ASSERT(aSuite, err == CHIP_NO_ERROR); + + err = writer.PutString(TLV::ContextTag(7), "bye"); + NL_TEST_ASSERT(aSuite, err == CHIP_NO_ERROR); + + err = writer.Put(kFabricIndexTag, static_cast(kTestFabricIndex + 1)); + NL_TEST_ASSERT(aSuite, err == CHIP_NO_ERROR); + + err = writer.EndContainer(outerContainerType); + NL_TEST_ASSERT(aSuite, err == CHIP_NO_ERROR); + + err = writer.Finalize(); + NL_TEST_ASSERT(aSuite, err == CHIP_NO_ERROR); + + values[1].emplace(ByteSpan(buffers[1], writer.GetLengthWritten())); + } + + CHIP_ERROR err = test.encoder.EncodeList([&values](const auto & encoder) { + for (auto & value : values) + { + ReturnErrorOnFailure(encoder.Encode(value.value())); + } + return CHIP_NO_ERROR; + }); + NL_TEST_ASSERT(aSuite, err == CHIP_NO_ERROR); + + const uint8_t expected[] = { + // clang-format off + 0x15, 0x36, 0x01, // Test overhead, Start Anonymous struct + Start 1 byte Tag Array + Tag (01) + 0x15, // Start anonymous struct + 0x35, 0x01, // Start 1 byte tag struct + Tag (01) + 0x24, 0x00, 0x99, // Tag (00) Value (1 byte uint) 0x99 (Attribute Version) + 0x37, 0x01, // Start 1 byte tag list + Tag (01) (Attribute Path) + 0x24, 0x02, 0x55, // Tag (02) Value (1 byte uint) 0x55 + 0x24, 0x03, 0xaa, // Tag (03) Value (1 byte uint) 0xaa + 0x24, 0x04, 0xcc, // Tag (04) Value (1 byte uint) 0xcc + 0x18, // End of container + 0x36, 0x02, // Start 1 byte tag array + Tag (02) (Attribute Value) + 0x15, // Start of struct + 0x2C, 0x07, 0x05, 0x68, 0x65, 0x6C, 0x6C, 0x6F, // "hello", with tag 07 and 1-byte length of 0x05 + 0x24, 0xFE, 0x01, // Fabric index, tag 254, value 1 + 0x18, // End of struct + 0x15, // Start of struct + 0x2C, 0x07, 0x03, 0x62, 0x79, 0x65, // "bye", with tag 07 and 1-byte length of 0x03 + 0x24, 0xFE, 0x02, // Fabric index, tag 254, value 2 + 0x18, // End of struct + 0x18, // End of array + 0x18, // End of container + 0x18, // End of container + // clang-format on + }; + VERIFY_BUFFER_STATE(aSuite, test, expected); +} + +void TestEncodeFabricFilteredListOfPreEncoded(nlTestSuite * aSuite, void * aContext) +{ + TestSetup test(aSuite, kTestFabricIndex); + + uint8_t buffers[2][128]; + std::optional values[2]; + + { + TLV::TLVWriter writer; + writer.Init(buffers[0]); + // Use a random tag that is not the right tag. + TLV::TLVType outerContainerType; + CHIP_ERROR err = + writer.StartContainer(TLV::ProfileTag(0x1234abcd, 0x5678fedc), TLV::kTLVType_Structure, outerContainerType); + NL_TEST_ASSERT(aSuite, err == CHIP_NO_ERROR); + + err = writer.PutString(TLV::ContextTag(7), "hello"); + NL_TEST_ASSERT(aSuite, err == CHIP_NO_ERROR); + + err = writer.Put(kFabricIndexTag, kTestFabricIndex); + NL_TEST_ASSERT(aSuite, err == CHIP_NO_ERROR); + + err = writer.EndContainer(outerContainerType); + NL_TEST_ASSERT(aSuite, err == CHIP_NO_ERROR); + + err = writer.Finalize(); + NL_TEST_ASSERT(aSuite, err == CHIP_NO_ERROR); + + values[0].emplace(ByteSpan(buffers[0], writer.GetLengthWritten())); + } + + { + TLV::TLVWriter writer; + writer.Init(buffers[1]); + // Use a random tag that is not the right tag. + TLV::TLVType outerContainerType; + CHIP_ERROR err = + writer.StartContainer(TLV::ProfileTag(0x1234abcd, 0x00010002), TLV::kTLVType_Structure, outerContainerType); + NL_TEST_ASSERT(aSuite, err == CHIP_NO_ERROR); + + err = writer.PutString(TLV::ContextTag(7), "bye"); + NL_TEST_ASSERT(aSuite, err == CHIP_NO_ERROR); + + err = writer.Put(kFabricIndexTag, static_cast(kTestFabricIndex + 1)); + NL_TEST_ASSERT(aSuite, err == CHIP_NO_ERROR); + + err = writer.EndContainer(outerContainerType); + NL_TEST_ASSERT(aSuite, err == CHIP_NO_ERROR); + + err = writer.Finalize(); + NL_TEST_ASSERT(aSuite, err == CHIP_NO_ERROR); + + values[1].emplace(ByteSpan(buffers[1], writer.GetLengthWritten())); + } + + CHIP_ERROR err = test.encoder.EncodeList([&values](const auto & encoder) { + for (auto & value : values) + { + ReturnErrorOnFailure(encoder.Encode(value.value())); + } + return CHIP_NO_ERROR; + }); + NL_TEST_ASSERT(aSuite, err == CHIP_NO_ERROR); + + const uint8_t expected[] = { + // clang-format off + 0x15, 0x36, 0x01, // Test overhead, Start Anonymous struct + Start 1 byte Tag Array + Tag (01) + 0x15, // Start anonymous struct + 0x35, 0x01, // Start 1 byte tag struct + Tag (01) + 0x24, 0x00, 0x99, // Tag (00) Value (1 byte uint) 0x99 (Attribute Version) + 0x37, 0x01, // Start 1 byte tag list + Tag (01) (Attribute Path) + 0x24, 0x02, 0x55, // Tag (02) Value (1 byte uint) 0x55 + 0x24, 0x03, 0xaa, // Tag (03) Value (1 byte uint) 0xaa + 0x24, 0x04, 0xcc, // Tag (04) Value (1 byte uint) 0xcc + 0x18, // End of container + 0x36, 0x02, // Start 1 byte tag array + Tag (02) (Attribute Value) + 0x15, // Start of struct + 0x2C, 0x07, 0x05, 0x68, 0x65, 0x6C, 0x6C, 0x6F, // "hello", with tag 07 and 1-byte length of 0x05 + 0x24, 0xFE, 0x01, // Fabric index, tag 254, value 1 + 0x18, // End of struct + // No second struct, because we are doing fabric filtering + 0x18, // End of array + 0x18, // End of container + 0x18, // End of container + // clang-format on + }; + VERIFY_BUFFER_STATE(aSuite, test, expected); +} + #undef VERIFY_BUFFER_STATE } // anonymous namespace @@ -569,6 +858,11 @@ const nlTest sTests[] = { NL_TEST_DEF("TestEncodeListChunking", TestEncodeListChunking), NL_TEST_DEF("TestEncodeListChunking2", TestEncodeListChunking2), NL_TEST_DEF("TestEncodeFabricScoped", TestEncodeFabricScoped), + NL_TEST_DEF("TestEncodePreEncoded", TestEncodePreEncoded), + NL_TEST_DEF("TestEncodeListOfPreEncoded", TestEncodeListOfPreEncoded), + NL_TEST_DEF("TestEncodeListFabricScopedPreEncoded", TestEncodeListOfPreEncoded), + NL_TEST_DEF("TestEncodeListOfFabricScopedPreEncoded", TestEncodeListOfFabricScopedPreEncoded), + NL_TEST_DEF("TestEncodeFabricFilteredListOfPreEncoded", TestEncodeFabricFilteredListOfPreEncoded), NL_TEST_SENTINEL() // clang-format on };