/* * 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 #include #include #include #include namespace chip { namespace app { namespace { using namespace chip::app::Compatibility::Internal; using Protocols::InteractionModel::Status; /// 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). /// /// 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 TryReadViaAccessInterface(const ConcreteAttributePath & path, AttributeAccessInterface * aai, AttributeValueEncoder & encoder) { // Processing can happen only if an attribute access interface actually exists.. if (aai == nullptr) { return std::nullopt; } CHIP_ERROR err = aai->Read(path, encoder); if (err != CHIP_NO_ERROR) { // Implementation of 8.4.3.2 of the spec for path expansion if (path.mExpanded && (err == CHIP_IM_GLOBAL_STATUS(UnsupportedRead))) { return CHIP_NO_ERROR; } return err; } // If the encoder tried to encode, then a value should have been written. // - if encode, assume DONE (i.e. FINAL CHIP_NO_ERROR) // - if no encode, say that processing must continue return encoder.TriedEncode() ? 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 size_t GetLength(ByteSpan buffer) { VerifyOrDie(buffer.size() >= 1); // NOTE: we do NOT use emberAfStringLength from ember-strings.h because that will result in 0 // length for null sizes (i.e. 0xFF is translated to 0 and we do not want that here) return buffer[0]; } }; /// 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; static size_t GetLength(ByteSpan buffer) { // NOTE: we do NOT use emberAfLongStringLength from ember-strings.h because that will result in 0 // length for null sizes (i.e. 0xFFFF is translated to 0 and we do not want that here) VerifyOrDie(buffer.size() >= 2); const uint8_t * data = buffer.data(); return Encoding::LittleEndian::Read16(data); } }; // ember assumptions ... should just work static_assert(sizeof(ShortPascalString::LengthType) == 1); static_assert(sizeof(LongPascalString::LengthType) == 2); /// Given a ByteSpan containing data from ember, interpret it /// as a span of type OUT (i.e. ByteSpan or CharSpan) given a ENCODING /// where ENCODING is Short or Long pascal strings. template std::optional ExtractEmberString(ByteSpan data) { constexpr size_t kLengthTypeSize = sizeof(typename ENCODING::LengthType); VerifyOrDie(kLengthTypeSize <= data.size()); auto len = ENCODING::GetLength(data); if (len == ENCODING::kNullLength) { return std::nullopt; } VerifyOrDie(len + sizeof(len) <= data.size()); return std::make_optional(reinterpret_cast(data.data() + kLengthTypeSize), len); } /// Encode a value inside `encoder` /// /// The value encoded will be of type T (e.g. CharSpan or ByteSpan) and it will be decoded /// via the given ENCODING (i.e. ShortPascalString or LongPascalString) /// /// isNullable defines if the value of NULL is allowed to be encoded. template CHIP_ERROR EncodeStringLike(ByteSpan data, bool isNullable, AttributeValueEncoder & encoder) { std::optional value = ExtractEmberString(data); if (!value.has_value()) { if (isNullable) { return encoder.EncodeNull(); } return CHIP_ERROR_INCORRECT_STATE; } // encode value as-is return encoder.Encode(*value); } /// Encodes a numeric data value of type T from the given ember-encoded buffer `data`. /// /// isNullable defines if the value of NULL is allowed to be encoded. template CHIP_ERROR EncodeFromSpan(ByteSpan data, bool isNullable, AttributeValueEncoder & encoder) { typename NumericAttributeTraits::StorageType value; VerifyOrReturnError(data.size() >= sizeof(value), CHIP_ERROR_INVALID_ARGUMENT); memcpy(&value, data.data(), sizeof(value)); if (isNullable && NumericAttributeTraits::IsNullValue(value)) { return encoder.EncodeNull(); } if (!NumericAttributeTraits::CanRepresentValue(isNullable, value)) { return CHIP_ERROR_INCORRECT_STATE; } return encoder.Encode(NumericAttributeTraits::StorageToWorking(value)); } /// Converts raw ember data from `data` into the encoder /// /// Uses the attribute `metadata` to determine how the data is encoded into `data` and /// write a suitable value into `encoder`. CHIP_ERROR EncodeEmberValue(ByteSpan data, const EmberAfAttributeMetadata * metadata, AttributeValueEncoder & encoder) { VerifyOrReturnError(metadata != nullptr, CHIP_ERROR_INVALID_ARGUMENT); const bool isNullable = metadata->IsNullable(); switch (AttributeBaseType(metadata->attributeType)) { case ZCL_NO_DATA_ATTRIBUTE_TYPE: // No data return encoder.EncodeNull(); case ZCL_BOOLEAN_ATTRIBUTE_TYPE: // Boolean return EncodeFromSpan(data, isNullable, encoder); case ZCL_INT8U_ATTRIBUTE_TYPE: // Unsigned 8-bit integer return EncodeFromSpan(data, isNullable, encoder); case ZCL_INT16U_ATTRIBUTE_TYPE: // Unsigned 16-bit integer return EncodeFromSpan(data, isNullable, encoder); case ZCL_INT24U_ATTRIBUTE_TYPE: // Unsigned 24-bit integer return EncodeFromSpan>(data, isNullable, encoder); case ZCL_INT32U_ATTRIBUTE_TYPE: // Unsigned 32-bit integer return EncodeFromSpan(data, isNullable, encoder); case ZCL_INT40U_ATTRIBUTE_TYPE: // Unsigned 40-bit integer return EncodeFromSpan>(data, isNullable, encoder); case ZCL_INT48U_ATTRIBUTE_TYPE: // Unsigned 48-bit integer return EncodeFromSpan>(data, isNullable, encoder); case ZCL_INT56U_ATTRIBUTE_TYPE: // Unsigned 56-bit integer return EncodeFromSpan>(data, isNullable, encoder); case ZCL_INT64U_ATTRIBUTE_TYPE: // Unsigned 64-bit integer return EncodeFromSpan(data, isNullable, encoder); case ZCL_INT8S_ATTRIBUTE_TYPE: // Signed 8-bit integer return EncodeFromSpan(data, isNullable, encoder); case ZCL_INT16S_ATTRIBUTE_TYPE: // Signed 16-bit integer return EncodeFromSpan(data, isNullable, encoder); case ZCL_INT24S_ATTRIBUTE_TYPE: // Signed 24-bit integer return EncodeFromSpan>(data, isNullable, encoder); case ZCL_INT32S_ATTRIBUTE_TYPE: // Signed 32-bit integer return EncodeFromSpan(data, isNullable, encoder); case ZCL_INT40S_ATTRIBUTE_TYPE: // Signed 40-bit integer return EncodeFromSpan>(data, isNullable, encoder); case ZCL_INT48S_ATTRIBUTE_TYPE: // Signed 48-bit integer return EncodeFromSpan>(data, isNullable, encoder); case ZCL_INT56S_ATTRIBUTE_TYPE: // Signed 56-bit integer return EncodeFromSpan>(data, isNullable, encoder); case ZCL_INT64S_ATTRIBUTE_TYPE: // Signed 64-bit integer return EncodeFromSpan(data, isNullable, encoder); case ZCL_SINGLE_ATTRIBUTE_TYPE: // 32-bit float return EncodeFromSpan(data, isNullable, encoder); case ZCL_DOUBLE_ATTRIBUTE_TYPE: // 64-bit float return EncodeFromSpan(data, isNullable, encoder); case ZCL_CHAR_STRING_ATTRIBUTE_TYPE: // Char string return EncodeStringLike(data, isNullable, encoder); case ZCL_LONG_CHAR_STRING_ATTRIBUTE_TYPE: return EncodeStringLike(data, isNullable, encoder); case ZCL_OCTET_STRING_ATTRIBUTE_TYPE: // Octet string return EncodeStringLike(data, isNullable, encoder); case ZCL_LONG_OCTET_STRING_ATTRIBUTE_TYPE: return EncodeStringLike(data, isNullable, encoder); default: ChipLogError(DataManagement, "Attribute type 0x%x not handled", static_cast(metadata->attributeType)); return CHIP_IM_GLOBAL_STATUS(Failure); } } } // namespace /// separated-out ReadAttribute implementation (given existing complexity) /// /// Generally will: /// - validate ACL (only for non-internal requests) /// - Try to read attribute via the AttributeAccessInterface /// - Try to read the value from ember RAM storage DataModel::ActionReturnStatus CodegenDataModelProvider::ReadAttribute(const DataModel::ReadAttributeRequest & request, AttributeValueEncoder & encoder) { ChipLogDetail(DataManagement, "Reading attribute: Cluster=" ChipLogFormatMEI " Endpoint=0x%x AttributeId=" ChipLogFormatMEI " (expanded=%d)", ChipLogValueMEI(request.path.mClusterId), request.path.mEndpointId, ChipLogValueMEI(request.path.mAttributeId), request.path.mExpanded); // ACL check for non-internal requests if (!request.operationFlags.Has(DataModel::OperationFlags::kInternal)) { VerifyOrReturnError(request.subjectDescriptor.has_value(), CHIP_ERROR_INVALID_ARGUMENT); Access::RequestPath requestPath{ .cluster = request.path.mClusterId, .endpoint = request.path.mEndpointId, .requestType = Access::RequestType::kAttributeReadRequest, .entityId = request.path.mAttributeId }; CHIP_ERROR err = Access::GetAccessControl().Check(*request.subjectDescriptor, requestPath, RequiredPrivilege::ForReadAttribute(request.path)); if (err != CHIP_NO_ERROR) { VerifyOrReturnError((err == CHIP_ERROR_ACCESS_DENIED) || (err == CHIP_ERROR_ACCESS_RESTRICTED_BY_ARL), err); // Implementation of 8.4.3.2 of the spec for path expansion if (request.path.mExpanded) { return CHIP_NO_ERROR; } // access denied and access restricted have specific codes for IM return err == CHIP_ERROR_ACCESS_DENIED ? CHIP_IM_GLOBAL_STATUS(UnsupportedAccess) : CHIP_IM_GLOBAL_STATUS(AccessRestricted); } } 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; } // Read via AAI std::optional aai_result; if (const EmberAfCluster ** cluster = std::get_if(&metadata)) { Compatibility::GlobalAttributeReader aai(*cluster); aai_result = TryReadViaAccessInterface(request.path, &aai, encoder); } else { aai_result = TryReadViaAccessInterface( request.path, AttributeAccessInterfaceRegistry::Instance().Get(request.path.mEndpointId, request.path.mClusterId), encoder); } VerifyOrReturnError(!aai_result.has_value(), *aai_result); if (!std::holds_alternative(metadata)) { // if we only got a cluster, this was for a global attribute. We cannot read ember attributes // at this point, so give up (although GlobalAttributeReader should have returned something here). chipDie(); } const EmberAfAttributeMetadata * attributeMetadata = std::get(metadata); // At this point, we have to use ember directly to read the data. EmberAfAttributeSearchRecord record; record.endpoint = request.path.mEndpointId; record.clusterId = request.path.mClusterId; record.attributeId = request.path.mAttributeId; Protocols::InteractionModel::Status status = emAfReadOrWriteAttribute( &record, &attributeMetadata, gEmberAttributeIOBufferSpan.data(), static_cast(gEmberAttributeIOBufferSpan.size()), /* write = */ false); if (status != Protocols::InteractionModel::Status::Success) { return CHIP_ERROR_IM_GLOBAL_STATUS_VALUE(status); } return EncodeEmberValue(gEmberAttributeIOBufferSpan, attributeMetadata, encoder); } } // namespace app } // namespace chip