diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index c1073d90678ed2..dc152c1a714f19 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -204,11 +204,13 @@ jobs: run: | ./scripts/run_in_build_env.sh "./scripts/run_codegen_targets.sh out/sanitizers" - name: Clang-tidy validation + # NOTE: clang-tidy crashes on CodegenDataModel_Write due to Nullable/std::optional check. + # See https://github.com/llvm/llvm-project/issues/97426 run: | ./scripts/run_in_build_env.sh \ "./scripts/run-clang-tidy-on-compile-commands.py \ --compile-database out/sanitizers/compile_commands.json \ - --file-exclude-regex '/(repo|zzz_generated|lwip/standalone)/|-ReadImpl|-InvokeSubscribeImpl' \ + --file-exclude-regex '/(repo|zzz_generated|lwip/standalone)/|-ReadImpl|-InvokeSubscribeImpl|CodegenDataModel_Write' \ check \ " - name: Clean output @@ -422,10 +424,13 @@ jobs: run: | ./scripts/run_in_build_env.sh "./scripts/run_codegen_targets.sh out/default" - name: Clang-tidy validation + # NOTE: clang-tidy crashes on CodegenDataModel_Write due to Nullable/std::optional check. + # See https://github.com/llvm/llvm-project/issues/97426 run: | ./scripts/run_in_build_env.sh \ "./scripts/run-clang-tidy-on-compile-commands.py \ --compile-database out/default/compile_commands.json \ + --file-exclude-regex '/(repo|zzz_generated|lwip/standalone)/|CodegenDataModel_Write' \ check \ " - name: Uploading diagnostic logs diff --git a/src/app/codegen-data-model/BUILD.gn b/src/app/codegen-data-model/BUILD.gn index 5803f01a37778e..cc856539d001e7 100644 --- a/src/app/codegen-data-model/BUILD.gn +++ b/src/app/codegen-data-model/BUILD.gn @@ -20,8 +20,11 @@ import("//build_overrides/chip.gni") # # Use `model.gni` to get access to: # CodegenDataModel.cpp -# CodegenDataModel_Read.cpp # CodegenDataModel.h +# CodegenDataModel_Read.cpp +# CodegenDataModel_Write.cpp +# EmberMetadata.cpp +# EmberMetadata.h # # The above list of files exists to satisfy the "dependency linter" # since those files should technically be "visible to gn" even though we diff --git a/src/app/codegen-data-model/CodegenDataModel.cpp b/src/app/codegen-data-model/CodegenDataModel.cpp index ec7b13af357287..0cc9b42aaa64f7 100644 --- a/src/app/codegen-data-model/CodegenDataModel.cpp +++ b/src/app/codegen-data-model/CodegenDataModel.cpp @@ -231,13 +231,6 @@ bool CodegenDataModel::EmberCommandListIterator::Exists(const CommandId * list, return (*mCurrentHint == toCheck); } -CHIP_ERROR CodegenDataModel::WriteAttribute(const InteractionModel::WriteAttributeRequest & request, - AttributeValueDecoder & decoder) -{ - // TODO: this needs an implementation - return CHIP_ERROR_NOT_IMPLEMENTED; -} - CHIP_ERROR CodegenDataModel::Invoke(const InteractionModel::InvokeRequest & request, TLV::TLVReader & input_arguments, InteractionModel::InvokeReply & reply) { diff --git a/src/app/codegen-data-model/CodegenDataModel_Read.cpp b/src/app/codegen-data-model/CodegenDataModel_Read.cpp index 04265d37f8623f..867431084e51a2 100644 --- a/src/app/codegen-data-model/CodegenDataModel_Read.cpp +++ b/src/app/codegen-data-model/CodegenDataModel_Read.cpp @@ -14,7 +14,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -#include "lib/core/CHIPError.h" #include #include @@ -29,6 +28,7 @@ #include #include #include +#include #include #include #include @@ -49,56 +49,6 @@ namespace app { namespace { using namespace chip::app::Compatibility::Internal; -// Fetch the source for the given attribute path: either a cluster (for global ones) or attribute -// path. -// -// if returning a CHIP_ERROR, it will NEVER be CHIP_NO_ERROR. -std::variant -FindAttributeMetadata(const ConcreteAttributePath & aPath) -{ - for (auto & attr : GlobalAttributesNotInMetadata) - { - - if (attr == aPath.mAttributeId) - { - const EmberAfCluster * cluster = emberAfFindServerCluster(aPath.mEndpointId, aPath.mClusterId); - if (cluster == nullptr) - { - return (emberAfFindEndpointType(aPath.mEndpointId) == nullptr) ? CHIP_IM_GLOBAL_STATUS(UnsupportedEndpoint) - : CHIP_IM_GLOBAL_STATUS(UnsupportedCluster); - } - - return cluster; - } - } - const EmberAfAttributeMetadata * metadata = - emberAfLocateAttributeMetadata(aPath.mEndpointId, aPath.mClusterId, aPath.mAttributeId); - - if (metadata == nullptr) - { - const EmberAfEndpointType * type = emberAfFindEndpointType(aPath.mEndpointId); - if (type == nullptr) - { - return CHIP_IM_GLOBAL_STATUS(UnsupportedEndpoint); - } - - const EmberAfCluster * cluster = emberAfFindClusterInType(type, aPath.mClusterId, CLUSTER_MASK_SERVER); - if (cluster == nullptr) - { - return CHIP_IM_GLOBAL_STATUS(UnsupportedCluster); - } - - // Since we know the attribute is unsupported and the endpoint/cluster are - // OK, this is the only option left. - return CHIP_IM_GLOBAL_STATUS(UnsupportedAttribute); - } - - return metadata; -} - /// Attempts to read via an attribute access interface (AAI) /// /// If it returns a CHIP_ERROR, then this is a FINAL result (i.e. either failure or success). @@ -138,6 +88,13 @@ struct ShortPascalString { using LengthType = uint8_t; static constexpr LengthType kNullLength = 0xFF; + + static LengthType GetLength(const uint8_t * buffer) + { + // NOTE: we do NOT use emberAfLongStringLength because that will result in 0 length + // for null strings + return *buffer; + } }; /// Metadata of what a ember/pascal LONG string means (prepended by a u16 length) @@ -145,6 +102,13 @@ struct LongPascalString { using LengthType = uint16_t; static constexpr LengthType kNullLength = 0xFFFF; + + static LengthType GetLength(const uint8_t * buffer) + { + // NOTE: we do NOT use emberAfLongStringLength because that will result in 0 length + // for null strings + return Encoding::LittleEndian::Read16(buffer); + } }; // ember assumptions ... should just work @@ -157,12 +121,8 @@ static_assert(sizeof(LongPascalString::LengthType) == 2); template std::optional ExtractEmberString(ByteSpan data) { - typename ENCODING::LengthType len; - - // Ember storage format for pascal-prefix data is specifically "native byte order", - // hence the use of memcpy. - VerifyOrDie(sizeof(len) <= data.size()); - memcpy(&len, data.data(), sizeof(len)); + VerifyOrDie(sizeof(typename ENCODING::LengthType) <= data.size()); + auto len = ENCODING::GetLength(data.data()); if (len == ENCODING::kNullLength) { @@ -282,7 +242,7 @@ CHIP_ERROR EncodeEmberValue(ByteSpan data, const EmberAfAttributeMetadata * meta return EncodeStringLike(data, isNullable, encoder); default: ChipLogError(DataManagement, "Attribute type 0x%x not handled", static_cast(metadata->attributeType)); - return CHIP_IM_GLOBAL_STATUS(UnsupportedRead); + return CHIP_IM_GLOBAL_STATUS(Failure); } } @@ -311,21 +271,26 @@ CHIP_ERROR CodegenDataModel::ReadAttribute(const InteractionModel::ReadAttribute RequiredPrivilege::ForReadAttribute(request.path)); if (err != CHIP_NO_ERROR) { + ReturnErrorCodeIf(err != CHIP_ERROR_ACCESS_DENIED, err); + // Implementation of 8.4.3.2 of the spec for path expansion - if (request.path.mExpanded && (err == CHIP_ERROR_ACCESS_DENIED)) + if (request.path.mExpanded) { return CHIP_NO_ERROR; } - return err; + // access denied has a specific code for IM + return CHIP_IM_GLOBAL_STATUS(UnsupportedAccess); } } - auto metadata = FindAttributeMetadata(request.path); + auto metadata = Ember::FindAttributeMetadata(request.path); // Explicit failure in finding a suitable metadata if (const CHIP_ERROR * err = std::get_if(&metadata)) { - VerifyOrDie(*err != CHIP_NO_ERROR); + VerifyOrDie((*err == CHIP_IM_GLOBAL_STATUS(UnsupportedEndpoint)) || // + (*err == CHIP_IM_GLOBAL_STATUS(UnsupportedCluster)) || // + (*err == CHIP_IM_GLOBAL_STATUS(UnsupportedAttribute))); return *err; } diff --git a/src/app/codegen-data-model/CodegenDataModel_Write.cpp b/src/app/codegen-data-model/CodegenDataModel_Write.cpp new file mode 100644 index 00000000000000..b9a55114c0b30d --- /dev/null +++ b/src/app/codegen-data-model/CodegenDataModel_Write.cpp @@ -0,0 +1,386 @@ +/* + * 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 +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +namespace chip { +namespace app { +namespace { + +using namespace chip::app::Compatibility::Internal; + +/// Attempts to write via an attribute access interface (AAI) +/// +/// If it returns a CHIP_ERROR, then this is a FINAL result (i.e. either failure or success) +/// +/// If it returns std::nullopt, then there is no AAI to handle the given path +/// and processing should figure out the value otherwise (generally from other ember data) +std::optional TryWriteViaAccessInterface(const ConcreteAttributePath & path, AttributeAccessInterface * aai, + AttributeValueDecoder & decoder) +{ + // Processing can happen only if an attribute access interface actually exists.. + if (aai == nullptr) + { + return std::nullopt; + } + + CHIP_ERROR err = aai->Write(path, decoder); + + if (err != CHIP_NO_ERROR) + { + return std::make_optional(err); + } + + // If the decoder tried to decode, then a value should have been read for processing. + // - if decoding was done, assume DONE (i.e. final CHIP_NO_ERROR) + // - otherwise, if no decoding done, return that processing must continue via nullopt + return decoder.TriedDecode() ? std::make_optional(CHIP_NO_ERROR) : std::nullopt; +} + +/// Metadata of what a ember/pascal short string means (prepended by a u8 length) +struct ShortPascalString +{ + using LengthType = uint8_t; + static constexpr LengthType kNullLength = 0xFF; + + static void SetLength(uint8_t * buffer, LengthType value) { *buffer = value; } +}; + +/// Metadata of what a ember/pascal LONG string means (prepended by a u16 length) +struct LongPascalString +{ + using LengthType = uint16_t; + static constexpr LengthType kNullLength = 0xFFFF; + + // Encoding for ember string lengths is little-endian (see ember-strings.cpp) + static void SetLength(uint8_t * buffer, LengthType value) { Encoding::LittleEndian::Put16(buffer, value); } +}; + +// ember assumptions ... should just work +static_assert(sizeof(ShortPascalString::LengthType) == 1); +static_assert(sizeof(LongPascalString::LengthType) == 2); + +/// Convert the value stored in 'decoder' into an ember format span 'out' +/// +/// The value converted will be of type T (e.g. CharSpan or ByteSpan) and it will be converted +/// via the given ENCODING (i.e. ShortPascalString or LongPascalString) +/// +/// isNullable defines if the value of NULL is allowed to be converted. +template +CHIP_ERROR DecodeStringLikeIntoEmberBuffer(AttributeValueDecoder decoder, bool isNullable, MutableByteSpan out) +{ + T workingValue; + + if (isNullable) + { + typename DataModel::Nullable nullableWorkingValue; + ReturnErrorOnFailure(decoder.Decode(nullableWorkingValue)); + + if (nullableWorkingValue.IsNull()) + { + VerifyOrReturnError(out.size() >= sizeof(typename ENCODING::LengthType), CHIP_ERROR_BUFFER_TOO_SMALL); + ENCODING::SetLength(out.data(), ENCODING::kNullLength); + return CHIP_NO_ERROR; + } + + // continue encoding non-null value + workingValue = nullableWorkingValue.Value(); + } + else + { + ReturnErrorOnFailure(decoder.Decode(workingValue)); + } + + auto len = static_cast(workingValue.size()); + VerifyOrReturnError(out.size() >= sizeof(len) + len, CHIP_ERROR_BUFFER_TOO_SMALL); + + uint8_t * output_buffer = out.data(); + + ENCODING::SetLength(output_buffer, len); + output_buffer += sizeof(len); + + memcpy(output_buffer, workingValue.data(), workingValue.size()); + + return CHIP_NO_ERROR; +} + +/// Decodes a numeric data value of type T from the `decoder` into a ember-encoded buffer `out` +/// +/// isNullable defines if the value of NULL is allowed to be decoded. +template +CHIP_ERROR DecodeIntoEmberBuffer(AttributeValueDecoder & decoder, bool isNullable, MutableByteSpan out) +{ + using Traits = NumericAttributeTraits; + typename Traits::StorageType storageValue; + + if (isNullable) + { + DataModel::Nullable workingValue; + ReturnErrorOnFailure(decoder.Decode(workingValue)); + + if (workingValue.IsNull()) + { + Traits::SetNull(storageValue); + } + else + { + // This guards against trying to decode something that overlaps nullable, for example + // Nullable(0xFF) is not representable because 0xFF is the encoding of NULL in ember + // as well as odd-sized integers (e.g. full 32-bit value like 0x11223344 cannot be written + // to a 3-byte odd-sized integger). + VerifyOrReturnError(Traits::CanRepresentValue(isNullable, *workingValue), CHIP_ERROR_INVALID_ARGUMENT); + Traits::WorkingToStorage(*workingValue, storageValue); + } + + VerifyOrReturnError(out.size() >= sizeof(storageValue), CHIP_ERROR_INVALID_ARGUMENT); + } + else + { + typename Traits::WorkingType workingValue; + ReturnErrorOnFailure(decoder.Decode(workingValue)); + + Traits::WorkingToStorage(workingValue, storageValue); + + VerifyOrReturnError(out.size() >= sizeof(storageValue), CHIP_ERROR_INVALID_ARGUMENT); + + // Even non-nullable values may be outside range: e.g. odd-sized integers have working values + // that are larger than the storage values (e.g. a uint32_t being stored as a 3-byte integer) + VerifyOrReturnError(Traits::CanRepresentValue(isNullable, workingValue), CHIP_ERROR_INVALID_ARGUMENT); + } + + const uint8_t * data = Traits::ToAttributeStoreRepresentation(storageValue); + + // The decoding + ToAttributeStoreRepresentation will result in data being + // stored in native format/byteorder, suitable to directly be stored in the data store + memcpy(out.data(), data, sizeof(storageValue)); + + return CHIP_NO_ERROR; +} + +/// Read the data from "decoder" into an ember-formatted buffer "out" +/// +/// Uses the attribute `metadata` to determine how the data is to be encoded into out. +CHIP_ERROR DecodeValueIntoEmberBuffer(AttributeValueDecoder & decoder, const EmberAfAttributeMetadata * metadata, + MutableByteSpan out) +{ + VerifyOrReturnError(metadata != nullptr, CHIP_ERROR_INVALID_ARGUMENT); + + const bool isNullable = metadata->IsNullable(); + + switch (AttributeBaseType(metadata->attributeType)) + { + case ZCL_BOOLEAN_ATTRIBUTE_TYPE: // Boolean + return DecodeIntoEmberBuffer(decoder, isNullable, out); + case ZCL_INT8U_ATTRIBUTE_TYPE: // Unsigned 8-bit integer + return DecodeIntoEmberBuffer(decoder, isNullable, out); + case ZCL_INT16U_ATTRIBUTE_TYPE: // Unsigned 16-bit integer + return DecodeIntoEmberBuffer(decoder, isNullable, out); + case ZCL_INT24U_ATTRIBUTE_TYPE: // Unsigned 24-bit integer + return DecodeIntoEmberBuffer>(decoder, isNullable, out); + case ZCL_INT32U_ATTRIBUTE_TYPE: // Unsigned 32-bit integer + return DecodeIntoEmberBuffer(decoder, isNullable, out); + case ZCL_INT40U_ATTRIBUTE_TYPE: // Unsigned 40-bit integer + return DecodeIntoEmberBuffer>(decoder, isNullable, out); + case ZCL_INT48U_ATTRIBUTE_TYPE: // Unsigned 48-bit integer + return DecodeIntoEmberBuffer>(decoder, isNullable, out); + case ZCL_INT56U_ATTRIBUTE_TYPE: // Unsigned 56-bit integer + return DecodeIntoEmberBuffer>(decoder, isNullable, out); + case ZCL_INT64U_ATTRIBUTE_TYPE: // Unsigned 64-bit integer + return DecodeIntoEmberBuffer(decoder, isNullable, out); + case ZCL_INT8S_ATTRIBUTE_TYPE: // Signed 8-bit integer + return DecodeIntoEmberBuffer(decoder, isNullable, out); + case ZCL_INT16S_ATTRIBUTE_TYPE: // Signed 16-bit integer + return DecodeIntoEmberBuffer(decoder, isNullable, out); + case ZCL_INT24S_ATTRIBUTE_TYPE: // Signed 24-bit integer + return DecodeIntoEmberBuffer>(decoder, isNullable, out); + case ZCL_INT32S_ATTRIBUTE_TYPE: // Signed 32-bit integer + return DecodeIntoEmberBuffer(decoder, isNullable, out); + case ZCL_INT40S_ATTRIBUTE_TYPE: // Signed 40-bit integer + return DecodeIntoEmberBuffer>(decoder, isNullable, out); + case ZCL_INT48S_ATTRIBUTE_TYPE: // Signed 48-bit integer + return DecodeIntoEmberBuffer>(decoder, isNullable, out); + case ZCL_INT56S_ATTRIBUTE_TYPE: // Signed 56-bit integer + return DecodeIntoEmberBuffer>(decoder, isNullable, out); + case ZCL_INT64S_ATTRIBUTE_TYPE: // Signed 64-bit integer + return DecodeIntoEmberBuffer(decoder, isNullable, out); + case ZCL_SINGLE_ATTRIBUTE_TYPE: // 32-bit float + return DecodeIntoEmberBuffer(decoder, isNullable, out); + case ZCL_DOUBLE_ATTRIBUTE_TYPE: // 64-bit float + return DecodeIntoEmberBuffer(decoder, isNullable, out); + case ZCL_CHAR_STRING_ATTRIBUTE_TYPE: // Char string + return DecodeStringLikeIntoEmberBuffer(decoder, isNullable, out); + case ZCL_LONG_CHAR_STRING_ATTRIBUTE_TYPE: + return DecodeStringLikeIntoEmberBuffer(decoder, isNullable, out); + case ZCL_OCTET_STRING_ATTRIBUTE_TYPE: // Octet string + return DecodeStringLikeIntoEmberBuffer(decoder, isNullable, out); + case ZCL_LONG_OCTET_STRING_ATTRIBUTE_TYPE: + return DecodeStringLikeIntoEmberBuffer(decoder, isNullable, out); + default: + ChipLogError(DataManagement, "Attribute type 0x%x not handled", static_cast(metadata->attributeType)); + return CHIP_IM_GLOBAL_STATUS(Failure); + } +} + +} // namespace + +CHIP_ERROR CodegenDataModel::WriteAttribute(const InteractionModel::WriteAttributeRequest & request, + AttributeValueDecoder & decoder) +{ + ChipLogDetail(DataManagement, "Writing attribute: Cluster=" ChipLogFormatMEI " Endpoint=0x%x AttributeId=" ChipLogFormatMEI, + ChipLogValueMEI(request.path.mClusterId), request.path.mEndpointId, ChipLogValueMEI(request.path.mAttributeId)); + + // ACL check for non-internal requests + if (!request.operationFlags.Has(InteractionModel::OperationFlags::kInternal)) + { + ReturnErrorCodeIf(!request.subjectDescriptor.has_value(), CHIP_IM_GLOBAL_STATUS(UnsupportedAccess)); + + Access::RequestPath requestPath{ .cluster = request.path.mClusterId, .endpoint = request.path.mEndpointId }; + CHIP_ERROR err = Access::GetAccessControl().Check(*request.subjectDescriptor, requestPath, + RequiredPrivilege::ForWriteAttribute(request.path)); + + if (err != CHIP_NO_ERROR) + { + ReturnErrorCodeIf(err != CHIP_ERROR_ACCESS_DENIED, err); + + // TODO: when wildcard/group writes are supported, handle them to discard rather than fail with status + return CHIP_IM_GLOBAL_STATUS(UnsupportedAccess); + } + } + + auto metadata = Ember::FindAttributeMetadata(request.path); + + if (const CHIP_ERROR * err = std::get_if(&metadata)) + { + VerifyOrDie((*err == CHIP_IM_GLOBAL_STATUS(UnsupportedEndpoint)) || // + (*err == CHIP_IM_GLOBAL_STATUS(UnsupportedCluster)) || // + (*err == CHIP_IM_GLOBAL_STATUS(UnsupportedAttribute))); + return *err; + } + + const EmberAfAttributeMetadata ** attributeMetadata = std::get_if(&metadata); + + // All the global attributes that we do not have metadata for are + // read-only. Specifically only the following list-based attributes match the + // "global attributes not in metadata" (see GlobalAttributes.h :: GlobalAttributesNotInMetadata): + // - AttributeList + // - EventList + // - AcceptedCommands + // - GeneratedCommands + // + // Given the above, UnsupportedWrite should be correct (attempt to write to a read-only list) + bool isReadOnly = (attributeMetadata == nullptr) || (*attributeMetadata)->IsReadOnly(); + + // Internal is allowed to bypass timed writes and read-only. + if (!request.operationFlags.Has(InteractionModel::OperationFlags::kInternal)) + { + VerifyOrReturnError(!isReadOnly, CHIP_IM_GLOBAL_STATUS(UnsupportedWrite)); + + VerifyOrReturnError(!(*attributeMetadata)->MustUseTimedWrite() || + request.writeFlags.Has(InteractionModel::WriteFlags::kTimed), + CHIP_IM_GLOBAL_STATUS(NeedsTimedInteraction)); + } + + // Extra check: internal requests can bypass the read only check, however global attributes + // have no underlying storage, so write still cannot be done + VerifyOrReturnError(attributeMetadata != nullptr, CHIP_IM_GLOBAL_STATUS(UnsupportedWrite)); + + if (request.path.mDataVersion.HasValue()) + { + std::optional clusterInfo = GetClusterInfo(request.path); + if (!clusterInfo.has_value()) + { + ChipLogError(DataManagement, "Unable to get cluster info for Endpoint 0x%x, Cluster " ChipLogFormatMEI, + request.path.mEndpointId, ChipLogValueMEI(request.path.mClusterId)); + return CHIP_IM_GLOBAL_STATUS(DataVersionMismatch); + } + + if (request.path.mDataVersion.Value() != clusterInfo->dataVersion) + { + ChipLogError(DataManagement, "Write Version mismatch for Endpoint 0x%x, Cluster " ChipLogFormatMEI, + request.path.mEndpointId, ChipLogValueMEI(request.path.mClusterId)); + return CHIP_IM_GLOBAL_STATUS(DataVersionMismatch); + } + } + + AttributeAccessInterface * aai = GetAttributeAccessOverride(request.path.mEndpointId, request.path.mClusterId); + std::optional aai_result = TryWriteViaAccessInterface(request.path, aai, decoder); + if (aai_result.has_value()) + { + if (*aai_result == CHIP_NO_ERROR) + { + // TODO: change callbacks should likely be routed through the context `MarkDirty` only + // however for now this is called directly because ember code does this call + // inside emberAfWriteAttribute. + MatterReportingAttributeChangeCallback(request.path); + CurrentContext().dataModelChangeListener->MarkDirty(request.path); + } + return *aai_result; + } + + ReturnErrorOnFailure(DecodeValueIntoEmberBuffer(decoder, *attributeMetadata, gEmberAttributeIOBufferSpan)); + + Protocols::InteractionModel::Status status; + + if (request.operationFlags.Has(InteractionModel::OperationFlags::kInternal)) + { + // Internal requests use the non-External interface that has less enforcement + // than the external version (e.g. does not check/enforce writable settings, does not + // validate atribute types) - see attribute-table.h documentation for details. + status = emberAfWriteAttribute(request.path.mEndpointId, request.path.mClusterId, request.path.mAttributeId, + gEmberAttributeIOBufferSpan.data(), (*attributeMetadata)->attributeType); + } + else + { + status = emAfWriteAttributeExternal(request.path.mEndpointId, request.path.mClusterId, request.path.mAttributeId, + gEmberAttributeIOBufferSpan.data(), (*attributeMetadata)->attributeType); + } + + if (status != Protocols::InteractionModel::Status::Success) + { + return CHIP_ERROR_IM_GLOBAL_STATUS_VALUE(status); + } + + // TODO: this may need more refinement: + // - should internal requests be able to decide if something is marked dirty or not? + // - changes-omitted paths should not be marked dirty (ember is not aware of these) + CurrentContext().dataModelChangeListener->MarkDirty(request.path); + return CHIP_NO_ERROR; +} + +} // namespace app +} // namespace chip diff --git a/src/app/codegen-data-model/EmberMetadata.cpp b/src/app/codegen-data-model/EmberMetadata.cpp new file mode 100644 index 00000000000000..9114196a377906 --- /dev/null +++ b/src/app/codegen-data-model/EmberMetadata.cpp @@ -0,0 +1,79 @@ +/* + * 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 Ember { + +std::variant +FindAttributeMetadata(const ConcreteAttributePath & aPath) +{ + if (IsGlobalAttribute(aPath.mAttributeId)) + { + // Global list attribute check first: during path expansion a lot of attributes + // will actually be global attributes (so not too much of a performance hit) + for (auto & attr : GlobalAttributesNotInMetadata) + { + if (attr == aPath.mAttributeId) + { + const EmberAfCluster * cluster = emberAfFindServerCluster(aPath.mEndpointId, aPath.mClusterId); + if (cluster == nullptr) + { + return (emberAfFindEndpointType(aPath.mEndpointId) == nullptr) ? CHIP_IM_GLOBAL_STATUS(UnsupportedEndpoint) + : CHIP_IM_GLOBAL_STATUS(UnsupportedCluster); + } + + return cluster; + } + } + } + const EmberAfAttributeMetadata * metadata = + emberAfLocateAttributeMetadata(aPath.mEndpointId, aPath.mClusterId, aPath.mAttributeId); + + if (metadata == nullptr) + { + const EmberAfEndpointType * type = emberAfFindEndpointType(aPath.mEndpointId); + if (type == nullptr) + { + return CHIP_IM_GLOBAL_STATUS(UnsupportedEndpoint); + } + + const EmberAfCluster * cluster = emberAfFindClusterInType(type, aPath.mClusterId, CLUSTER_MASK_SERVER); + if (cluster == nullptr) + { + return CHIP_IM_GLOBAL_STATUS(UnsupportedCluster); + } + + // Since we know the attribute is unsupported and the endpoint/cluster are + // OK, this is the only option left. + return CHIP_IM_GLOBAL_STATUS(UnsupportedAttribute); + } + + return metadata; +} + +} // namespace Ember +} // namespace app +} // namespace chip diff --git a/src/app/codegen-data-model/EmberMetadata.h b/src/app/codegen-data-model/EmberMetadata.h new file mode 100644 index 00000000000000..f8f41312f1f7c9 --- /dev/null +++ b/src/app/codegen-data-model/EmberMetadata.h @@ -0,0 +1,46 @@ +/* + * 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. + */ +#pragma once + +#include +#include + +#include + +namespace chip { +namespace app { +namespace Ember { + +/// Fetch the source for the given attribute path: either a cluster (for global ones) or attribute +/// path. +/// +/// Possible return values: +/// - EmberAfCluster (NEVER null) - Only for GlobalAttributesNotInMetaData +/// - EmberAfAttributeMetadata (NEVER null) - if the attribute is known to ember datastore +/// - CHIP_ERROR, only specifically for unknown attributes, may only be one of: +/// - CHIP_IM_GLOBAL_STATUS(UnsupportedEndpoint); +/// - CHIP_IM_GLOBAL_STATUS(UnsupportedCluster); +/// - CHIP_IM_GLOBAL_STATUS(UnsupportedAttribute); +std::variant +FindAttributeMetadata(const ConcreteAttributePath & aPath); + +} // namespace Ember +} // namespace app +} // namespace chip diff --git a/src/app/codegen-data-model/model.gni b/src/app/codegen-data-model/model.gni index 3be7b2d2610513..a2abf6377c07a4 100644 --- a/src/app/codegen-data-model/model.gni +++ b/src/app/codegen-data-model/model.gni @@ -28,6 +28,9 @@ codegen_data_model_SOURCES = [ "${chip_root}/src/app/codegen-data-model/CodegenDataModel.h", "${chip_root}/src/app/codegen-data-model/CodegenDataModel.cpp", "${chip_root}/src/app/codegen-data-model/CodegenDataModel_Read.cpp", + "${chip_root}/src/app/codegen-data-model/CodegenDataModel_Write.cpp", + "${chip_root}/src/app/codegen-data-model/EmberMetadata.cpp", + "${chip_root}/src/app/codegen-data-model/EmberMetadata.h", ] codegen_data_model_PUBLIC_DEPS = [ diff --git a/src/app/codegen-data-model/tests/EmberReadWriteOverride.cpp b/src/app/codegen-data-model/tests/EmberReadWriteOverride.cpp index 8c65ee29b05556..d3c3b9975f8176 100644 --- a/src/app/codegen-data-model/tests/EmberReadWriteOverride.cpp +++ b/src/app/codegen-data-model/tests/EmberReadWriteOverride.cpp @@ -17,6 +17,8 @@ #include "EmberReadWriteOverride.h" #include +#include +#include using chip::Protocols::InteractionModel::Status; @@ -63,6 +65,11 @@ void SetEmberReadOutput(std::variant what) gEmberStatusCode = Status::InvalidAction; } +ByteSpan GetEmberBuffer() +{ + return ByteSpan(gEmberIoBuffer, gEmberIoBufferFill); +} + } // namespace Test } // namespace chip @@ -76,12 +83,47 @@ Status emAfReadOrWriteAttribute(const EmberAfAttributeSearchRecord * attRecord, return gEmberStatusCode; } - if (gEmberIoBufferFill > readLength) + if (write) + { + // copy over as much data as possible + // NOTE: we do NOT use (*metadata)->size since it is unclear if our mocks set that correctly + size_t len = std::min(sizeof(gEmberIoBuffer), readLength); + memcpy(gEmberIoBuffer, buffer, len); + gEmberIoBufferFill = len; + } + else { - ChipLogError(Test, "Internal TEST error: insufficient output buffer space."); - return Status::ResourceExhausted; + VerifyOrDie(gEmberIoBufferFill <= readLength); + memcpy(buffer, gEmberIoBuffer, gEmberIoBufferFill); } - memcpy(buffer, gEmberIoBuffer, gEmberIoBufferFill); return Status::Success; } + +Status emAfWriteAttributeExternal(chip::EndpointId endpoint, chip::ClusterId cluster, chip::AttributeId attributeID, + uint8_t * dataPtr, EmberAfAttributeType dataType) +{ + if (gEmberStatusCode != Status::Success) + { + return gEmberStatusCode; + } + + // ember here deduces the size of dataPtr. For testing however, we KNOW we read + // out of the ember IO buffer, so we try to use that + VerifyOrDie(dataPtr == chip::app::Compatibility::Internal::gEmberAttributeIOBufferSpan.data()); + + // In theory this should do type validation and sizes. This is NOT done for testing. + // copy over as much data as possible + // NOTE: we do NOT use (*metadata)->size since it is unclear if our mocks set that correctly + size_t len = std::min(sizeof(gEmberIoBuffer), chip::app::Compatibility::Internal::gEmberAttributeIOBufferSpan.size()); + memcpy(gEmberIoBuffer, dataPtr, len); + gEmberIoBufferFill = len; + + return Status::Success; +} + +Status emberAfWriteAttribute(chip::EndpointId endpoint, chip::ClusterId cluster, chip::AttributeId attributeID, uint8_t * dataPtr, + EmberAfAttributeType dataType) +{ + return emAfWriteAttributeExternal(endpoint, cluster, attributeID, dataPtr, dataType); +} diff --git a/src/app/codegen-data-model/tests/EmberReadWriteOverride.h b/src/app/codegen-data-model/tests/EmberReadWriteOverride.h index 527a6cfd0d18c7..5aeaeadc254086 100644 --- a/src/app/codegen-data-model/tests/EmberReadWriteOverride.h +++ b/src/app/codegen-data-model/tests/EmberReadWriteOverride.h @@ -29,5 +29,8 @@ namespace Test { /// It may return a value with success or some error. The byte span WILL BE COPIED. void SetEmberReadOutput(std::variant what); +/// Grab the data currently in the buffer +chip::ByteSpan GetEmberBuffer(); + } // namespace Test } // namespace chip diff --git a/src/app/codegen-data-model/tests/InteractionModelTemporaryOverrides.cpp b/src/app/codegen-data-model/tests/InteractionModelTemporaryOverrides.cpp index 868a26880d3ce7..f23702a4d34727 100644 --- a/src/app/codegen-data-model/tests/InteractionModelTemporaryOverrides.cpp +++ b/src/app/codegen-data-model/tests/InteractionModelTemporaryOverrides.cpp @@ -82,3 +82,9 @@ void DispatchSingleClusterCommand(const ConcreteCommandPath & aRequestCommandPat } // namespace app } // namespace chip + +void MatterReportingAttributeChangeCallback(const chip::app::ConcreteAttributePath & aPath) +{ + // TODO: should we add logic to track these calls for test purposes? +} + diff --git a/src/app/codegen-data-model/tests/TestCodegenModelViaMocks.cpp b/src/app/codegen-data-model/tests/TestCodegenModelViaMocks.cpp index df759e78666659..4cb6a48475382d 100644 --- a/src/app/codegen-data-model/tests/TestCodegenModelViaMocks.cpp +++ b/src/app/codegen-data-model/tests/TestCodegenModelViaMocks.cpp @@ -21,6 +21,7 @@ #include #include +#include #include #include #include @@ -29,19 +30,27 @@ #include #include #include +#include #include #include +#include +#include #include +#include #include #include #include #include #include #include +#include #include #include #include +#include +#include #include +#include #include #include @@ -57,6 +66,9 @@ namespace { constexpr FabricIndex kTestFabrixIndex = kMinValidFabricIndex; constexpr NodeId kTestNodeId = 0xFFFF'1234'ABCD'4321; +constexpr AttributeId kAttributeIdReadOnly = 0x3001; +constexpr AttributeId kAttributeIdTimedWrite = 0x3002; + constexpr EndpointId kEndpointIdThatIsMissing = kMockEndpointMin - 1; constexpr AttributeId kReadOnlyAttributeId = 0x5001; @@ -107,6 +119,57 @@ bool operator==(const Access::SubjectDescriptor & a, const Access::SubjectDescri return true; } +class TestDataModelChangeListener : public DataModelChangeListener +{ +public: + void MarkDirty(const ConcreteAttributePath & path) override { mDirtyList.push_back(path); } + + std::vector & DirtyList() { return mDirtyList; } + const std::vector & DirtyList() const { return mDirtyList; } + +private: + std::vector mDirtyList; +}; + +class TestEventGenerator : public EventsGenerator +{ + CHIP_ERROR GenerateEvent(EventLoggingDelegate * eventPayloadWriter, const EventOptions & options, + EventNumber & generatedEventNumber) override + { + return CHIP_ERROR_NOT_IMPLEMENTED; + } +}; + +class TestActionContext : public ActionContext +{ +public: + Messaging::ExchangeContext * CurrentExchange() override { return nullptr; } +}; + +class CodegenDataModelWithContext : public CodegenDataModel +{ +public: + CodegenDataModelWithContext() + { + InteractionModelContext context{ + .eventsGenerator = &mEventGenerator, + .dataModelChangeListener = &mChangeListener, + .actionContext = &mActionContext, + }; + + Startup(context); + } + ~CodegenDataModelWithContext() { Shutdown(); } + + TestDataModelChangeListener & ChangeListener() { return mChangeListener; } + const TestDataModelChangeListener & ChangeListener() const { return mChangeListener; } + +private: + TestEventGenerator mEventGenerator; + TestDataModelChangeListener mChangeListener; + TestActionContext mActionContext; +}; + class MockAccessControl : public Access::AccessControl::Delegate, public Access::AccessControl::DeviceTypeResolver { public: @@ -348,6 +411,10 @@ const MockNodeConfig gTestNodeConfig({ MOCK_ATTRIBUTE_CONFIG_NULLABLE(ZCL_IPV6ADR_ATTRIBUTE_TYPE), MOCK_ATTRIBUTE_CONFIG_NULLABLE(ZCL_IPV6PRE_ATTRIBUTE_TYPE), MOCK_ATTRIBUTE_CONFIG_NULLABLE(ZCL_HWADR_ATTRIBUTE_TYPE), + + // Special case handling + MockAttributeConfig(kAttributeIdReadOnly, ZCL_INT32S_ATTRIBUTE_TYPE, 0), + MockAttributeConfig(kAttributeIdTimedWrite, ZCL_INT32S_ATTRIBUTE_TYPE, ATTRIBUTE_MASK_WRITABLE | ATTRIBUTE_MASK_MUST_USE_TIMED_WRITE), }), }), }); @@ -375,7 +442,7 @@ CHIP_ERROR DecodeList(TLV::TLVReader & reader, std::vector & out) ReturnErrorOnFailure(err); T value; - ReturnErrorOnFailure(chip::app::DataModel::Decode(reader, value)); + ReturnErrorOnFailure(DataModel::Decode(reader, value)); out.emplace_back(std::move(value)); } } @@ -422,14 +489,58 @@ class StructAttributeAccessInterface : public AttributeAccessInterface return encoder.Encode(mData); } + CHIP_ERROR Write(const ConcreteDataAttributePath & path, AttributeValueDecoder & decoder) override + { + if (static_cast(path) != mPath) + { + // returning without trying to handle means "I do not handle this" + return CHIP_NO_ERROR; + } + + return decoder.Decode(mData); + } + void SetReturnedData(const Clusters::UnitTesting::Structs::SimpleStruct::Type & data) { mData = data; } - Clusters::UnitTesting::Structs::SimpleStruct::Type simpleStruct; + const Clusters::UnitTesting::Structs::SimpleStruct::Type & GetData() const { return mData; } private: ConcreteAttributePath mPath; Clusters::UnitTesting::Structs::SimpleStruct::Type mData; }; +class ErrorAccessInterface : public AttributeAccessInterface +{ +public: + ErrorAccessInterface(ConcreteAttributePath path, CHIP_ERROR err) : + AttributeAccessInterface(MakeOptional(path.mEndpointId), path.mClusterId), mPath(path), mError(err) + {} + ~ErrorAccessInterface() = default; + + CHIP_ERROR Read(const ConcreteReadAttributePath & path, AttributeValueEncoder & encoder) override + { + if (static_cast(path) != mPath) + { + // returning without trying to handle means "I do not handle this" + return CHIP_NO_ERROR; + } + return mError; + } + + CHIP_ERROR Write(const ConcreteDataAttributePath & path, AttributeValueDecoder & decoder) override + { + if (static_cast(path) != mPath) + { + // returning without trying to handle means "I do not handle this" + return CHIP_NO_ERROR; + } + return mError; + } + +private: + ConcreteAttributePath mPath; + CHIP_ERROR mError; +}; + class ListAttributeAcessInterface : public AttributeAccessInterface { public: @@ -458,7 +569,6 @@ class ListAttributeAcessInterface : public AttributeAccessInterface void SetReturnedData(const Clusters::UnitTesting::Structs::SimpleStruct::Type & data) { mData = data; } void SetReturnedDataCount(unsigned count) { mCount = count; } - Clusters::UnitTesting::Structs::SimpleStruct::Type simpleStruct; private: ConcreteAttributePath mPath; @@ -508,7 +618,7 @@ struct TestReadRequest request.path = path; } - std::unique_ptr StartEncoding(chip::app::InteractionModel::DataModel * model, + std::unique_ptr StartEncoding(InteractionModel::DataModel * model, AttributeEncodeState state = AttributeEncodeState()) { std::optional info = model->GetClusterInfo(request.path); @@ -538,11 +648,60 @@ struct TestReadRequest CHIP_ERROR FinishEncoding() { return encodedIBs.FinishEncoding(reportBuilder); } }; +// Sets up data for writing +struct TestWriteRequest +{ + InteractionModel::WriteAttributeRequest request; + uint8_t tlvBuffer[128] = { 0 }; + TLV::TLVReader + tlvReader; /// tlv reader used for the returned AttributeValueDecoder (since attributeValueDecoder uses references) + + TestWriteRequest(const Access::SubjectDescriptor & subject, const ConcreteDataAttributePath & path) + { + request.subjectDescriptor = subject; + request.path = path; + } + + template + TLV::TLVReader ReadEncodedValue(const T & value) + { + TLV::TLVWriter writer; + writer.Init(tlvBuffer); + + // Encoding is within a structure: + // - BEGIN_STRUCT + // - 1: ..... + // - END_STRUCT + TLV::TLVType outerContainerType; + VerifyOrDie(writer.StartContainer(TLV::AnonymousTag(), TLV::kTLVType_Structure, outerContainerType) == CHIP_NO_ERROR); + VerifyOrDie(DataModel::Encode(writer, TLV::ContextTag(1), value) == CHIP_NO_ERROR); + VerifyOrDie(writer.EndContainer(outerContainerType) == CHIP_NO_ERROR); + VerifyOrDie(writer.Finalize() == CHIP_NO_ERROR); + + TLV::TLVReader reader; + reader.Init(tlvBuffer); + + // position the reader inside the buffer, on the encoded value + VerifyOrDie(reader.Next() == CHIP_NO_ERROR); + VerifyOrDie(reader.EnterContainer(outerContainerType) == CHIP_NO_ERROR); + VerifyOrDie(reader.Next() == CHIP_NO_ERROR); + + return reader; + } + + template + AttributeValueDecoder DecoderFor(const T & value) + { + tlvReader = ReadEncodedValue(value); + return AttributeValueDecoder(tlvReader, request.subjectDescriptor.value_or(kDenySubjectDescriptor)); + } +}; + template void TestEmberScalarTypeRead(typename NumericAttributeTraits::WorkingType value) { UseMockNodeConfig config(gTestNodeConfig); - chip::app::CodegenDataModel model; + CodegenDataModelWithContext model; ScopedMockAccessControl accessControl; TestReadRequest testRequest( @@ -568,8 +727,7 @@ void TestEmberScalarTypeRead(typename NumericAttributeTraits::WorkingType val ASSERT_EQ(encodedData.attributePath, testRequest.request.path); typename NumericAttributeTraits::WorkingType actual; - ASSERT_EQ(chip::app::DataModel::Decode::WorkingType>(encodedData.dataReader, actual), - CHIP_NO_ERROR); + ASSERT_EQ(DataModel::Decode::WorkingType>(encodedData.dataReader, actual), CHIP_NO_ERROR); ASSERT_EQ(actual, value); } @@ -577,7 +735,7 @@ template void TestEmberScalarNullRead() { UseMockNodeConfig config(gTestNodeConfig); - chip::app::CodegenDataModel model; + CodegenDataModelWithContext model; ScopedMockAccessControl accessControl; TestReadRequest testRequest( @@ -601,17 +759,133 @@ void TestEmberScalarNullRead() DecodedAttributeData & encodedData = attribute_data[0]; ASSERT_EQ(encodedData.attributePath, testRequest.request.path); - chip::app::DataModel::Nullable::WorkingType> actual; - ASSERT_EQ(chip::app::DataModel::Decode(encodedData.dataReader, actual), CHIP_NO_ERROR); + DataModel::Nullable::WorkingType> actual; + ASSERT_EQ(DataModel::Decode(encodedData.dataReader, actual), CHIP_NO_ERROR); ASSERT_TRUE(actual.IsNull()); } +template +void TestEmberScalarTypeWrite(const typename NumericAttributeTraits::WorkingType value) +{ + UseMockNodeConfig config(gTestNodeConfig); + CodegenDataModelWithContext model; + ScopedMockAccessControl accessControl; + + // non-nullable test + { + TestWriteRequest test( + kAdminSubjectDescriptor, + ConcreteAttributePath(kMockEndpoint3, MockClusterId(4), MOCK_ATTRIBUTE_ID_FOR_NON_NULLABLE_TYPE(ZclType))); + AttributeValueDecoder decoder = test.DecoderFor(value); + + // write should succeed + ASSERT_EQ(model.WriteAttribute(test.request, decoder), CHIP_NO_ERROR); + + // Validate data after write + chip::ByteSpan writtenData = Test::GetEmberBuffer(); + + typename NumericAttributeTraits::StorageType storage; + ASSERT_GE(writtenData.size(), sizeof(storage)); + memcpy(&storage, writtenData.data(), sizeof(storage)); + typename NumericAttributeTraits::WorkingType actual = NumericAttributeTraits::StorageToWorking(storage); + + EXPECT_EQ(actual, value); + ASSERT_EQ(model.ChangeListener().DirtyList().size(), 1u); + EXPECT_EQ(model.ChangeListener().DirtyList()[0], test.request.path); + + // reset for the next test + model.ChangeListener().DirtyList().clear(); + } + + // nullable test + { + TestWriteRequest test( + kAdminSubjectDescriptor, + ConcreteAttributePath(kMockEndpoint3, MockClusterId(4), MOCK_ATTRIBUTE_ID_FOR_NULLABLE_TYPE(ZclType))); + AttributeValueDecoder decoder = test.DecoderFor(value); + + // write should succeed + ASSERT_EQ(model.WriteAttribute(test.request, decoder), CHIP_NO_ERROR); + + // Validate data after write + chip::ByteSpan writtenData = Test::GetEmberBuffer(); + + typename NumericAttributeTraits::StorageType storage; + ASSERT_GE(writtenData.size(), sizeof(storage)); + memcpy(&storage, writtenData.data(), sizeof(storage)); + typename NumericAttributeTraits::WorkingType actual = NumericAttributeTraits::StorageToWorking(storage); + + ASSERT_EQ(actual, value); + ASSERT_EQ(model.ChangeListener().DirtyList().size(), 1u); + EXPECT_EQ(model.ChangeListener().DirtyList()[0], test.request.path); + } +} + +template +void TestEmberScalarNullWrite() +{ + UseMockNodeConfig config(gTestNodeConfig); + CodegenDataModelWithContext model; + ScopedMockAccessControl accessControl; + + TestWriteRequest test(kAdminSubjectDescriptor, + ConcreteAttributePath(kMockEndpoint3, MockClusterId(4), MOCK_ATTRIBUTE_ID_FOR_NULLABLE_TYPE(ZclType))); + + using NumericType = NumericAttributeTraits; + using NullableType = DataModel::Nullable; + AttributeValueDecoder decoder = test.DecoderFor(NullableType()); + + // write should succeed + ASSERT_EQ(model.WriteAttribute(test.request, decoder), CHIP_NO_ERROR); + + // Validate data after write + chip::ByteSpan writtenData = Test::GetEmberBuffer(); + + using Traits = NumericAttributeTraits; + + typename Traits::StorageType storage; + ASSERT_GE(writtenData.size(), sizeof(storage)); + memcpy(&storage, writtenData.data(), sizeof(storage)); + ASSERT_TRUE(Traits::IsNullValue(storage)); +} + +template +void TestEmberScalarTypeWriteNullValueToNullable() +{ + UseMockNodeConfig config(gTestNodeConfig); + CodegenDataModelWithContext model; + ScopedMockAccessControl accessControl; + + TestWriteRequest test( + kAdminSubjectDescriptor, + ConcreteAttributePath(kMockEndpoint3, MockClusterId(4), MOCK_ATTRIBUTE_ID_FOR_NON_NULLABLE_TYPE(ZclType))); + + using NumericType = NumericAttributeTraits; + using NullableType = DataModel::Nullable; + AttributeValueDecoder decoder = test.DecoderFor(NullableType()); + + // write should fail: we are trying to write null + ASSERT_EQ(model.WriteAttribute(test.request, decoder), CHIP_ERROR_WRONG_TLV_TYPE); +} + +uint16_t ReadLe16(const void * buffer) +{ + const uint8_t * p = reinterpret_cast(buffer); + return chip::Encoding::LittleEndian::Read16(p); +} + +void WriteLe16(void * buffer, uint16_t value) +{ + uint8_t * p = reinterpret_cast(buffer); + chip::Encoding::LittleEndian::Write16(p, value); +} + } // namespace TEST(TestCodegenModelViaMocks, IterateOverEndpoints) { UseMockNodeConfig config(gTestNodeConfig); - chip::app::CodegenDataModel model; + CodegenDataModelWithContext model; // This iteration relies on the hard-coding that occurs when mock_ember is used EXPECT_EQ(model.FirstEndpoint(), kMockEndpoint1); @@ -639,7 +913,7 @@ TEST(TestCodegenModelViaMocks, IterateOverEndpoints) TEST(TestCodegenModelViaMocks, IterateOverClusters) { UseMockNodeConfig config(gTestNodeConfig); - chip::app::CodegenDataModel model; + CodegenDataModelWithContext model; chip::Test::ResetVersion(); @@ -702,7 +976,7 @@ TEST(TestCodegenModelViaMocks, GetClusterInfo) { UseMockNodeConfig config(gTestNodeConfig); - chip::app::CodegenDataModel model; + CodegenDataModelWithContext model; chip::Test::ResetVersion(); @@ -727,7 +1001,7 @@ TEST(TestCodegenModelViaMocks, GetClusterInfo) TEST(TestCodegenModelViaMocks, IterateOverAttributes) { UseMockNodeConfig config(gTestNodeConfig); - chip::app::CodegenDataModel model; + CodegenDataModelWithContext model; // invalid paths should return in "no more data" ASSERT_FALSE(model.FirstAttribute(ConcreteClusterPath(kEndpointIdThatIsMissing, MockClusterId(1))).path.HasValidIds()); @@ -798,7 +1072,7 @@ TEST(TestCodegenModelViaMocks, IterateOverAttributes) TEST(TestCodegenModelViaMocks, GetAttributeInfo) { UseMockNodeConfig config(gTestNodeConfig); - chip::app::CodegenDataModel model; + CodegenDataModelWithContext model; // various non-existent or invalid paths should return no info data ASSERT_FALSE( @@ -838,7 +1112,7 @@ TEST(TestCodegenModelViaMocks, GetAttributeInfo) TEST(TestCodegenModelViaMocks, GlobalAttributeInfo) { UseMockNodeConfig config(gTestNodeConfig); - chip::app::CodegenDataModel model; + CodegenDataModelWithContext model; std::optional info = model.GetAttributeInfo( ConcreteAttributePath(kMockEndpoint1, MockClusterId(1), Clusters::Globals::Attributes::GeneratedCommandList::Id)); @@ -853,7 +1127,7 @@ TEST(TestCodegenModelViaMocks, GlobalAttributeInfo) TEST(TestCodegenModelViaMocks, IterateOverAcceptedCommands) { UseMockNodeConfig config(gTestNodeConfig); - chip::app::CodegenDataModel model; + CodegenDataModelWithContext model; // invalid paths should return in "no more data" ASSERT_FALSE(model.FirstAcceptedCommand(ConcreteClusterPath(kEndpointIdThatIsMissing, MockClusterId(1))).path.HasValidIds()); @@ -918,7 +1192,7 @@ TEST(TestCodegenModelViaMocks, IterateOverAcceptedCommands) TEST(TestCodegenModelViaMocks, AcceptedCommandInfo) { UseMockNodeConfig config(gTestNodeConfig); - chip::app::CodegenDataModel model; + CodegenDataModelWithContext model; // invalid paths should return in "no more data" ASSERT_FALSE(model.GetAcceptedCommandInfo(ConcreteCommandPath(kEndpointIdThatIsMissing, MockClusterId(1), 1)).has_value()); @@ -950,7 +1224,7 @@ TEST(TestCodegenModelViaMocks, AcceptedCommandInfo) TEST(TestCodegenModelViaMocks, IterateOverGeneratedCommands) { UseMockNodeConfig config(gTestNodeConfig); - chip::app::CodegenDataModel model; + CodegenDataModelWithContext model; // invalid paths should return in "no more data" ASSERT_FALSE(model.FirstGeneratedCommand(ConcreteClusterPath(kEndpointIdThatIsMissing, MockClusterId(1))).HasValidIds()); @@ -1009,20 +1283,20 @@ TEST(TestCodegenModelViaMocks, IterateOverGeneratedCommands) TEST(TestCodegenModelViaMocks, EmberAttributeReadAclDeny) { UseMockNodeConfig config(gTestNodeConfig); - chip::app::CodegenDataModel model; + CodegenDataModelWithContext model; ScopedMockAccessControl accessControl; TestReadRequest testRequest(kDenySubjectDescriptor, ConcreteAttributePath(kMockEndpoint1, MockClusterId(1), MockAttributeId(10))); std::unique_ptr encoder = testRequest.StartEncoding(&model); - ASSERT_EQ(model.ReadAttribute(testRequest.request, *encoder), CHIP_ERROR_ACCESS_DENIED); + ASSERT_EQ(model.ReadAttribute(testRequest.request, *encoder), CHIP_IM_GLOBAL_STATUS(UnsupportedAccess)); } TEST(TestCodegenModelViaMocks, ReadForInvalidGlobalAttributePath) { UseMockNodeConfig config(gTestNodeConfig); - chip::app::CodegenDataModel model; + CodegenDataModelWithContext model; ScopedMockAccessControl accessControl; { @@ -1043,7 +1317,7 @@ TEST(TestCodegenModelViaMocks, ReadForInvalidGlobalAttributePath) TEST(TestCodegenModelViaMocks, EmberAttributeInvalidRead) { UseMockNodeConfig config(gTestNodeConfig); - chip::app::CodegenDataModel model; + CodegenDataModelWithContext model; ScopedMockAccessControl accessControl; // Invalid attribute @@ -1077,7 +1351,7 @@ TEST(TestCodegenModelViaMocks, EmberAttributeInvalidRead) TEST(TestCodegenModelViaMocks, EmberAttributePathExpansionAccessDeniedRead) { UseMockNodeConfig config(gTestNodeConfig); - chip::app::CodegenDataModel model; + CodegenDataModelWithContext model; ScopedMockAccessControl accessControl; TestReadRequest testRequest(kDenySubjectDescriptor, @@ -1095,7 +1369,7 @@ TEST(TestCodegenModelViaMocks, EmberAttributePathExpansionAccessDeniedRead) TEST(TestCodegenModelViaMocks, AccessInterfaceUnsupportedRead) { UseMockNodeConfig config(gTestNodeConfig); - chip::app::CodegenDataModel model; + CodegenDataModelWithContext model; ScopedMockAccessControl accessControl; const ConcreteAttributePath kTestPath(kMockEndpoint3, MockClusterId(4), @@ -1199,7 +1473,7 @@ TEST(TestCodegenModelViaMocks, EmberAttributeReadNulls) TEST(TestCodegenModelViaMocks, EmberAttributeReadErrorReading) { UseMockNodeConfig config(gTestNodeConfig); - chip::app::CodegenDataModel model; + CodegenDataModelWithContext model; ScopedMockAccessControl accessControl; { @@ -1227,12 +1501,15 @@ TEST(TestCodegenModelViaMocks, EmberAttributeReadErrorReading) std::unique_ptr encoder = testRequest.StartEncoding(&model); ASSERT_EQ(model.ReadAttribute(testRequest.request, *encoder), CHIP_IM_GLOBAL_STATUS(Busy)); } + + // reset things to success to not affect other tests + chip::Test::SetEmberReadOutput(ByteSpan()); } TEST(TestCodegenModelViaMocks, EmberAttributeReadNullOctetString) { UseMockNodeConfig config(gTestNodeConfig); - chip::app::CodegenDataModel model; + CodegenDataModelWithContext model; ScopedMockAccessControl accessControl; TestReadRequest testRequest(kAdminSubjectDescriptor, @@ -1259,15 +1536,15 @@ TEST(TestCodegenModelViaMocks, EmberAttributeReadNullOctetString) // data element should be null for the given 0xFFFF length ASSERT_EQ(encodedData.dataReader.GetType(), TLV::kTLVType_Null); - chip::app::DataModel::Nullable actual; - ASSERT_EQ(chip::app::DataModel::Decode(encodedData.dataReader, actual), CHIP_NO_ERROR); + DataModel::Nullable actual; + ASSERT_EQ(DataModel::Decode(encodedData.dataReader, actual), CHIP_NO_ERROR); ASSERT_TRUE(actual.IsNull()); } TEST(TestCodegenModelViaMocks, EmberAttributeReadOctetString) { UseMockNodeConfig config(gTestNodeConfig); - chip::app::CodegenDataModel model; + CodegenDataModelWithContext model; ScopedMockAccessControl accessControl; TestReadRequest testRequest( @@ -1277,9 +1554,8 @@ TEST(TestCodegenModelViaMocks, EmberAttributeReadOctetString) // NOTE: This is a pascal string, so actual data is "test" // the longer encoding is to make it clear we do not encode the overflow - char data[] = "\0\0testing here with overflow"; - uint16_t len = 4; - memcpy(data, &len, sizeof(uint16_t)); + char data[] = "\0\0testing here with overflow"; + WriteLe16(data, 4); chip::Test::SetEmberReadOutput(ByteSpan(reinterpret_cast(data), sizeof(data))); // Actual read via an encoder @@ -1307,7 +1583,7 @@ TEST(TestCodegenModelViaMocks, EmberAttributeReadOctetString) TEST(TestCodegenModelViaMocks, EmberAttributeReadLongOctetString) { UseMockNodeConfig config(gTestNodeConfig); - chip::app::CodegenDataModel model; + CodegenDataModelWithContext model; ScopedMockAccessControl accessControl; TestReadRequest testRequest(kAdminSubjectDescriptor, @@ -1344,7 +1620,7 @@ TEST(TestCodegenModelViaMocks, EmberAttributeReadLongOctetString) TEST(TestCodegenModelViaMocks, EmberAttributeReadShortString) { UseMockNodeConfig config(gTestNodeConfig); - chip::app::CodegenDataModel model; + CodegenDataModelWithContext model; ScopedMockAccessControl accessControl; TestReadRequest testRequest(kAdminSubjectDescriptor, @@ -1353,9 +1629,8 @@ TEST(TestCodegenModelViaMocks, EmberAttributeReadShortString) // NOTE: This is a pascal string, so actual data is "abcde" // the longer encoding is to make it clear we do not encode the overflow - char data[] = "\0abcdef...this is the alphabet"; - uint16_t len = 5; - memcpy(data, &len, sizeof(uint8_t)); + char data[] = "\0abcdef...this is the alphabet"; + *data = 5; chip::Test::SetEmberReadOutput(ByteSpan(reinterpret_cast(data), sizeof(data))); // Actual read via an encoder @@ -1381,7 +1656,7 @@ TEST(TestCodegenModelViaMocks, EmberAttributeReadShortString) TEST(TestCodegenModelViaMocks, EmberAttributeReadLongString) { UseMockNodeConfig config(gTestNodeConfig); - chip::app::CodegenDataModel model; + CodegenDataModelWithContext model; ScopedMockAccessControl accessControl; TestReadRequest testRequest( @@ -1391,9 +1666,8 @@ TEST(TestCodegenModelViaMocks, EmberAttributeReadLongString) // NOTE: This is a pascal string, so actual data is "abcde" // the longer encoding is to make it clear we do not encode the overflow - char data[] = "\0\0abcdef...this is the alphabet"; - uint16_t len = 5; - memcpy(data, &len, sizeof(uint16_t)); + char data[] = "\0\0abcdef...this is the alphabet"; + WriteLe16(data, 5); chip::Test::SetEmberReadOutput(ByteSpan(reinterpret_cast(data), sizeof(data))); // Actual read via an encoder @@ -1419,7 +1693,7 @@ TEST(TestCodegenModelViaMocks, EmberAttributeReadLongString) TEST(TestCodegenModelViaMocks, AttributeAccessInterfaceStructRead) { UseMockNodeConfig config(gTestNodeConfig); - chip::app::CodegenDataModel model; + CodegenDataModelWithContext model; ScopedMockAccessControl accessControl; const ConcreteAttributePath kStructPath(kMockEndpoint3, MockClusterId(4), @@ -1450,7 +1724,7 @@ TEST(TestCodegenModelViaMocks, AttributeAccessInterfaceStructRead) ASSERT_EQ(encodedData.dataReader.GetType(), TLV::kTLVType_Structure); Clusters::UnitTesting::Structs::SimpleStruct::DecodableType actual; - ASSERT_EQ(chip::app::DataModel::Decode(encodedData.dataReader, actual), CHIP_NO_ERROR); + ASSERT_EQ(DataModel::Decode(encodedData.dataReader, actual), CHIP_NO_ERROR); ASSERT_EQ(actual.a, 123); ASSERT_EQ(actual.b, true); @@ -1459,10 +1733,25 @@ TEST(TestCodegenModelViaMocks, AttributeAccessInterfaceStructRead) ASSERT_TRUE(actual.e.data_equal("foo"_span)); } +TEST(TestCodegenModelViaMocks, AttributeAccessInterfaceReadError) +{ + UseMockNodeConfig config(gTestNodeConfig); + CodegenDataModelWithContext model; + ScopedMockAccessControl accessControl; + + const ConcreteAttributePath kStructPath(kMockEndpoint3, MockClusterId(4), + MOCK_ATTRIBUTE_ID_FOR_NON_NULLABLE_TYPE(ZCL_STRUCT_ATTRIBUTE_TYPE)); + + TestReadRequest testRequest(kAdminSubjectDescriptor, kStructPath); + RegisteredAttributeAccessInterface aai(kStructPath, CHIP_ERROR_KEY_NOT_FOUND); + std::unique_ptr encoder = testRequest.StartEncoding(&model); + ASSERT_EQ(model.ReadAttribute(testRequest.request, *encoder), CHIP_ERROR_KEY_NOT_FOUND); +} + TEST(TestCodegenModelViaMocks, AttributeAccessInterfaceListRead) { UseMockNodeConfig config(gTestNodeConfig); - chip::app::CodegenDataModel model; + CodegenDataModelWithContext model; ScopedMockAccessControl accessControl; const ConcreteAttributePath kStructPath(kMockEndpoint3, MockClusterId(4), @@ -1514,7 +1803,7 @@ TEST(TestCodegenModelViaMocks, AttributeAccessInterfaceListRead) TEST(TestCodegenModelViaMocks, AttributeAccessInterfaceListOverflowRead) { UseMockNodeConfig config(gTestNodeConfig); - chip::app::CodegenDataModel model; + CodegenDataModelWithContext model; ScopedMockAccessControl accessControl; const ConcreteAttributePath kStructPath(kMockEndpoint3, MockClusterId(4), @@ -1573,7 +1862,7 @@ TEST(TestCodegenModelViaMocks, AttributeAccessInterfaceListOverflowRead) TEST(TestCodegenModelViaMocks, AttributeAccessInterfaceListIncrementalRead) { UseMockNodeConfig config(gTestNodeConfig); - chip::app::CodegenDataModel model; + CodegenDataModelWithContext model; ScopedMockAccessControl accessControl; const ConcreteAttributePath kStructPath(kMockEndpoint3, MockClusterId(4), @@ -1622,7 +1911,7 @@ TEST(TestCodegenModelViaMocks, AttributeAccessInterfaceListIncrementalRead) ASSERT_EQ(encodedData.dataReader.GetType(), TLV::kTLVType_Structure); Clusters::UnitTesting::Structs::SimpleStruct::DecodableType actual; - ASSERT_EQ(chip::app::DataModel::Decode(encodedData.dataReader, actual), CHIP_NO_ERROR); + ASSERT_EQ(DataModel::Decode(encodedData.dataReader, actual), CHIP_NO_ERROR); ASSERT_EQ(actual.a, static_cast((i + kEncodeIndexStart) & 0xFF)); ASSERT_EQ(actual.b, true); @@ -1635,7 +1924,7 @@ TEST(TestCodegenModelViaMocks, AttributeAccessInterfaceListIncrementalRead) TEST(TestCodegenModelViaMocks, ReadGlobalAttributeAttributeList) { UseMockNodeConfig config(gTestNodeConfig); - chip::app::CodegenDataModel model; + CodegenDataModelWithContext model; ScopedMockAccessControl accessControl; TestReadRequest testRequest(kAdminSubjectDescriptor, @@ -1687,3 +1976,486 @@ TEST(TestCodegenModelViaMocks, ReadGlobalAttributeAttributeList) EXPECT_EQ(items[i], expected[i]); } } + +TEST(TestCodegenModelViaMocks, EmberAttributeWriteAclDeny) +{ + UseMockNodeConfig config(gTestNodeConfig); + CodegenDataModelWithContext model; + ScopedMockAccessControl accessControl; + + TestWriteRequest test(kDenySubjectDescriptor, ConcreteDataAttributePath(kMockEndpoint1, MockClusterId(1), MockAttributeId(10))); + AttributeValueDecoder decoder = test.DecoderFor(1234); + + ASSERT_EQ(model.WriteAttribute(test.request, decoder), CHIP_IM_GLOBAL_STATUS(UnsupportedAccess)); + ASSERT_TRUE(model.ChangeListener().DirtyList().empty()); +} + +TEST(TestCodegenModelViaMocks, EmberAttributeWriteBasicTypes) +{ + TestEmberScalarTypeWrite(0x12); + TestEmberScalarTypeWrite(0x1234); + TestEmberScalarTypeWrite, ZCL_INT24U_ATTRIBUTE_TYPE>(0x112233); + TestEmberScalarTypeWrite(0x11223344); + TestEmberScalarTypeWrite, ZCL_INT40U_ATTRIBUTE_TYPE>(0x1122334455ULL); + TestEmberScalarTypeWrite, ZCL_INT48U_ATTRIBUTE_TYPE>(0x112233445566ULL); + TestEmberScalarTypeWrite, ZCL_INT56U_ATTRIBUTE_TYPE>(0x11223344556677ULL); + TestEmberScalarTypeWrite(0x1122334455667788ULL); + + TestEmberScalarTypeWrite(-10); + TestEmberScalarTypeWrite(-123); + TestEmberScalarTypeWrite, ZCL_INT24S_ATTRIBUTE_TYPE>(-1234); + TestEmberScalarTypeWrite(-12345); + TestEmberScalarTypeWrite, ZCL_INT40S_ATTRIBUTE_TYPE>(-123456); + TestEmberScalarTypeWrite, ZCL_INT48S_ATTRIBUTE_TYPE>(-1234567); + TestEmberScalarTypeWrite, ZCL_INT56S_ATTRIBUTE_TYPE>(-12345678); + TestEmberScalarTypeWrite(-123456789); + + TestEmberScalarTypeWrite(true); + TestEmberScalarTypeWrite(false); + TestEmberScalarTypeWrite(0.625); + TestEmberScalarTypeWrite(0.625); +} + +TEST(TestCodegenModelViaMocks, EmberAttributeWriteInvalidValueToNullable) +{ + TestEmberScalarTypeWriteNullValueToNullable(); + TestEmberScalarTypeWriteNullValueToNullable(); + TestEmberScalarTypeWriteNullValueToNullable, ZCL_INT24U_ATTRIBUTE_TYPE>(); + TestEmberScalarTypeWriteNullValueToNullable(); + TestEmberScalarTypeWriteNullValueToNullable, ZCL_INT40U_ATTRIBUTE_TYPE>(); + TestEmberScalarTypeWriteNullValueToNullable, ZCL_INT48U_ATTRIBUTE_TYPE>(); + TestEmberScalarTypeWriteNullValueToNullable, ZCL_INT56U_ATTRIBUTE_TYPE>(); + TestEmberScalarTypeWriteNullValueToNullable(); + + TestEmberScalarTypeWriteNullValueToNullable(); + TestEmberScalarTypeWriteNullValueToNullable(); + TestEmberScalarTypeWriteNullValueToNullable, ZCL_INT24S_ATTRIBUTE_TYPE>(); + TestEmberScalarTypeWriteNullValueToNullable(); + TestEmberScalarTypeWriteNullValueToNullable, ZCL_INT40S_ATTRIBUTE_TYPE>(); + TestEmberScalarTypeWriteNullValueToNullable, ZCL_INT48S_ATTRIBUTE_TYPE>(); + TestEmberScalarTypeWriteNullValueToNullable, ZCL_INT56S_ATTRIBUTE_TYPE>(); + TestEmberScalarTypeWriteNullValueToNullable(); + + TestEmberScalarTypeWriteNullValueToNullable(); + TestEmberScalarTypeWriteNullValueToNullable(); + TestEmberScalarTypeWriteNullValueToNullable(); + TestEmberScalarTypeWriteNullValueToNullable(); +} + +TEST(TestCodegenModelViaMocks, EmberTestWriteReservedNullPlaceholderToNullable) +{ + UseMockNodeConfig config(gTestNodeConfig); + CodegenDataModelWithContext model; + ScopedMockAccessControl accessControl; + + TestWriteRequest test( + kAdminSubjectDescriptor, + ConcreteAttributePath(kMockEndpoint3, MockClusterId(4), MOCK_ATTRIBUTE_ID_FOR_NULLABLE_TYPE(ZCL_INT32U_ATTRIBUTE_TYPE))); + + using NumericType = NumericAttributeTraits; + using NullableType = DataModel::Nullable; + AttributeValueDecoder decoder = test.DecoderFor(0xFFFFFFFF); + + // write should fail: we are trying to write null which is out of range + ASSERT_EQ(model.WriteAttribute(test.request, decoder), CHIP_IM_GLOBAL_STATUS(ConstraintError)); +} + +TEST(TestCodegenModelViaMocks, EmberTestWriteOutOfRepresentableRangeOddIntegerNonNullable) +{ + UseMockNodeConfig config(gTestNodeConfig); + CodegenDataModelWithContext model; + ScopedMockAccessControl accessControl; + + TestWriteRequest test(kAdminSubjectDescriptor, + ConcreteAttributePath(kMockEndpoint3, MockClusterId(4), + MOCK_ATTRIBUTE_ID_FOR_NON_NULLABLE_TYPE(ZCL_INT24U_ATTRIBUTE_TYPE))); + + using NumericType = NumericAttributeTraits; + using NullableType = DataModel::Nullable; + AttributeValueDecoder decoder = test.DecoderFor(0x1223344); + + // write should fail: written value is not in range + // NOTE: this matches legacy behaviour, however realistically maybe ConstraintError would be more correct + ASSERT_EQ(model.WriteAttribute(test.request, decoder), CHIP_ERROR_INVALID_ARGUMENT); +} + +TEST(TestCodegenModelViaMocks, EmberTestWriteOutOfRepresentableRangeOddIntegerNullable) +{ + UseMockNodeConfig config(gTestNodeConfig); + CodegenDataModelWithContext model; + ScopedMockAccessControl accessControl; + + TestWriteRequest test( + kAdminSubjectDescriptor, + ConcreteAttributePath(kMockEndpoint3, MockClusterId(4), MOCK_ATTRIBUTE_ID_FOR_NULLABLE_TYPE(ZCL_INT24U_ATTRIBUTE_TYPE))); + + using NumericType = NumericAttributeTraits; + using NullableType = DataModel::Nullable; + AttributeValueDecoder decoder = test.DecoderFor(0x1223344); + + // write should fail: written value is not in range + // NOTE: this matches legacy behaviour, however realistically maybe ConstraintError would be more correct + ASSERT_EQ(model.WriteAttribute(test.request, decoder), CHIP_ERROR_INVALID_ARGUMENT); +} + +TEST(TestCodegenModelViaMoceNullValueToNullables, EmberAttributeWriteBasicTypesLowestValue) +{ + TestEmberScalarTypeWrite(-127); + TestEmberScalarTypeWrite(-32767); + TestEmberScalarTypeWrite, ZCL_INT24S_ATTRIBUTE_TYPE>(-8388607); + TestEmberScalarTypeWrite(-2147483647); + TestEmberScalarTypeWrite, ZCL_INT40S_ATTRIBUTE_TYPE>(-549755813887); + TestEmberScalarTypeWrite, ZCL_INT48S_ATTRIBUTE_TYPE>(-140737488355327); + TestEmberScalarTypeWrite, ZCL_INT56S_ATTRIBUTE_TYPE>(-36028797018963967); + TestEmberScalarTypeWrite(-9223372036854775807); +} + +TEST(TestCodegenModelViaMocks, EmberAttributeWriteNulls) +{ + TestEmberScalarNullWrite(); + TestEmberScalarNullWrite(); + TestEmberScalarNullWrite, ZCL_INT24U_ATTRIBUTE_TYPE>(); + TestEmberScalarNullWrite(); + TestEmberScalarNullWrite, ZCL_INT40U_ATTRIBUTE_TYPE>(); + TestEmberScalarNullWrite, ZCL_INT48U_ATTRIBUTE_TYPE>(); + TestEmberScalarNullWrite, ZCL_INT56U_ATTRIBUTE_TYPE>(); + TestEmberScalarNullWrite(); + + TestEmberScalarNullWrite(); + TestEmberScalarNullWrite(); + TestEmberScalarNullWrite, ZCL_INT24S_ATTRIBUTE_TYPE>(); + TestEmberScalarNullWrite(); + TestEmberScalarNullWrite, ZCL_INT40S_ATTRIBUTE_TYPE>(); + TestEmberScalarNullWrite, ZCL_INT48S_ATTRIBUTE_TYPE>(); + TestEmberScalarNullWrite, ZCL_INT56S_ATTRIBUTE_TYPE>(); + TestEmberScalarNullWrite(); + TestEmberScalarNullWrite(); + TestEmberScalarNullWrite(); + TestEmberScalarNullWrite(); +} + +TEST(TestCodegenModelViaMocks, EmberAttributeWriteShortString) +{ + UseMockNodeConfig config(gTestNodeConfig); + CodegenDataModelWithContext model; + ScopedMockAccessControl accessControl; + + TestWriteRequest test(kAdminSubjectDescriptor, + ConcreteAttributePath(kMockEndpoint3, MockClusterId(4), + MOCK_ATTRIBUTE_ID_FOR_NON_NULLABLE_TYPE(ZCL_CHAR_STRING_ATTRIBUTE_TYPE))); + AttributeValueDecoder decoder = test.DecoderFor("hello world"_span); + + ASSERT_EQ(model.WriteAttribute(test.request, decoder), CHIP_NO_ERROR); + chip::ByteSpan writtenData = GetEmberBuffer(); + chip::CharSpan asCharSpan(reinterpret_cast(writtenData.data()), writtenData[0] + 1); + ASSERT_TRUE(asCharSpan.data_equal("\x0Bhello world"_span)); +} + +TEST(TestCodegenModelViaMocks, EmberAttributeWriteLongString) +{ + UseMockNodeConfig config(gTestNodeConfig); + CodegenDataModelWithContext model; + ScopedMockAccessControl accessControl; + + TestWriteRequest test(kAdminSubjectDescriptor, + ConcreteAttributePath(kMockEndpoint3, MockClusterId(4), + MOCK_ATTRIBUTE_ID_FOR_NON_NULLABLE_TYPE(ZCL_LONG_CHAR_STRING_ATTRIBUTE_TYPE))); + AttributeValueDecoder decoder = test.DecoderFor("text"_span); + + ASSERT_EQ(model.WriteAttribute(test.request, decoder), CHIP_NO_ERROR); + chip::ByteSpan writtenData = GetEmberBuffer(); + + uint16_t len = ReadLe16(writtenData.data()); + EXPECT_EQ(len, 4); + chip::CharSpan asCharSpan(reinterpret_cast(writtenData.data() + 2), 4); + + ASSERT_TRUE(asCharSpan.data_equal("text"_span)); +} + +TEST(TestCodegenModelViaMocks, EmberAttributeWriteNullableLongStringValue) +{ + UseMockNodeConfig config(gTestNodeConfig); + CodegenDataModelWithContext model; + ScopedMockAccessControl accessControl; + + TestWriteRequest test(kAdminSubjectDescriptor, + ConcreteAttributePath(kMockEndpoint3, MockClusterId(4), + MOCK_ATTRIBUTE_ID_FOR_NULLABLE_TYPE(ZCL_LONG_CHAR_STRING_ATTRIBUTE_TYPE))); + AttributeValueDecoder decoder = test.DecoderFor>(chip::app::DataModel::MakeNullable("text"_span)); + + ASSERT_EQ(model.WriteAttribute(test.request, decoder), CHIP_NO_ERROR); + chip::ByteSpan writtenData = GetEmberBuffer(); + + uint16_t len = ReadLe16(writtenData.data()); + EXPECT_EQ(len, 4); + chip::CharSpan asCharSpan(reinterpret_cast(writtenData.data() + 2), 4); + + ASSERT_TRUE(asCharSpan.data_equal("text"_span)); +} + +TEST(TestCodegenModelViaMocks, EmberAttributeWriteLongNullableStringNull) +{ + UseMockNodeConfig config(gTestNodeConfig); + CodegenDataModelWithContext model; + ScopedMockAccessControl accessControl; + + TestWriteRequest test(kAdminSubjectDescriptor, + ConcreteAttributePath(kMockEndpoint3, MockClusterId(4), + MOCK_ATTRIBUTE_ID_FOR_NULLABLE_TYPE(ZCL_LONG_CHAR_STRING_ATTRIBUTE_TYPE))); + AttributeValueDecoder decoder = test.DecoderFor>(chip::app::DataModel::Nullable()); + + ASSERT_EQ(model.WriteAttribute(test.request, decoder), CHIP_NO_ERROR); + chip::ByteSpan writtenData = GetEmberBuffer(); + ASSERT_EQ(writtenData[0], 0xFF); + ASSERT_EQ(writtenData[1], 0xFF); +} + +TEST(TestCodegenModelViaMocks, EmberAttributeWriteShortBytes) +{ + UseMockNodeConfig config(gTestNodeConfig); + CodegenDataModelWithContext model; + ScopedMockAccessControl accessControl; + + TestWriteRequest test(kAdminSubjectDescriptor, + ConcreteAttributePath(kMockEndpoint3, MockClusterId(4), + MOCK_ATTRIBUTE_ID_FOR_NON_NULLABLE_TYPE(ZCL_OCTET_STRING_ATTRIBUTE_TYPE))); + uint8_t buffer[] = { 11, 12, 13 }; + + AttributeValueDecoder decoder = test.DecoderFor(ByteSpan(buffer)); + + ASSERT_EQ(model.WriteAttribute(test.request, decoder), CHIP_NO_ERROR); + chip::ByteSpan writtenData = GetEmberBuffer(); + + EXPECT_EQ(writtenData[0], 3u); + EXPECT_EQ(writtenData[1], 11u); + EXPECT_EQ(writtenData[2], 12u); + EXPECT_EQ(writtenData[3], 13u); +} + +TEST(TestCodegenModelViaMocks, EmberAttributeWriteLongBytes) +{ + UseMockNodeConfig config(gTestNodeConfig); + CodegenDataModelWithContext model; + ScopedMockAccessControl accessControl; + + TestWriteRequest test(kAdminSubjectDescriptor, + ConcreteAttributePath(kMockEndpoint3, MockClusterId(4), + MOCK_ATTRIBUTE_ID_FOR_NON_NULLABLE_TYPE(ZCL_LONG_OCTET_STRING_ATTRIBUTE_TYPE))); + uint8_t buffer[] = { 11, 12, 13 }; + + AttributeValueDecoder decoder = test.DecoderFor(ByteSpan(buffer)); + + ASSERT_EQ(model.WriteAttribute(test.request, decoder), CHIP_NO_ERROR); + chip::ByteSpan writtenData = GetEmberBuffer(); + + uint16_t len = ReadLe16(writtenData.data()); + EXPECT_EQ(len, 3); + + EXPECT_EQ(writtenData[2], 11u); + EXPECT_EQ(writtenData[3], 12u); + EXPECT_EQ(writtenData[4], 13u); +} + +TEST(TestCodegenModelViaMocks, EmberAttributeWriteTimedWrite) +{ + UseMockNodeConfig config(gTestNodeConfig); + CodegenDataModelWithContext model; + ScopedMockAccessControl accessControl; + + TestWriteRequest test(kAdminSubjectDescriptor, ConcreteAttributePath(kMockEndpoint3, MockClusterId(4), kAttributeIdTimedWrite)); + AttributeValueDecoder decoder = test.DecoderFor(1234); + + ASSERT_EQ(model.WriteAttribute(test.request, decoder), CHIP_IM_GLOBAL_STATUS(NeedsTimedInteraction)); + + // writing as timed should be fine + test.request.writeFlags.Set(WriteFlags::kTimed); + ASSERT_EQ(model.WriteAttribute(test.request, decoder), CHIP_NO_ERROR); +} + +TEST(TestCodegenModelViaMocks, EmberAttributeWriteReadOnlyAttribute) +{ + UseMockNodeConfig config(gTestNodeConfig); + CodegenDataModelWithContext model; + ScopedMockAccessControl accessControl; + + TestWriteRequest test(kAdminSubjectDescriptor, ConcreteAttributePath(kMockEndpoint3, MockClusterId(4), kAttributeIdReadOnly)); + AttributeValueDecoder decoder = test.DecoderFor(1234); + + ASSERT_EQ(model.WriteAttribute(test.request, decoder), CHIP_IM_GLOBAL_STATUS(UnsupportedWrite)); + + // Internal writes bypass the read only requirement + test.request.operationFlags.Set(OperationFlags::kInternal); + ASSERT_EQ(model.WriteAttribute(test.request, decoder), CHIP_NO_ERROR); +} + +TEST(TestCodegenModelViaMocks, EmberAttributeWriteDataVersion) +{ + UseMockNodeConfig config(gTestNodeConfig); + CodegenDataModelWithContext model; + ScopedMockAccessControl accessControl; + + TestWriteRequest test(kAdminSubjectDescriptor, + ConcreteAttributePath(kMockEndpoint3, MockClusterId(4), + MOCK_ATTRIBUTE_ID_FOR_NON_NULLABLE_TYPE(ZCL_INT32S_ATTRIBUTE_TYPE))); + + // Initialize to some version + ResetVersion(); + BumpVersion(); + test.request.path.mDataVersion = MakeOptional(GetVersion()); + + // Make version invalid + BumpVersion(); + + AttributeValueDecoder decoder = test.DecoderFor(1234); + + ASSERT_EQ(model.WriteAttribute(test.request, decoder), CHIP_IM_GLOBAL_STATUS(DataVersionMismatch)); + + // Write passes if we set the right version for the data + test.request.path.mDataVersion = MakeOptional(GetVersion()); + ASSERT_EQ(model.WriteAttribute(test.request, decoder), CHIP_NO_ERROR); +} + +TEST(TestCodegenModelViaMocks, WriteToInvalidPath) +{ + UseMockNodeConfig config(gTestNodeConfig); + CodegenDataModelWithContext model; + ScopedMockAccessControl accessControl; + + { + TestWriteRequest test(kAdminSubjectDescriptor, ConcreteAttributePath(kInvalidEndpointId, MockClusterId(1234), 1234)); + AttributeValueDecoder decoder = test.DecoderFor(1234); + ASSERT_EQ(model.WriteAttribute(test.request, decoder), CHIP_IM_GLOBAL_STATUS(UnsupportedEndpoint)); + } + { + TestWriteRequest test(kAdminSubjectDescriptor, ConcreteAttributePath(kMockEndpoint1, MockClusterId(1234), 1234)); + AttributeValueDecoder decoder = test.DecoderFor(1234); + ASSERT_EQ(model.WriteAttribute(test.request, decoder), CHIP_IM_GLOBAL_STATUS(UnsupportedCluster)); + } + + { + TestWriteRequest test(kAdminSubjectDescriptor, ConcreteAttributePath(kMockEndpoint1, MockClusterId(1), 1234)); + AttributeValueDecoder decoder = test.DecoderFor(1234); + ASSERT_EQ(model.WriteAttribute(test.request, decoder), CHIP_IM_GLOBAL_STATUS(UnsupportedAttribute)); + } +} + +TEST(TestCodegenModelViaMocks, WriteToGlobalAttribute) +{ + UseMockNodeConfig config(gTestNodeConfig); + CodegenDataModelWithContext model; + ScopedMockAccessControl accessControl; + + TestWriteRequest test(kAdminSubjectDescriptor, ConcreteAttributePath(kMockEndpoint1, MockClusterId(1), AttributeList::Id)); + AttributeValueDecoder decoder = test.DecoderFor(1234); + ASSERT_EQ(model.WriteAttribute(test.request, decoder), CHIP_IM_GLOBAL_STATUS(UnsupportedWrite)); +} + +TEST(TestCodegenModelViaMocks, EmberWriteFailure) +{ + UseMockNodeConfig config(gTestNodeConfig); + CodegenDataModelWithContext model; + ScopedMockAccessControl accessControl; + + TestWriteRequest test(kAdminSubjectDescriptor, + ConcreteAttributePath(kMockEndpoint3, MockClusterId(4), + MOCK_ATTRIBUTE_ID_FOR_NON_NULLABLE_TYPE(ZCL_INT32S_ATTRIBUTE_TYPE))); + + { + AttributeValueDecoder decoder = test.DecoderFor(1234); + chip::Test::SetEmberReadOutput(Protocols::InteractionModel::Status::Failure); + ASSERT_EQ(model.WriteAttribute(test.request, decoder), CHIP_IM_GLOBAL_STATUS(Failure)); + } + { + AttributeValueDecoder decoder = test.DecoderFor(1234); + chip::Test::SetEmberReadOutput(Protocols::InteractionModel::Status::Busy); + ASSERT_EQ(model.WriteAttribute(test.request, decoder), CHIP_IM_GLOBAL_STATUS(Busy)); + } + // reset things to success to not affect other tests + chip::Test::SetEmberReadOutput(ByteSpan()); +} + +TEST(TestCodegenModelViaMocks, EmberWriteAttributeAccessInterfaceTest) +{ + UseMockNodeConfig config(gTestNodeConfig); + CodegenDataModelWithContext model; + ScopedMockAccessControl accessControl; + + const ConcreteAttributePath kStructPath(kMockEndpoint3, MockClusterId(4), + MOCK_ATTRIBUTE_ID_FOR_NON_NULLABLE_TYPE(ZCL_STRUCT_ATTRIBUTE_TYPE)); + RegisteredAttributeAccessInterface aai(kStructPath); + + TestWriteRequest test(kAdminSubjectDescriptor, kStructPath); + Clusters::UnitTesting::Structs::SimpleStruct::Type testValue{ + .a = 112, + .b = true, + .e = "aai_write_test"_span, + .g = 0.5, + .h = 0.125, + }; + + AttributeValueDecoder decoder = test.DecoderFor(testValue); + ASSERT_EQ(model.WriteAttribute(test.request, decoder), CHIP_NO_ERROR); + + EXPECT_EQ(aai->GetData().a, 112); + EXPECT_TRUE(aai->GetData().e.data_equal("aai_write_test"_span)); + + // AAI marks dirty paths + ASSERT_EQ(model.ChangeListener().DirtyList().size(), 1u); + EXPECT_EQ(model.ChangeListener().DirtyList()[0], kStructPath); + + // AAI does not prevent read/write of regular attributes + // validate that once AAI is added, we still can go through writing regular bits (i.e. + // AAI returning "unknown" has fallback to ember) + TestEmberScalarTypeWrite(1234); + TestEmberScalarNullWrite(); +} + +TEST(TestCodegenModelViaMocks, EmberWriteAttributeAccessInterfaceReturningError) +{ + UseMockNodeConfig config(gTestNodeConfig); + CodegenDataModelWithContext model; + ScopedMockAccessControl accessControl; + + const ConcreteAttributePath kStructPath(kMockEndpoint3, MockClusterId(4), + MOCK_ATTRIBUTE_ID_FOR_NON_NULLABLE_TYPE(ZCL_STRUCT_ATTRIBUTE_TYPE)); + RegisteredAttributeAccessInterface aai(kStructPath, CHIP_ERROR_KEY_NOT_FOUND); + + TestWriteRequest test(kAdminSubjectDescriptor, kStructPath); + Clusters::UnitTesting::Structs::SimpleStruct::Type testValue{ + .a = 112, + .b = true, + .e = "aai_write_test"_span, + .g = 0.5, + .h = 0.125, + }; + + AttributeValueDecoder decoder = test.DecoderFor(testValue); + ASSERT_EQ(model.WriteAttribute(test.request, decoder), CHIP_ERROR_KEY_NOT_FOUND); + ASSERT_TRUE(model.ChangeListener().DirtyList().empty()); +} + +TEST(TestCodegenModelViaMocks, EmberWriteInvalidDataType) +{ + UseMockNodeConfig config(gTestNodeConfig); + CodegenDataModelWithContext model; + ScopedMockAccessControl accessControl; + + const ConcreteAttributePath kStructPath(kMockEndpoint3, MockClusterId(4), + MOCK_ATTRIBUTE_ID_FOR_NON_NULLABLE_TYPE(ZCL_STRUCT_ATTRIBUTE_TYPE)); + + TestWriteRequest test(kAdminSubjectDescriptor, kStructPath); + Clusters::UnitTesting::Structs::SimpleStruct::Type testValue{ + .a = 112, + .b = true, + .e = "aai_write_test"_span, + .g = 0.5, + .h = 0.125, + }; + + AttributeValueDecoder decoder = test.DecoderFor(testValue); + + // Embed specifically DOES NOT support structures. + // Without AAI, we expect a data type error (translated to failure) + ASSERT_EQ(model.WriteAttribute(test.request, decoder), CHIP_IM_GLOBAL_STATUS(Failure)); + ASSERT_TRUE(model.ChangeListener().DirtyList().empty()); +} diff --git a/src/app/data-model-interface/DataModel.h b/src/app/data-model-interface/DataModel.h index 04911fd75cccc3..d673b79aac72a9 100644 --- a/src/app/data-model-interface/DataModel.h +++ b/src/app/data-model-interface/DataModel.h @@ -58,7 +58,7 @@ class DataModel : public DataModelMetadataTree /// TEMPORARY/TRANSITIONAL requirement for transitioning from ember-specific code /// ReadAttribute is REQUIRED to perform: /// - ACL validation (see notes on OperationFlags::kInternal) - /// - Validation of readability/writability + /// - Validation of readability/writability (also controlled by OperationFlags::kInternal) /// - use request.path.mExpanded to skip encoding replies for data according /// to 8.4.3.2 of the spec: /// > If the path indicates attribute data that is not readable, then the path SHALL @@ -84,8 +84,11 @@ class DataModel : public DataModelMetadataTree /// When this is invoked, caller is expected to have already done some validations: /// - cluster `data version` has been checked for the incoming request if applicable /// - /// When `request.writeFlags.Has(WriteFlags::kForceInternal)` the request is from an internal app update - /// and SHOULD bypass some internal checks (like timed enforcement, potentially read-only restrictions) + /// TEMPORARY/TRANSITIONAL requirement for transitioning from ember-specific code + /// WriteAttribute is REQUIRED to perform: + /// - ACL validation (see notes on OperationFlags::kInternal) + /// - Validation of readability/writability (also controlled by OperationFlags::kInternal) + /// - Validation of timed interaction required (also controlled by OperationFlags::kInternal) /// /// Return codes /// CHIP_IM_GLOBAL_STATUS(code): diff --git a/src/app/data-model-interface/DataModelChangeListener.h b/src/app/data-model-interface/DataModelChangeListener.h index 37c278c76b92c8..a5ba12684baffe 100644 --- a/src/app/data-model-interface/DataModelChangeListener.h +++ b/src/app/data-model-interface/DataModelChangeListener.h @@ -16,7 +16,7 @@ */ #pragma once -#include +#include namespace chip { namespace app { @@ -34,12 +34,12 @@ namespace InteractionModel { class DataModelChangeListener { public: - virtual ~DataModelChangeListener() = 0; + virtual ~DataModelChangeListener() = default; /// Mark all attributes matching the given path (which may be a wildcard) dirty. /// /// Wildcards are supported. - virtual void MarkDirty(const AttributePathParams & path) = 0; + virtual void MarkDirty(const ConcreteAttributePath & path) = 0; }; } // namespace InteractionModel diff --git a/src/app/util/ember-compatibility-functions.cpp b/src/app/util/ember-compatibility-functions.cpp index cc3185f78a5df6..676197be9ab2f2 100644 --- a/src/app/util/ember-compatibility-functions.cpp +++ b/src/app/util/ember-compatibility-functions.cpp @@ -570,7 +570,7 @@ CHIP_ERROR ReadSingleClusterData(const SubjectDescriptor & aSubjectDescriptor, b } default: ChipLogError(DataManagement, "Attribute type 0x%x not handled", static_cast(attributeType)); - status = Status::UnsupportedRead; + status = Status::Failure; } }