/* * 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 #include namespace chip { namespace app { namespace { using namespace chip::app::Compatibility::Internal; using Protocols::InteractionModel::Status; class ContextAttributesChangeListener : public AttributesChangedListener { public: ContextAttributesChangeListener(const DataModel::InteractionModelContext & context) : mListener(context.dataModelChangeListener) {} void MarkDirty(const AttributePathParams & path) override { mListener->MarkDirty(path); } private: DataModel::ProviderChangeListener * mListener; }; /// 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 ConcreteDataAttributePath & 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); out.reduce_size(sizeof(typename ENCODING::LengthType)); 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()); output_buffer += workingValue.size(); out.reduce_size(static_cast(output_buffer - out.data())); 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.Value()), CHIP_ERROR_INVALID_ARGUMENT); Traits::WorkingToStorage(workingValue.Value(), 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)); out.reduce_size(sizeof(storageValue)); return CHIP_NO_ERROR; } /// Read the data from "decoder" into an ember-formatted buffer "out" /// /// `out` is a in/out buffer: /// - its initial size determines the maximum size of the buffer /// - its output size reflects the actual data size /// /// 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 DataModel::ActionReturnStatus CodegenDataModelProvider::WriteAttribute(const DataModel::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)); // TODO: ordering is to check writability/existence BEFORE ACL and this seems wrong, however // existing unit tests (TC_AcessChecker.py) validate that we get UnsupportedWrite instead of UnsupportedAccess // // This should likely be fixed in spec (probably already fixed by // https://github.com/CHIP-Specifications/connectedhomeip-spec/pull/9024) // and tests and implementation // // Open issue that needs fixing: https://github.com/project-chip/connectedhomeip/issues/33735 auto metadata = Ember::FindAttributeMetadata(request.path); // Explicit failure in finding a suitable metadata if (const Status * status = std::get_if(&metadata)) { VerifyOrDie((*status == Status::UnsupportedEndpoint) || // (*status == Status::UnsupportedCluster) || // (*status == Status::UnsupportedAttribute)); return *status; } 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(DataModel::OperationFlags::kInternal)) { VerifyOrReturnError(!isReadOnly, Status::UnsupportedWrite); } // ACL check for non-internal requests bool checkAcl = !request.operationFlags.Has(DataModel::OperationFlags::kInternal); // For chunking, ACL check is not re-done if the previous write was successful for the exact same // path. We apply this everywhere as a shortcut, although realistically this is only for AccessControl cluster if (checkAcl && request.previousSuccessPath.has_value()) { // NOTE: explicit cast/check only for attribute path and nothing else. // // In particular `request.path` is a DATA path (contains a list index) // and we do not want request.previousSuccessPath to be auto-cast to a // data path with a empty list and fail the compare. // // This could be `request.previousSuccessPath != request.path` (where order // is important) however that would seem more brittle (relying that a != b // behaves differently than b != a due to casts). Overall Data paths are not // the same as attribute paths. // // Also note that Concrete path have a mExpanded that is not used in compares. const ConcreteAttributePath & attributePathA = request.path; const ConcreteAttributePath & attributePathB = *request.previousSuccessPath; checkAcl = (attributePathA != attributePathB); } if (checkAcl) { VerifyOrReturnError(request.subjectDescriptor.has_value(), Status::UnsupportedAccess); Access::RequestPath requestPath{ .cluster = request.path.mClusterId, .endpoint = request.path.mEndpointId, .requestType = Access::RequestType::kAttributeWriteRequest, .entityId = request.path.mAttributeId }; CHIP_ERROR err = Access::GetAccessControl().Check(*request.subjectDescriptor, requestPath, RequiredPrivilege::ForWriteAttribute(request.path)); if (err != CHIP_NO_ERROR) { VerifyOrReturnValue(err != CHIP_ERROR_ACCESS_DENIED, Status::UnsupportedAccess); VerifyOrReturnValue(err != CHIP_ERROR_ACCESS_RESTRICTED_BY_ARL, Status::AccessRestricted); return err; } } // Internal is allowed to bypass timed writes and read-only. if (!request.operationFlags.Has(DataModel::OperationFlags::kInternal)) { VerifyOrReturnError(!(*attributeMetadata)->MustUseTimedWrite() || request.writeFlags.Has(DataModel::WriteFlags::kTimed), 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, 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 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 Status::DataVersionMismatch; } } ContextAttributesChangeListener change_listener(CurrentContext()); AttributeAccessInterface * aai = AttributeAccessInterfaceRegistry::Instance().Get(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: this is awkward since it provides AAI no control over this, specifically // AAI may not want to increase versions for some attributes that are Q emberAfAttributeChanged(request.path.mEndpointId, request.path.mClusterId, request.path.mAttributeId, &change_listener); } return *aai_result; } MutableByteSpan dataBuffer = gEmberAttributeIOBufferSpan; ReturnErrorOnFailure(DecodeValueIntoEmberBuffer(decoder, *attributeMetadata, dataBuffer)); Protocols::InteractionModel::Status status; if (dataBuffer.size() > (*attributeMetadata)->size) { ChipLogDetail(Zcl, "Data to write exceeds the attribute size claimed."); return Status::InvalidValue; } EmberAfWriteDataInput dataInput(dataBuffer.data(), (*attributeMetadata)->attributeType); dataInput.SetChangeListener(&change_listener); // TODO: dataInput.SetMarkDirty() should be according to `ChangesOmmited` if (request.operationFlags.Has(DataModel::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 attribute types) - see attribute-table.h documentation for details. status = emberAfWriteAttribute(request.path, dataInput); } else { status = emAfWriteAttributeExternal(request.path, dataInput); } if (status != Protocols::InteractionModel::Status::Success) { return status; } return CHIP_NO_ERROR; } } // namespace app } // namespace chip