| /** |
| * |
| * Copyright (c) 2020 Project CHIP Authors |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| #include <app/util/attribute-table.h> |
| |
| #include <app/util/attribute-table-detail.h> |
| |
| #include <app/util/attribute-storage-detail.h> |
| #include <app/util/attribute-storage.h> |
| #include <app/util/config.h> |
| #include <app/util/ember-strings.h> |
| #include <app/util/generic-callbacks.h> |
| #include <app/util/odd-sized-integers.h> |
| #include <lib/core/CHIPConfig.h> |
| |
| #include <app/reporting/reporting.h> |
| #include <protocols/interaction_model/Constants.h> |
| |
| #if (CHIP_CONFIG_BIG_ENDIAN_TARGET) |
| #define EM_BIG_ENDIAN true |
| #else |
| #define EM_BIG_ENDIAN false |
| #endif |
| |
| using chip::Protocols::InteractionModel::Status; |
| |
| using namespace chip; |
| using namespace chip::app; |
| |
| namespace { |
| /** |
| * @brief Simple integer comparison function. |
| * Compares two values of a known length as integers. |
| * Signed integer comparison are supported for numbers with length of |
| * 4 (bytes) or less. |
| * The integers are in native endianness. |
| * |
| * @return -1, if val1 is smaller |
| * 0, if they are the same or if two negative numbers with length |
| * greater than 4 is being compared |
| * 1, if val2 is smaller. |
| * |
| * You can pass in val1 as NULL, which will assume that it is |
| * pointing to an array of all zeroes. This is used so that |
| * default value of NULL is treated as all zeroes. |
| */ |
| int8_t emberAfCompareValues(const uint8_t * val1, const uint8_t * val2, uint16_t len, bool signedNumber) |
| { |
| if (len == 0) |
| { |
| // no length means nothing to compare. Shouldn't even happen, since len is sizeof(some-integer-type). |
| return 0; |
| } |
| |
| if (signedNumber) |
| { // signed number comparison |
| if (len <= 4) |
| { // only number with 32-bits or less is supported |
| int32_t accum1 = 0x0; |
| int32_t accum2 = 0x0; |
| int32_t all1s = -1; |
| |
| for (uint16_t i = 0; i < len; i++) |
| { |
| uint8_t j = (val1 == nullptr ? 0 : (EM_BIG_ENDIAN ? val1[i] : val1[(len - 1) - i])); |
| accum1 |= j << (8 * (len - 1 - i)); |
| |
| uint8_t k = (EM_BIG_ENDIAN ? val2[i] : val2[(len - 1) - i]); |
| accum2 |= k << (8 * (len - 1 - i)); |
| } |
| |
| // sign extending, no need for 32-bits numbers |
| if (len < 4) |
| { |
| if ((accum1 & (1 << (8 * len - 1))) != 0) |
| { // check sign |
| accum1 |= all1s - ((1 << (len * 8)) - 1); |
| } |
| if ((accum2 & (1 << (8 * len - 1))) != 0) |
| { // check sign |
| accum2 |= all1s - ((1 << (len * 8)) - 1); |
| } |
| } |
| |
| if (accum1 > accum2) |
| { |
| return 1; |
| } |
| if (accum1 < accum2) |
| { |
| return -1; |
| } |
| |
| return 0; |
| } |
| |
| // not supported |
| return 0; |
| } |
| |
| // regular unsigned number comparison |
| for (uint16_t i = 0; i < len; i++) |
| { |
| uint8_t j = (val1 == nullptr ? 0 : (EM_BIG_ENDIAN ? val1[i] : val1[(len - 1) - i])); |
| uint8_t k = (EM_BIG_ENDIAN ? val2[i] : val2[(len - 1) - i]); |
| |
| if (j > k) |
| { |
| return 1; |
| } |
| if (k > j) |
| { |
| return -1; |
| } |
| } |
| return 0; |
| } |
| |
| /** |
| * @brief write an attribute, performing all the checks. |
| * |
| * This function will attempt to write the attribute value from |
| * the provided pointer. This function will only check that the |
| * attribute exists. If it does it will write the value into |
| * the attribute table for the given attribute. |
| * |
| * This function will not check to see if the attribute is |
| * writable since the read only / writable characteristic |
| * of an attribute only pertains to external devices writing |
| * over the air. Because this function is being called locally |
| * it assumes that the device knows what it is doing and has permission |
| * to perform the given operation. |
| * |
| * if true is passed in for overrideReadOnlyAndDataType then the data type is |
| * not checked and the read-only flag is ignored. This mode is meant for |
| * testing or setting the initial value of the attribute on the device. |
| * |
| * this returns: |
| * - Status::UnsupportedEndpoint: if endpoint isn't supported by the device. |
| * - Status::UnsupportedCluster: if cluster isn't supported on the endpoint. |
| * - Status::UnsupportedAttribute: if attribute isn't supported in the cluster. |
| * - Status::InvalidDataType: if the data type passed in doesnt match the type |
| * stored in the attribute table |
| * - Status::UnsupportedWrite: if the attribute isnt writable |
| * - Status::ConstraintError: if the value is set out of the allowable range for |
| * the attribute |
| * - Status::Success: if the attribute was found and successfully written |
| */ |
| Status emAfWriteAttribute(const ConcreteAttributePath & path, const EmberAfWriteDataInput & input, |
| bool overrideReadOnlyAndDataType); |
| |
| } // anonymous namespace |
| |
| Protocols::InteractionModel::Status emAfWriteAttributeExternal(const ConcreteAttributePath & path, |
| const EmberAfWriteDataInput & input) |
| { |
| EmberAfWriteDataInput completeInput = input; |
| |
| if (completeInput.changeListener == nullptr) |
| { |
| completeInput.SetChangeListener(emberAfGlobalInteractionModelAttributesChangedListener()); |
| } |
| |
| return emAfWriteAttribute(path, completeInput, false /* overrideReadOnlyAndDataType */); |
| } |
| |
| Status emberAfWriteAttribute(EndpointId endpoint, ClusterId cluster, AttributeId attributeID, uint8_t * dataPtr, |
| EmberAfAttributeType dataType) |
| { |
| |
| return emberAfWriteAttribute( |
| ConcreteAttributePath(endpoint, cluster, attributeID), |
| EmberAfWriteDataInput(dataPtr, dataType).SetChangeListener(emberAfGlobalInteractionModelAttributesChangedListener())); |
| } |
| |
| Status emberAfWriteAttribute(const ConcreteAttributePath & path, const EmberAfWriteDataInput & input) |
| { |
| EmberAfWriteDataInput completeInput = input; |
| |
| if (completeInput.changeListener == nullptr) |
| { |
| completeInput.SetChangeListener(emberAfGlobalInteractionModelAttributesChangedListener()); |
| } |
| |
| return emAfWriteAttribute(path, completeInput, true /* overrideReadOnlyAndDataType */); |
| } |
| |
| //------------------------------------------------------------------------------ |
| // Internal Functions |
| |
| // Helper for determining whether a value is a null value. |
| template <typename T> |
| static bool IsNullValue(const uint8_t * data) |
| { |
| using Traits = app::NumericAttributeTraits<T>; |
| // We don't know how data is aligned, so safely copy it over to the relevant |
| // StorageType value. |
| typename Traits::StorageType val; |
| memcpy(&val, data, sizeof(val)); |
| return Traits::IsNullValue(val); |
| } |
| |
| static bool IsNullValue(const uint8_t * data, uint16_t dataLen, bool isAttributeSigned) |
| { |
| if (dataLen > 4) |
| { |
| // We don't support this, just like emberAfCompareValues does not. |
| return false; |
| } |
| |
| switch (dataLen) |
| { |
| case 1: { |
| if (isAttributeSigned) |
| { |
| return IsNullValue<int8_t>(data); |
| } |
| return IsNullValue<uint8_t>(data); |
| } |
| case 2: { |
| if (isAttributeSigned) |
| { |
| return IsNullValue<int16_t>(data); |
| } |
| return IsNullValue<uint16_t>(data); |
| } |
| case 3: { |
| if (isAttributeSigned) |
| { |
| return IsNullValue<app::OddSizedInteger<3, true>>(data); |
| } |
| return IsNullValue<app::OddSizedInteger<3, false>>(data); |
| } |
| case 4: { |
| if (isAttributeSigned) |
| { |
| return IsNullValue<int32_t>(data); |
| } |
| return IsNullValue<uint32_t>(data); |
| } |
| } |
| |
| // Not reached. |
| return false; |
| } |
| |
| namespace { |
| |
| /** |
| * Helper function to determine whether the attribute value for the given |
| * attribute is changing. On success, the isChanging outparam will be set to |
| * whether the value is changing. |
| */ |
| Status AttributeValueIsChanging(EndpointId endpoint, ClusterId cluster, AttributeId attributeID, |
| const EmberAfAttributeMetadata * metadata, uint8_t * newValueData, bool * isChanging) |
| { |
| EmberAfAttributeType attributeType = metadata->attributeType; |
| |
| // We don't know how to size our buffer for strings in general, but if the |
| // string happens to fit into our fixed-size buffer, great. |
| size_t valueSize = metadata->size; |
| constexpr size_t kMaxValueSize = 16; // ipv6adr |
| if (valueSize > kMaxValueSize) |
| { |
| if (emberAfIsStringAttributeType(attributeType) || emberAfIsLongStringAttributeType(attributeType)) |
| { |
| // It's a string that may not fit in our buffer. Just claim it's |
| // changing, since we have no way to tell. |
| *isChanging = true; |
| return Status::Success; |
| } |
| |
| // Very much unexpected |
| ChipLogError(Zcl, "Attribute type %d has too-large size %u", attributeType, static_cast<unsigned>(valueSize)); |
| return Status::ConstraintError; |
| } |
| |
| uint8_t oldValueBuffer[kMaxValueSize]; |
| // Cast to uint16_t is safe, because we checked valueSize <= kMaxValueSize above. |
| if (emberAfReadAttribute(endpoint, cluster, attributeID, oldValueBuffer, static_cast<uint16_t>(valueSize)) != Status::Success) |
| { |
| // We failed to read the old value, so flag the value as changing to be safe. |
| *isChanging = true; |
| return Status::Success; |
| } |
| |
| if (emberAfIsStringAttributeType(attributeType)) |
| { |
| size_t oldLength = emberAfStringLength(oldValueBuffer); |
| size_t newLength = emberAfStringLength(newValueData); |
| // The first byte of the buffer is the string length, and |
| // oldLength/newLength refer to the number of bytes after that. We want |
| // to include that first byte in our comparison, because null and empty |
| // string have different values there but both return 0 from |
| // emberAfStringLength. |
| *isChanging = (oldLength != newLength) || (memcmp(oldValueBuffer, newValueData, oldLength + 1) != 0); |
| } |
| else if (emberAfIsLongStringAttributeType(attributeType)) |
| { |
| size_t oldLength = emberAfLongStringLength(oldValueBuffer); |
| size_t newLength = emberAfLongStringLength(newValueData); |
| // The first two bytes of the buffer are the string length, and |
| // oldLength/newLength refer to the number of bytes after that. We want |
| // to include those first two bytes in our comparison, because null and |
| // empty string have different values there but both return 0 from |
| // emberAfLongStringLength. |
| *isChanging = (oldLength != newLength) || (memcmp(oldValueBuffer, newValueData, oldLength + 2) != 0); |
| } |
| else |
| { |
| *isChanging = (memcmp(newValueData, oldValueBuffer, valueSize) != 0); |
| } |
| |
| return Status::Success; |
| } |
| |
| Status emAfWriteAttribute(const ConcreteAttributePath & path, const EmberAfWriteDataInput & input, bool overrideReadOnlyAndDataType) |
| { |
| const EmberAfAttributeMetadata * metadata = nullptr; |
| EmberAfAttributeSearchRecord record; |
| record.endpoint = path.mEndpointId; |
| record.clusterId = path.mClusterId; |
| record.attributeId = path.mAttributeId; |
| Status status = emAfReadOrWriteAttribute(&record, &metadata, |
| nullptr, // buffer |
| 0, // buffer size |
| false); // write? |
| |
| // if we dont support that attribute |
| if (metadata == nullptr) |
| { |
| ChipLogProgress(Zcl, "%p ep %x clus " ChipLogFormatMEI " attr " ChipLogFormatMEI " not supported", |
| "WRITE ERR: ", path.mEndpointId, ChipLogValueMEI(path.mClusterId), ChipLogValueMEI(path.mAttributeId)); |
| return status; |
| } |
| |
| // if the data type specified by the caller is incorrect |
| if (!(overrideReadOnlyAndDataType)) |
| { |
| if (input.dataType != metadata->attributeType) |
| { |
| ChipLogProgress(Zcl, "%p invalid data type", "WRITE ERR: "); |
| return Status::InvalidDataType; |
| } |
| |
| if (metadata->IsReadOnly()) |
| { |
| ChipLogProgress(Zcl, "%p attr not writable", "WRITE ERR: "); |
| return Status::UnsupportedWrite; |
| } |
| } |
| |
| // if the value the attribute is being set to is out of range |
| // return Status::ConstraintError |
| if ((metadata->mask & ATTRIBUTE_MASK_MIN_MAX) != 0U) |
| { |
| EmberAfDefaultAttributeValue minv = metadata->defaultValue.ptrToMinMaxValue->minValue; |
| EmberAfDefaultAttributeValue maxv = metadata->defaultValue.ptrToMinMaxValue->maxValue; |
| uint16_t dataLen = emberAfAttributeSize(metadata); |
| const uint8_t * minBytes; |
| const uint8_t * maxBytes; |
| if (dataLen <= 2) |
| { |
| static_assert(sizeof(minv.defaultValue) == 2, "if statement relies on size of minv.defaultValue being 2"); |
| static_assert(sizeof(maxv.defaultValue) == 2, "if statement relies on size of maxv.defaultValue being 2"); |
| minBytes = reinterpret_cast<const uint8_t *>(&(minv.defaultValue)); |
| maxBytes = reinterpret_cast<const uint8_t *>(&(maxv.defaultValue)); |
| // On big endian cpu with length 1 only the second byte counts |
| #if (CHIP_CONFIG_BIG_ENDIAN_TARGET) |
| if (dataLen == 1) |
| { |
| minBytes++; |
| maxBytes++; |
| } |
| #endif // CHIP_CONFIG_BIG_ENDIAN_TARGET |
| } |
| else |
| { |
| minBytes = minv.ptrToDefaultValue; |
| maxBytes = maxv.ptrToDefaultValue; |
| } |
| |
| bool isAttributeSigned = metadata->IsSignedIntegerAttribute(); |
| bool isOutOfRange = emberAfCompareValues(minBytes, input.dataPtr, dataLen, isAttributeSigned) == 1 || |
| emberAfCompareValues(maxBytes, input.dataPtr, dataLen, isAttributeSigned) == -1; |
| |
| if (isOutOfRange && |
| // null value is always in-range for a nullable attribute. |
| (!metadata->IsNullable() || !IsNullValue(input.dataPtr, dataLen, isAttributeSigned))) |
| { |
| return Status::ConstraintError; |
| } |
| } |
| |
| // Check whether anything is actually changing, before we do any work here. |
| bool valueChanging; |
| Status imStatus = |
| AttributeValueIsChanging(path.mEndpointId, path.mClusterId, path.mAttributeId, metadata, input.dataPtr, &valueChanging); |
| if (imStatus != Status::Success) |
| { |
| return imStatus; |
| } |
| |
| if (!valueChanging) |
| { |
| // Just do nothing, except triggering reporting if forced. |
| if (input.markDirty == MarkAttributeDirty::kYes) |
| { |
| emberAfAttributeChanged(path.mEndpointId, path.mClusterId, path.mAttributeId, input.changeListener); |
| } |
| |
| return Status::Success; |
| } |
| |
| const app::ConcreteAttributePath attributePath(path.mEndpointId, path.mClusterId, path.mAttributeId); |
| |
| // Pre write attribute callback for all attribute changes, |
| // regardless of cluster. |
| imStatus = MatterPreAttributeChangeCallback(attributePath, input.dataType, emberAfAttributeSize(metadata), input.dataPtr); |
| if (imStatus != Protocols::InteractionModel::Status::Success) |
| { |
| return imStatus; |
| } |
| |
| // Pre-write attribute callback specific |
| // to the cluster that the attribute lives in. |
| status = emAfClusterPreAttributeChangedCallback(attributePath, input.dataType, emberAfAttributeSize(metadata), input.dataPtr); |
| |
| // Ignore the following write operation and return success |
| if (status == Status::WriteIgnored) |
| { |
| return Status::Success; |
| } |
| |
| if (status != Status::Success) |
| { |
| return status; |
| } |
| |
| // write the attribute |
| status = emAfReadOrWriteAttribute(&record, |
| nullptr, // metadata |
| input.dataPtr, |
| 0, // buffer size - unused |
| true); // write? |
| |
| if (status != Status::Success) |
| { |
| return status; |
| } |
| |
| // Save the attribute to persistent storage if needed |
| // The callee will weed out attributes that do not need to be stored. |
| emAfSaveAttributeToStorageIfNeeded(input.dataPtr, path.mEndpointId, path.mClusterId, metadata); |
| |
| if (input.markDirty != MarkAttributeDirty::kNo) |
| { |
| emberAfAttributeChanged(path.mEndpointId, path.mClusterId, path.mAttributeId, input.changeListener); |
| } |
| |
| // Post write attribute callback for all attributes changes, regardless |
| // of cluster. |
| MatterPostAttributeChangeCallback(attributePath, input.dataType, emberAfAttributeSize(metadata), input.dataPtr); |
| |
| // Post-write attribute callback specific |
| // to the cluster that the attribute lives in. |
| emAfClusterAttributeChangedCallback(attributePath); |
| |
| return Status::Success; |
| } |
| |
| } // anonymous namespace |
| |
| Status emberAfReadAttribute(EndpointId endpoint, ClusterId cluster, AttributeId attributeID, uint8_t * dataPtr, uint16_t readLength) |
| { |
| const EmberAfAttributeMetadata * metadata = nullptr; |
| EmberAfAttributeSearchRecord record; |
| Status status; |
| record.endpoint = endpoint; |
| record.clusterId = cluster; |
| record.attributeId = attributeID; |
| status = emAfReadOrWriteAttribute(&record, &metadata, dataPtr, readLength, |
| false); // write? |
| |
| // failed, print debug info |
| if (status == Status::ResourceExhausted) |
| { |
| ChipLogProgress(Zcl, "READ: attribute size too large for caller"); |
| } |
| |
| return status; |
| } |