From 9423a8cd0d2ed729b524e86c5b61c292091eebd7 Mon Sep 17 00:00:00 2001 From: Andrei Litvin Date: Thu, 24 Oct 2024 09:58:46 -0400 Subject: [PATCH] Use TLV-based encoding for ember data buffer. (#36172) * Use TLV-based encoding for ember data buffer. This saves 2K of flash on some test devices, being much more efficient in code size compared to using datamodel code. * Restyled by clang-format * Restyled by shfmt * Undo unrelated change * Add some casts to make android compiler happy * Updates based on code review * Added unit tests for min/max int64 values * Rename PascalString to PascalStringType * Fix rename * Restyle * Add helper methods inside odd sized integers to localize code logic * Restyled by clang-format * Fix up negative ranges * Fixed ranges * Fix signed max bug and update unit tests * Make android happy * Typo fix * Switch up unit tests * Update a nullable check to make use of ValueToNullValue * Add namespace prefix * Update src/app/codegen-data-model-provider/EmberDataBuffer.cpp Co-authored-by: Boris Zbarsky * Update src/app/codegen-data-model-provider/EmberDataBuffer.h Co-authored-by: Boris Zbarsky * Correct comments:signed, not unsigned * Use constructors for the buffer info * Rename things to EmberAttributeDataBuffer * Undo submodule updates * Restyled by clang-format --------- Co-authored-by: Restyled.io Co-authored-by: Andrei Litvin Co-authored-by: Boris Zbarsky --- scripts/build_coverage.sh | 12 +- src/app/codegen-data-model-provider/BUILD.gn | 2 + .../CodegenDataModelProvider_Write.cpp | 198 +----- .../EmberAttributeDataBuffer.cpp | 326 +++++++++ .../EmberAttributeDataBuffer.h | 101 +++ .../codegen-data-model-provider/model.cmake | 2 + src/app/codegen-data-model-provider/model.gni | 2 + .../tests/BUILD.gn | 5 +- .../tests/TestCodegenModelViaMocks.cpp | 10 +- .../tests/TestEmberAttributeDataBuffer.cpp | 616 ++++++++++++++++++ .../util/attribute-storage-null-handling.h | 1 - src/app/util/odd-sized-integers.h | 54 ++ 12 files changed, 1124 insertions(+), 205 deletions(-) create mode 100644 src/app/codegen-data-model-provider/EmberAttributeDataBuffer.cpp create mode 100644 src/app/codegen-data-model-provider/EmberAttributeDataBuffer.h create mode 100644 src/app/codegen-data-model-provider/tests/TestEmberAttributeDataBuffer.cpp diff --git a/scripts/build_coverage.sh b/scripts/build_coverage.sh index 9e7b4a0fe8a6d6..1bb68a2911c525 100755 --- a/scripts/build_coverage.sh +++ b/scripts/build_coverage.sh @@ -50,6 +50,7 @@ SUPPORTED_TESTS=(unit yaml all) CODE="core" TESTS="unit" skip_gn=false +TEST_TARGET=check help() { @@ -70,6 +71,7 @@ help() { 'unit': Run unit test to drive the coverage check. --default 'yaml': Run yaml test to drive the coverage check. 'all': Run unit & yaml test to drive the coverage check. + --target Specific test target to run (e.g. TestEmberAttributeBuffer.run) " } @@ -89,6 +91,9 @@ for i in "$@"; do TESTS="${i#*=}" shift ;; + --target=*) + TEST_TARGET="${i#*=}" + ;; -o=* | --output_root=*) OUTPUT_ROOT="${i#*=}" COVERAGE_ROOT="$OUTPUT_ROOT/coverage" @@ -121,20 +126,21 @@ if [ "$skip_gn" == false ]; then # Generates ninja files EXTRA_GN_ARGS="" if [[ "$TESTS" == "yaml" || "$TESTS" == "all" ]]; then - EXTRA_GN_ARGS="$EXTRA_GN_ARGS chip_build_all_clusters_app=true" + EXTRA_GN_ARGS="$EXTRA_GN_ARGS chip_build_all_clusters_app=true" else EXTRA_GN_ARGS="$EXTRA_GN_ARGS chip_build_tools=false" fi gn --root="$CHIP_ROOT" gen "$OUTPUT_ROOT" --args="use_coverage=true$EXTRA_GN_ARGS" - ninja -C "$OUTPUT_ROOT" # Run unit tests if [[ "$TESTS" == "unit" || "$TESTS" == "all" ]]; then - ninja -C "$OUTPUT_ROOT" check + ninja -C "$OUTPUT_ROOT" "$TEST_TARGET" fi # Run yaml tests if [[ "$TESTS" == "yaml" || "$TESTS" == "all" ]]; then + ninja -C "$OUTPUT_ROOT" + scripts/run_in_build_env.sh \ "./scripts/tests/run_test_suite.py \ --chip-tool ""$OUTPUT_ROOT/chip-tool \ diff --git a/src/app/codegen-data-model-provider/BUILD.gn b/src/app/codegen-data-model-provider/BUILD.gn index a3616b354c600e..3b414c595b9f67 100644 --- a/src/app/codegen-data-model-provider/BUILD.gn +++ b/src/app/codegen-data-model-provider/BUILD.gn @@ -24,6 +24,8 @@ import("//build_overrides/chip.gni") # CodegenDataModelProvider.h # CodegenDataModelProvider_Read.cpp # CodegenDataModelProvider_Write.cpp +# EmberAttributeDataBuffer.cpp +# EmberAttributeDataBuffer.h # EmberMetadata.cpp # EmberMetadata.h # Instance.cpp diff --git a/src/app/codegen-data-model-provider/CodegenDataModelProvider_Write.cpp b/src/app/codegen-data-model-provider/CodegenDataModelProvider_Write.cpp index 4c46a2a364d16b..51807fe98cf47d 100644 --- a/src/app/codegen-data-model-provider/CodegenDataModelProvider_Write.cpp +++ b/src/app/codegen-data-model-provider/CodegenDataModelProvider_Write.cpp @@ -21,6 +21,7 @@ #include #include #include +#include #include #include #include @@ -85,198 +86,6 @@ std::optional TryWriteViaAccessInterface(const ConcreteDataAttribute 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, @@ -414,7 +223,10 @@ DataModel::ActionReturnStatus CodegenDataModelProvider::WriteAttribute(const Dat } MutableByteSpan dataBuffer = gEmberAttributeIOBufferSpan; - ReturnErrorOnFailure(DecodeValueIntoEmberBuffer(decoder, *attributeMetadata, dataBuffer)); + { + Ember::EmberAttributeDataBuffer emberData(*attributeMetadata, dataBuffer); + ReturnErrorOnFailure(decoder.Decode(emberData)); + } Protocols::InteractionModel::Status status; diff --git a/src/app/codegen-data-model-provider/EmberAttributeDataBuffer.cpp b/src/app/codegen-data-model-provider/EmberAttributeDataBuffer.cpp new file mode 100644 index 00000000000000..ab8891657fde50 --- /dev/null +++ b/src/app/codegen-data-model-provider/EmberAttributeDataBuffer.cpp @@ -0,0 +1,326 @@ +/* + * 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 + +namespace chip { +namespace app { +namespace Ember { + +namespace { + +/// Maximum length of a string, inclusive +/// +/// the max size value (0xFF and 0xFFFF) is reserved for NULL representation so +/// it is not available +constexpr uint32_t MaxLength(EmberAttributeDataBuffer::PascalStringType s) +{ + if (s == EmberAttributeDataBuffer::PascalStringType::kShort) + { + return std::numeric_limits::max() - 1; + } + // EmberAttributeBuffer::PascalStringType::kLong: + return std::numeric_limits::max() - 1; +} + +struct UnsignedDecodeInfo +{ + unsigned byteCount; + uint64_t maxValue; + + constexpr UnsignedDecodeInfo(unsigned bytes) : byteCount(bytes), maxValue(NumericLimits::MaxUnsignedValue(bytes)) {} +}; + +constexpr UnsignedDecodeInfo GetUnsignedDecodeInfo(EmberAfAttributeType type) +{ + + switch (type) + { + case ZCL_INT8U_ATTRIBUTE_TYPE: // Unsigned 8-bit integer + return UnsignedDecodeInfo(1); + case ZCL_INT16U_ATTRIBUTE_TYPE: // Unsigned 16-bit integer + return UnsignedDecodeInfo(2); + case ZCL_INT24U_ATTRIBUTE_TYPE: // Unsigned 24-bit integer + return UnsignedDecodeInfo(3); + case ZCL_INT32U_ATTRIBUTE_TYPE: // Unsigned 32-bit integer + return UnsignedDecodeInfo(4); + case ZCL_INT40U_ATTRIBUTE_TYPE: // Unsigned 40-bit integer + return UnsignedDecodeInfo(5); + case ZCL_INT48U_ATTRIBUTE_TYPE: // Unsigned 48-bit integer + return UnsignedDecodeInfo(6); + case ZCL_INT56U_ATTRIBUTE_TYPE: // Unsigned 56-bit integer + return UnsignedDecodeInfo(7); + case ZCL_INT64U_ATTRIBUTE_TYPE: // Unsigned 64-bit integer + return UnsignedDecodeInfo(8); + } + chipDie(); +} + +struct SignedDecodeInfo +{ + unsigned byteCount; + int64_t minValue; + int64_t maxValue; + + constexpr SignedDecodeInfo(unsigned bytes) : + byteCount(bytes), minValue(NumericLimits::MinSignedValue(bytes)), maxValue(NumericLimits::MaxSignedValue(bytes)) + {} +}; + +constexpr SignedDecodeInfo GetSignedDecodeInfo(EmberAfAttributeType type) +{ + + switch (type) + { + case ZCL_INT8S_ATTRIBUTE_TYPE: // Signed 8-bit integer + return SignedDecodeInfo(1); + case ZCL_INT16S_ATTRIBUTE_TYPE: // Signed 16-bit integer + return SignedDecodeInfo(2); + case ZCL_INT24S_ATTRIBUTE_TYPE: // Signed 24-bit integer + return SignedDecodeInfo(3); + case ZCL_INT32S_ATTRIBUTE_TYPE: // Signed 32-bit integer + return SignedDecodeInfo(4); + case ZCL_INT40S_ATTRIBUTE_TYPE: // Signed 40-bit integer + return SignedDecodeInfo(5); + case ZCL_INT48S_ATTRIBUTE_TYPE: // Signed 48-bit integer + return SignedDecodeInfo(6); + case ZCL_INT56S_ATTRIBUTE_TYPE: // Signed 56-bit integer + return SignedDecodeInfo(7); + case ZCL_INT64S_ATTRIBUTE_TYPE: // Signed 64-bit integer + return SignedDecodeInfo(8); + } + chipDie(); +} + +} // namespace + +CHIP_ERROR EmberAttributeDataBuffer::DecodeUnsignedInteger(chip::TLV::TLVReader & reader, EndianWriter & writer) +{ + UnsignedDecodeInfo info = GetUnsignedDecodeInfo(mAttributeType); + + // Any size of integer can be read by TLV getting 64-bit integers + uint64_t value; + + if (reader.GetType() == TLV::kTLVType_Null) + { + // we know mIsNullable due to the check at the top of ::Decode + value = NumericLimits::UnsignedMaxValueToNullValue(info.maxValue); + } + else + { + ReturnErrorOnFailure(reader.Get(value)); + + bool valid = + // Value is in [0, max] RANGE + (value <= info.maxValue) + // Nullable values reserve a specific value to mean NULL + && !(mIsNullable && (value == NumericLimits::UnsignedMaxValueToNullValue(info.maxValue))); + + VerifyOrReturnError(valid, CHIP_IM_GLOBAL_STATUS(ConstraintError)); + } + + writer.EndianPut(value, info.byteCount); + return CHIP_NO_ERROR; +} + +CHIP_ERROR EmberAttributeDataBuffer::DecodeSignedInteger(chip::TLV::TLVReader & reader, EndianWriter & writer) +{ + SignedDecodeInfo info = GetSignedDecodeInfo(mAttributeType); + + // Any size of integer can be read by TLV getting 64-bit integers + int64_t value; + + if (reader.GetType() == TLV::kTLVType_Null) + { + // we know mIsNullable due to the check at the top of ::Decode + value = NumericLimits::SignedMinValueToNullValue(info.minValue); + } + else + { + ReturnErrorOnFailure(reader.Get(value)); + + bool valid = + // Value is in [min, max] RANGE + ((value >= info.minValue) && (value <= info.maxValue)) + // Nullable values reserve a specific value to mean NULL + && !(mIsNullable && (value == NumericLimits::SignedMinValueToNullValue(info.minValue))); + + VerifyOrReturnError(valid, CHIP_IM_GLOBAL_STATUS(ConstraintError)); + } + writer.EndianPutSigned(value, info.byteCount); + return CHIP_NO_ERROR; +} + +CHIP_ERROR EmberAttributeDataBuffer::DecodeAsString(chip::TLV::TLVReader & reader, PascalStringType stringType, + TLV::TLVType tlvType, EndianWriter & writer) +{ + // Handle null first, then the actual data + if (reader.GetType() == TLV::kTLVType_Null) + { + // we know mIsNullable due to the check at the top of ::Decode + switch (stringType) + { + case PascalStringType::kShort: + writer.Put8(NumericAttributeTraits::kNullValue); + break; + case PascalStringType::kLong: + writer.Put16(NumericAttributeTraits::kNullValue); + break; + } + return CHIP_NO_ERROR; + } + + const uint32_t stringLength = reader.GetLength(); + + VerifyOrReturnError(reader.GetType() == tlvType, CHIP_ERROR_WRONG_TLV_TYPE); + VerifyOrReturnError(stringLength <= MaxLength(stringType), CHIP_ERROR_INVALID_ARGUMENT); + + // Size is a prefix, where 0xFF/0xFFFF is the null marker (if applicable) + switch (stringType) + { + case PascalStringType::kShort: + writer.Put8(static_cast(stringLength)); + break; + case PascalStringType::kLong: + writer.Put16(static_cast(stringLength)); + break; + } + + // data copy + const uint8_t * tlvData; + ReturnErrorOnFailure(reader.GetDataPtr(tlvData)); + writer.Put(tlvData, stringLength); + + return CHIP_NO_ERROR; +} + +CHIP_ERROR EmberAttributeDataBuffer::Decode(chip::TLV::TLVReader & reader) +{ + // all methods below assume that nullable setting matches (this is to reduce code size + // even though clarity suffers) + VerifyOrReturnError(mIsNullable || reader.GetType() != TLV::kTLVType_Null, CHIP_ERROR_WRONG_TLV_TYPE); + + EndianWriter endianWriter(mDataBuffer.data(), mDataBuffer.size()); + + switch (mAttributeType) + { + case ZCL_BOOLEAN_ATTRIBUTE_TYPE: // Boolean + // Boolean values: + // 0x00 is FALSE + // 0x01 is TRUE + // 0xFF is NULL + if (reader.GetType() == TLV::kTLVType_Null) + { + // we know mIsNullable due to the check at the top of ::Decode + endianWriter.Put8(NumericAttributeTraits::kNullValue); + } + else + { + bool value; + ReturnErrorOnFailure(reader.Get(value)); + endianWriter.Put8(value ? 1 : 0); + } + break; + case ZCL_INT8U_ATTRIBUTE_TYPE: // Unsigned 8-bit integer + case ZCL_INT16U_ATTRIBUTE_TYPE: // Unsigned 16-bit integer + case ZCL_INT24U_ATTRIBUTE_TYPE: // Unsigned 24-bit integer + case ZCL_INT32U_ATTRIBUTE_TYPE: // Unsigned 32-bit integer + case ZCL_INT40U_ATTRIBUTE_TYPE: // Unsigned 40-bit integer + case ZCL_INT48U_ATTRIBUTE_TYPE: // Unsigned 48-bit integer + case ZCL_INT56U_ATTRIBUTE_TYPE: // Unsigned 56-bit integer + case ZCL_INT64U_ATTRIBUTE_TYPE: // Unsigned 64-bit integer + ReturnErrorOnFailure(DecodeUnsignedInteger(reader, endianWriter)); + break; + case ZCL_INT8S_ATTRIBUTE_TYPE: // Signed 8-bit integer + case ZCL_INT16S_ATTRIBUTE_TYPE: // Signed 16-bit integer + case ZCL_INT24S_ATTRIBUTE_TYPE: // Signed 24-bit integer + case ZCL_INT32S_ATTRIBUTE_TYPE: // Signed 32-bit integer + case ZCL_INT40S_ATTRIBUTE_TYPE: // Signed 40-bit integer + case ZCL_INT48S_ATTRIBUTE_TYPE: // Signed 48-bit integer + case ZCL_INT56S_ATTRIBUTE_TYPE: // Signed 56-bit integer + case ZCL_INT64S_ATTRIBUTE_TYPE: // Signed 64-bit integer + ReturnErrorOnFailure(DecodeSignedInteger(reader, endianWriter)); + break; + case ZCL_SINGLE_ATTRIBUTE_TYPE: { // 32-bit float + float value; + if (reader.GetType() == TLV::kTLVType_Null) + { + // we know mIsNullable due to the check at the top of ::Decode + NumericAttributeTraits::SetNull(value); + } + else + { + + ReturnErrorOnFailure(reader.Get(value)); + } + endianWriter.Put(&value, sizeof(value)); + break; + } + case ZCL_DOUBLE_ATTRIBUTE_TYPE: { // 64-bit float + double value; + if (reader.GetType() == TLV::kTLVType_Null) + { + // we know mIsNullable due to the check at the top of ::Decode + NumericAttributeTraits::SetNull(value); + } + else + { + ReturnErrorOnFailure(reader.Get(value)); + } + endianWriter.Put(&value, sizeof(value)); + break; + } + case ZCL_CHAR_STRING_ATTRIBUTE_TYPE: // Char string + ReturnErrorOnFailure(DecodeAsString(reader, PascalStringType::kShort, TLV::kTLVType_UTF8String, endianWriter)); + break; + case ZCL_LONG_CHAR_STRING_ATTRIBUTE_TYPE: + ReturnErrorOnFailure(DecodeAsString(reader, PascalStringType::kLong, TLV::kTLVType_UTF8String, endianWriter)); + break; + case ZCL_OCTET_STRING_ATTRIBUTE_TYPE: // Octet string + ReturnErrorOnFailure(DecodeAsString(reader, PascalStringType::kShort, TLV::kTLVType_ByteString, endianWriter)); + break; + case ZCL_LONG_OCTET_STRING_ATTRIBUTE_TYPE: + ReturnErrorOnFailure(DecodeAsString(reader, PascalStringType::kLong, TLV::kTLVType_ByteString, endianWriter)); + break; + default: + ChipLogError(DataManagement, "Attribute type 0x%x not handled", mAttributeType); + return CHIP_IM_GLOBAL_STATUS(Failure); + } + + size_t written; + if (!endianWriter.Fit(written)) + { + return CHIP_ERROR_NO_MEMORY; + } + + mDataBuffer.reduce_size(written); + return CHIP_NO_ERROR; +} + +} // namespace Ember +} // namespace app +} // namespace chip diff --git a/src/app/codegen-data-model-provider/EmberAttributeDataBuffer.h b/src/app/codegen-data-model-provider/EmberAttributeDataBuffer.h new file mode 100644 index 00000000000000..a4f24c78c0476b --- /dev/null +++ b/src/app/codegen-data-model-provider/EmberAttributeDataBuffer.h @@ -0,0 +1,101 @@ +/* + * 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 +#include +#include +#include + +namespace chip { +namespace app { +namespace Ember { + +/// This class represents a pointer to an ember-encoded attribute in a specific memory location. +/// +/// Ember attributes are stored as raw bytes for numeric types (i.e. memcpy-like storage except +/// unaligned) and strings are Pascal-like (short with 1-byte length prefix or long with 2-byte length +/// prefix). +/// +/// Class is to be used as a one-shot: +/// - create it out of metadata + data span +/// - call Decode (which modifies the input data span) +class EmberAttributeDataBuffer +{ +public: + enum class PascalStringType + { + kShort, + kLong, + }; + + static constexpr bool kIsFabricScoped = false; + + EmberAttributeDataBuffer(const EmberAfAttributeMetadata * meta, MutableByteSpan & data) : + mIsNullable(meta->IsNullable()), mAttributeType(chip::app::Compatibility::Internal::AttributeBaseType(meta->attributeType)), + mDataBuffer(data) + {} + + /// Reads the data pointed into by `reader` and updates the data + /// internally into mDataBuffer (which is then reflected outwards) + /// + /// Generally should be called ONLY ONCE as the internal mutable byte span gets + /// modified by this call. + CHIP_ERROR Decode(chip::TLV::TLVReader & reader); + +private: +#if CHIP_CONFIG_BIG_ENDIAN_TARGET + using EndianWriter = Encoding::BigEndian::BufferWriter; +#else + using EndianWriter = Encoding::LittleEndian::BufferWriter; +#endif + /// Decodes the UNSIGNED integer stored in `reader` and places its content into `writer` + /// Takes into account internal mIsNullable. + CHIP_ERROR DecodeUnsignedInteger(chip::TLV::TLVReader & reader, EndianWriter & writer); + + /// Decodes the SIGNED integer stored in `reader` and places its content into `writer` + /// Takes into account internal mIsNullable. + CHIP_ERROR DecodeSignedInteger(chip::TLV::TLVReader & reader, EndianWriter & writer); + + /// Decodes the string/byte string contained in `reader` and stores it into `writer`. + /// String is encoded using a pascal-prefix of size `stringType`. + /// Takes into account internal mIsNullable. + /// + /// The string in `reader` is expected to be of type `tlvType` + CHIP_ERROR DecodeAsString(chip::TLV::TLVReader & reader, PascalStringType stringType, TLV::TLVType tlvType, + EndianWriter & writer); + + const bool mIsNullable; // Contains if the attribute metadata marks the field as NULLABLE + const EmberAfAttributeType mAttributeType; // Initialized with the attribute type from the metadata + MutableByteSpan & mDataBuffer; // output buffer, modified by `Decode` +}; + +} // namespace Ember + +namespace DataModel { + +/// Helper method to forward the decode of this type to the class specific implementation +inline CHIP_ERROR Decode(TLV::TLVReader & reader, Ember::EmberAttributeDataBuffer & buffer) +{ + return buffer.Decode(reader); +} + +} // namespace DataModel +} // namespace app +} // namespace chip diff --git a/src/app/codegen-data-model-provider/model.cmake b/src/app/codegen-data-model-provider/model.cmake index 777219549e44eb..5ce4fbc84994ac 100644 --- a/src/app/codegen-data-model-provider/model.cmake +++ b/src/app/codegen-data-model-provider/model.cmake @@ -19,6 +19,8 @@ SET(CODEGEN_DATA_MODEL_SOURCES "${BASE_DIR}/CodegenDataModelProvider.h" "${BASE_DIR}/CodegenDataModelProvider_Read.cpp" "${BASE_DIR}/CodegenDataModelProvider_Write.cpp" + "${BASE_DIR}/EmberAttributeDataBuffer.cpp" + "${BASE_DIR}/EmberAttributeDataBuffer.h" "${BASE_DIR}/EmberMetadata.cpp" "${BASE_DIR}/EmberMetadata.h" "${BASE_DIR}/Instance.cpp" diff --git a/src/app/codegen-data-model-provider/model.gni b/src/app/codegen-data-model-provider/model.gni index b5909fb1c4f6f5..4205c6fcd73c25 100644 --- a/src/app/codegen-data-model-provider/model.gni +++ b/src/app/codegen-data-model-provider/model.gni @@ -29,6 +29,8 @@ codegen_data_model_SOURCES = [ "${chip_root}/src/app/codegen-data-model-provider/CodegenDataModelProvider.h", "${chip_root}/src/app/codegen-data-model-provider/CodegenDataModelProvider_Read.cpp", "${chip_root}/src/app/codegen-data-model-provider/CodegenDataModelProvider_Write.cpp", + "${chip_root}/src/app/codegen-data-model-provider/EmberAttributeDataBuffer.cpp", + "${chip_root}/src/app/codegen-data-model-provider/EmberAttributeDataBuffer.h", "${chip_root}/src/app/codegen-data-model-provider/EmberMetadata.cpp", "${chip_root}/src/app/codegen-data-model-provider/EmberMetadata.h", "${chip_root}/src/app/codegen-data-model-provider/Instance.cpp", diff --git a/src/app/codegen-data-model-provider/tests/BUILD.gn b/src/app/codegen-data-model-provider/tests/BUILD.gn index d8f82169b976a9..5bcfb9d911ab55 100644 --- a/src/app/codegen-data-model-provider/tests/BUILD.gn +++ b/src/app/codegen-data-model-provider/tests/BUILD.gn @@ -52,7 +52,10 @@ source_set("mock_model") { chip_test_suite("tests") { output_name = "libCodegenDataModelProviderTests" - test_sources = [ "TestCodegenModelViaMocks.cpp" ] + test_sources = [ + "TestCodegenModelViaMocks.cpp", + "TestEmberAttributeDataBuffer.cpp", + ] cflags = [ "-Wconversion" ] diff --git a/src/app/codegen-data-model-provider/tests/TestCodegenModelViaMocks.cpp b/src/app/codegen-data-model-provider/tests/TestCodegenModelViaMocks.cpp index 3840de9c8151dc..8ce369b7bd8220 100644 --- a/src/app/codegen-data-model-provider/tests/TestCodegenModelViaMocks.cpp +++ b/src/app/codegen-data-model-provider/tests/TestCodegenModelViaMocks.cpp @@ -2133,9 +2133,7 @@ TEST(TestCodegenModelViaMocks, EmberTestWriteOutOfRepresentableRangeOddIntegerNo using NullableType = chip::app::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.GetRequest(), decoder), CHIP_ERROR_INVALID_ARGUMENT); + ASSERT_EQ(model.WriteAttribute(test.GetRequest(), decoder), CHIP_IM_GLOBAL_STATUS(ConstraintError)); } TEST(TestCodegenModelViaMocks, EmberTestWriteOutOfRepresentableRangeOddIntegerNullable) @@ -2151,12 +2149,10 @@ TEST(TestCodegenModelViaMocks, EmberTestWriteOutOfRepresentableRangeOddIntegerNu using NullableType = chip::app::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.GetRequest(), decoder), CHIP_ERROR_INVALID_ARGUMENT); + ASSERT_EQ(model.WriteAttribute(test.GetRequest(), decoder), CHIP_IM_GLOBAL_STATUS(ConstraintError)); } -TEST(TestCodegenModelViaMoceNullValueToNullables, EmberAttributeWriteBasicTypesLowestValue) +TEST(TestCodegenModelViaMocksNullValueToNullables, EmberAttributeWriteBasicTypesLowestValue) { TestEmberScalarTypeWrite(-127); TestEmberScalarTypeWrite(-32767); diff --git a/src/app/codegen-data-model-provider/tests/TestEmberAttributeDataBuffer.cpp b/src/app/codegen-data-model-provider/tests/TestEmberAttributeDataBuffer.cpp new file mode 100644 index 00000000000000..bc089c8a798a6a --- /dev/null +++ b/src/app/codegen-data-model-provider/tests/TestEmberAttributeDataBuffer.cpp @@ -0,0 +1,616 @@ +/* + * 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 + +using namespace chip; +using namespace chip::app; + +namespace { + +/// encodes a simple value in a TLV buffer +class TLVEncodedValue +{ +public: + TLVEncodedValue() = default; + ~TLVEncodedValue() = default; + + template + TLV::TLVReader EncodeValue(const T & value) + { + const auto kTag = TLV::ContextTag(AttributeDataIB::Tag::kData); + + TLV::TLVWriter writer; + writer.Init(mBuffer, sizeof(mBuffer)); + + TLV::TLVType outer; + + VerifyOrDie(writer.StartContainer(TLV::AnonymousTag(), TLV::kTLVType_Structure, outer) == CHIP_NO_ERROR); + VerifyOrDie(DataModel::Encode(writer, kTag, value) == CHIP_NO_ERROR); + VerifyOrDie(writer.EndContainer(outer) == CHIP_NO_ERROR); + + VerifyOrDie(writer.Finalize() == CHIP_NO_ERROR); + size_t fill = writer.GetLengthWritten(); + + TLV::TLVReader reader; + reader.Init(mBuffer, fill); + VerifyOrDie(reader.Next() == CHIP_NO_ERROR); + VerifyOrDie(reader.GetTag() == TLV::AnonymousTag()); + VerifyOrDie(reader.EnterContainer(outer) == CHIP_NO_ERROR); + VerifyOrDie(reader.Next() == CHIP_NO_ERROR); + VerifyOrDie(reader.GetTag() == kTag); + + return reader; + } + +private: + static constexpr size_t kMaxSize = 128; + uint8_t mBuffer[kMaxSize]; +}; + +class EncodeResult +{ +public: + explicit EncodeResult() = default; + EncodeResult(CHIP_ERROR error) : mResult(error) { VerifyOrDie(error != CHIP_NO_ERROR); } + + static EncodeResult Ok() { return EncodeResult(); } + + bool IsSuccess() const { return !mResult.has_value(); } + + bool operator==(const CHIP_ERROR & other) const { return mResult.has_value() && (*mResult == other); } + + const std::optional & Value() const { return mResult; } + +private: + std::optional mResult; +}; + +/// Validates that an encoded value in ember takes a specific format +template +class EncodeTester +{ +public: + EncodeTester(const EmberAfAttributeMetadata * meta) : mMetaData(meta) {} + ~EncodeTester() = default; + + template + EncodeResult TryEncode(const T & value, const uint8_t (&arr)[N]) + { + ByteSpan expected(arr); + MutableByteSpan out_span(mEmberAttributeDataBuffer); + Ember::EmberAttributeDataBuffer buffer(mMetaData, out_span); + + TLVEncodedValue tlvEncoded; + TLV::TLVReader reader = tlvEncoded.EncodeValue(value); + + CHIP_ERROR err = buffer.Decode(reader); + if (err != CHIP_NO_ERROR) + { + ChipLogError(Test, "Decoding failed: %" CHIP_ERROR_FORMAT, err.Format()); + return err; + } + + if (expected.size() != out_span.size()) + { + ChipLogError(Test, "Decode mismatch in size: expected %u, got %u", static_cast(expected.size()), + static_cast(out_span.size())); + return CHIP_ERROR_INTERNAL; + } + + if (!expected.data_equal(out_span)) + { + ChipLogError(Test, "Decode mismatch in content for %u bytes", static_cast(expected.size())); + return CHIP_ERROR_INTERNAL; + } + + return EncodeResult::Ok(); + } + +private: + const EmberAfAttributeMetadata * mMetaData; + uint8_t mEmberAttributeDataBuffer[kMaxSize]; +}; + +const EmberAfAttributeMetadata * CreateFakeMeta(EmberAfAttributeType type, bool nullable) +{ + static EmberAfAttributeMetadata meta = { + .defaultValue = EmberAfDefaultOrMinMaxAttributeValue(static_cast(nullptr)), + .attributeId = 0, + .size = 0, // likely not valid, however not used for tests + .attributeType = ZCL_UNKNOWN_ATTRIBUTE_TYPE, + .mask = 0, + }; + + meta.attributeType = type; + meta.mask = nullable ? ATTRIBUTE_MASK_NULLABLE : 0; + + return &meta; +} + +} // namespace +// +namespace pw { + +// Pretty format in case of errors +template <> +StatusWithSize ToString(const EncodeResult & result, pw::span buffer) +{ + const std::optional & value = result.Value(); + + if (!value.has_value()) + { + return pw::string::Format(buffer, "SuccessResult"); + } + + return pw::string::Format(buffer, "FailureResult:CHIP_ERROR:<%" CHIP_ERROR_FORMAT ">", value->Format()); +} + +} // namespace pw + +// All the tests below assume buffer ordering in little endian format +// Since currently all chip platforms in CI are little endian, we just kept tests +// as-is +static_assert(!CHIP_CONFIG_BIG_ENDIAN_TARGET); + +TEST(TestEmberAttributeBuffer, TestEncodeUnsignedTypes) +{ + { + EncodeTester tester(CreateFakeMeta(ZCL_INT8U_ATTRIBUTE_TYPE, false /* nullable */)); + + EXPECT_TRUE(tester.TryEncode(0, { 0 }).IsSuccess()); + EXPECT_TRUE(tester.TryEncode(123, { 123 }).IsSuccess()); + EXPECT_TRUE(tester.TryEncode(0xFD, { 0xFD }).IsSuccess()); + EXPECT_TRUE(tester.TryEncode(255, { 0xFF }).IsSuccess()); + } + + { + EncodeTester tester(CreateFakeMeta(ZCL_INT8U_ATTRIBUTE_TYPE, true /* nullable */)); + + EXPECT_TRUE(tester.TryEncode(0, { 0 }).IsSuccess()); + EXPECT_TRUE(tester.TryEncode(123, { 123 }).IsSuccess()); + EXPECT_TRUE(tester.TryEncode(0xFD, { 0xFD }).IsSuccess()); + EXPECT_TRUE(tester.TryEncode>(DataModel::NullNullable, { 0xFF }).IsSuccess()); + + // Not allowed to encode null-equivalent + EXPECT_EQ(tester.TryEncode(0xFF, { 0xFF }), CHIP_IM_GLOBAL_STATUS(ConstraintError)); + } + + { + EncodeTester tester(CreateFakeMeta(ZCL_INT16U_ATTRIBUTE_TYPE, false /* nullable */)); + + EXPECT_TRUE(tester.TryEncode(0, { 0, 0 }).IsSuccess()); + EXPECT_TRUE(tester.TryEncode(123, { 123, 0 }).IsSuccess()); + EXPECT_TRUE(tester.TryEncode(0xFD, { 0xFD, 0 }).IsSuccess()); + EXPECT_TRUE(tester.TryEncode(255, { 0xFF, 0 }).IsSuccess()); + EXPECT_TRUE(tester.TryEncode(0xABCD, { 0xCD, 0xAB }).IsSuccess()); + EXPECT_TRUE(tester.TryEncode(0xFFFF, { 0xFF, 0xFF }).IsSuccess()); + } + + { + EncodeTester tester(CreateFakeMeta(ZCL_INT16U_ATTRIBUTE_TYPE, true /* nullable */)); + + EXPECT_TRUE(tester.TryEncode(0, { 0, 0 }).IsSuccess()); + EXPECT_TRUE(tester.TryEncode(123, { 123, 0 }).IsSuccess()); + EXPECT_TRUE(tester.TryEncode(0xFD, { 0xFD, 0 }).IsSuccess()); + EXPECT_TRUE(tester.TryEncode(255, { 0xFF, 0 }).IsSuccess()); + EXPECT_TRUE(tester.TryEncode(0xABCD, { 0xCD, 0xAB }).IsSuccess()); + EXPECT_TRUE(tester.TryEncode>(DataModel::NullNullable, { 0xFF, 0xFF }).IsSuccess()); + + // Not allowed to encode null-equivalent + EXPECT_EQ(tester.TryEncode(0xFFFF, { 0xFF, 0xFF }), CHIP_IM_GLOBAL_STATUS(ConstraintError)); + } + { + EncodeTester tester(CreateFakeMeta(ZCL_INT64U_ATTRIBUTE_TYPE, true /* nullable */)); + + EXPECT_TRUE(tester.TryEncode(0, { 0, 0, 0, 0, 0, 0, 0, 0 }).IsSuccess()); + EXPECT_TRUE(tester.TryEncode(0x1234567, { 0x67, 0x45, 0x23, 0x01, 0, 0, 0, 0 }).IsSuccess()); + EXPECT_TRUE(tester.TryEncode(0xAABBCCDDEEFF1122, { 0x22, 0x11, 0xFF, 0xEE, 0xDD, 0xCC, 0xBB, 0xAA }).IsSuccess()); + EXPECT_TRUE( + tester.TryEncode(std::numeric_limits::max() - 1, { 0xFE, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF }) + .IsSuccess()); + + EXPECT_TRUE(tester + .TryEncode>(DataModel::NullNullable, + { 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF }) + .IsSuccess()); + + EXPECT_EQ( + tester.TryEncode(std::numeric_limits::max(), { 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF }), + CHIP_IM_GLOBAL_STATUS(ConstraintError)); + } + { + EncodeTester tester(CreateFakeMeta(ZCL_INT64U_ATTRIBUTE_TYPE, false /* nullable */)); + + // we should be able to encode the maximum value + EXPECT_TRUE( + tester.TryEncode(std::numeric_limits::max(), { 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF }) + .IsSuccess()); + } + + /// Odd sized integers + { + EncodeTester tester(CreateFakeMeta(ZCL_INT24U_ATTRIBUTE_TYPE, false /* nullable */)); + EXPECT_TRUE(tester.TryEncode(0, { 0, 0, 0 }).IsSuccess()); + EXPECT_TRUE(tester.TryEncode(0x123456, { 0x56, 0x34, 0x12 }).IsSuccess()); + EXPECT_TRUE(tester.TryEncode(0xFFFFFF, { 0xFF, 0xFF, 0xFF }).IsSuccess()); + + // Out of range + EXPECT_EQ(tester.TryEncode(0x1000000, { 0 }), CHIP_IM_GLOBAL_STATUS(ConstraintError)); + EXPECT_EQ(tester.TryEncode(0xFF000000, { 0 }), CHIP_IM_GLOBAL_STATUS(ConstraintError)); + } + { + EncodeTester tester(CreateFakeMeta(ZCL_INT24U_ATTRIBUTE_TYPE, true /* nullable */)); + EXPECT_TRUE(tester.TryEncode(0, { 0, 0, 0 }).IsSuccess()); + EXPECT_TRUE(tester.TryEncode(0x123456, { 0x56, 0x34, 0x12 }).IsSuccess()); + EXPECT_TRUE(tester.TryEncode>(DataModel::NullNullable, { 0xFF, 0xFF, 0xFF }).IsSuccess()); + + // Out of range + EXPECT_EQ(tester.TryEncode(0x1000000, { 0 }), CHIP_IM_GLOBAL_STATUS(ConstraintError)); + // cannot encode null equivalent value + EXPECT_EQ(tester.TryEncode(0xFFFFFF, { 0x56, 0x34, 0x12 }), CHIP_IM_GLOBAL_STATUS(ConstraintError)); + } + + { + EncodeTester tester(CreateFakeMeta(ZCL_INT40U_ATTRIBUTE_TYPE, true /* nullable */)); + EXPECT_TRUE(tester.TryEncode(0, { 0, 0, 0, 0, 0 }).IsSuccess()); + EXPECT_TRUE(tester.TryEncode(0x123456, { 0x56, 0x34, 0x12, 0, 0 }).IsSuccess()); + EXPECT_TRUE(tester.TryEncode(0x123456FFFF, { 0xFF, 0xFF, 0x56, 0x34, 0x12 }).IsSuccess()); + EXPECT_TRUE( + tester.TryEncode>(DataModel::NullNullable, { 0xFF, 0xFF, 0xFF, 0xFF, 0xFF }).IsSuccess()); + + // Out of range + EXPECT_EQ(tester.TryEncode(0x10011001100, { 0 }), CHIP_IM_GLOBAL_STATUS(ConstraintError)); + // cannot encode null equivalent value + EXPECT_EQ(tester.TryEncode(0xFFFFFFFFFF, { 0 }), CHIP_IM_GLOBAL_STATUS(ConstraintError)); + } + + // Double-check tests, not as exhaustive, to cover all other unsigned values and get + // more test line coverage + { + EncodeTester tester(CreateFakeMeta(ZCL_INT32U_ATTRIBUTE_TYPE, true /* nullable */)); + EXPECT_TRUE(tester.TryEncode(0x1234, { 0x34, 0x12, 0, 0 }).IsSuccess()); + } + { + EncodeTester tester(CreateFakeMeta(ZCL_INT48U_ATTRIBUTE_TYPE, true /* nullable */)); + EXPECT_TRUE(tester.TryEncode(0x1234, { 0x34, 0x12, 0, 0, 0, 0 }).IsSuccess()); + } + { + EncodeTester tester(CreateFakeMeta(ZCL_INT56U_ATTRIBUTE_TYPE, true /* nullable */)); + EXPECT_TRUE(tester.TryEncode(0x1234, { 0x34, 0x12, 0, 0, 0, 0, 0 }).IsSuccess()); + } +} + +TEST(TestEmberAttributeBuffer, TestEncodeSignedTypes) +{ + { + EncodeTester tester(CreateFakeMeta(ZCL_INT8S_ATTRIBUTE_TYPE, false /* nullable */)); + + EXPECT_TRUE(tester.TryEncode(0, { 0 }).IsSuccess()); + EXPECT_TRUE(tester.TryEncode(123, { 123 }).IsSuccess()); + EXPECT_TRUE(tester.TryEncode(127, { 127 }).IsSuccess()); + EXPECT_TRUE(tester.TryEncode(-10, { 0xF6 }).IsSuccess()); + EXPECT_TRUE(tester.TryEncode(-128, { 0x80 }).IsSuccess()); + } + + { + EncodeTester tester(CreateFakeMeta(ZCL_INT8S_ATTRIBUTE_TYPE, true /* nullable */)); + + EXPECT_TRUE(tester.TryEncode(0, { 0 }).IsSuccess()); + EXPECT_TRUE(tester.TryEncode(123, { 123 }).IsSuccess()); + EXPECT_TRUE(tester.TryEncode(127, { 127 }).IsSuccess()); + EXPECT_TRUE(tester.TryEncode(-10, { 0xF6 }).IsSuccess()); + EXPECT_TRUE(tester.TryEncode(-127, { 0x81 }).IsSuccess()); + + // NULL canot be encoded + EXPECT_EQ(tester.TryEncode(std::numeric_limits::min(), { 0x80 }), CHIP_IM_GLOBAL_STATUS(ConstraintError)); + } + { + + EncodeTester tester(CreateFakeMeta(ZCL_INT16S_ATTRIBUTE_TYPE, false /* nullable */)); + + EXPECT_TRUE(tester.TryEncode(0, { 0, 0 }).IsSuccess()); + EXPECT_TRUE(tester.TryEncode(123, { 123, 0 }).IsSuccess()); + EXPECT_TRUE(tester.TryEncode(127, { 127, 0 }).IsSuccess()); + EXPECT_TRUE(tester.TryEncode(-10, { 0xF6, 0xFF }).IsSuccess()); + EXPECT_TRUE(tester.TryEncode(-128, { 0x80, 0xFF }).IsSuccess()); + EXPECT_TRUE(tester.TryEncode(-1234, { 0x2E, 0xFB }).IsSuccess()); + EXPECT_TRUE(tester.TryEncode(std::numeric_limits::min(), { 0x0, 0x80 }).IsSuccess()); + } + + { + EncodeTester tester(CreateFakeMeta(ZCL_INT16S_ATTRIBUTE_TYPE, true /* nullable */)); + + EXPECT_TRUE(tester.TryEncode(0, { 0, 0 }).IsSuccess()); + EXPECT_TRUE(tester.TryEncode(123, { 123, 0 }).IsSuccess()); + EXPECT_TRUE(tester.TryEncode(127, { 127, 0 }).IsSuccess()); + EXPECT_TRUE(tester.TryEncode(-10, { 0xF6, 0xFF }).IsSuccess()); + + // NULL canot be encoded + EXPECT_EQ(tester.TryEncode(std::numeric_limits::min(), { 0x80 }), CHIP_IM_GLOBAL_STATUS(ConstraintError)); + } + + // Odd size integers + { + EncodeTester tester(CreateFakeMeta(ZCL_INT24S_ATTRIBUTE_TYPE, false /* nullable */)); + + EXPECT_TRUE(tester.TryEncode(0, { 0, 0, 0 }).IsSuccess()); + EXPECT_TRUE(tester.TryEncode(0x123456, { 0x56, 0x34, 0x12 }).IsSuccess()); + EXPECT_TRUE(tester.TryEncode(-1, { 0xFF, 0xFF, 0xFF }).IsSuccess()); + EXPECT_TRUE(tester.TryEncode(-10, { 0xF6, 0xFF, 0xFF }).IsSuccess()); + EXPECT_TRUE(tester.TryEncode(-1234, { 0x2E, 0xFB, 0xFF }).IsSuccess()); + + // Out of range + EXPECT_EQ(tester.TryEncode(0x1000000, { 0 }), CHIP_IM_GLOBAL_STATUS(ConstraintError)); + EXPECT_EQ(tester.TryEncode(0x0F000000, { 0 }), CHIP_IM_GLOBAL_STATUS(ConstraintError)); + EXPECT_EQ(tester.TryEncode(-0x1000000, { 0 }), CHIP_IM_GLOBAL_STATUS(ConstraintError)); + } + { + EncodeTester tester(CreateFakeMeta(ZCL_INT24S_ATTRIBUTE_TYPE, true /* nullable */)); + + EXPECT_TRUE(tester.TryEncode(0, { 0, 0, 0 }).IsSuccess()); + EXPECT_TRUE(tester.TryEncode(0x123456, { 0x56, 0x34, 0x12 }).IsSuccess()); + EXPECT_TRUE(tester.TryEncode(-1, { 0xFF, 0xFF, 0xFF }).IsSuccess()); + EXPECT_TRUE(tester.TryEncode(-10, { 0xF6, 0xFF, 0xFF }).IsSuccess()); + EXPECT_TRUE(tester.TryEncode(-1234, { 0x2E, 0xFB, 0xFF }).IsSuccess()); + + EXPECT_TRUE(tester.TryEncode>(DataModel::NullNullable, { 0x00, 0x00, 0x80 }).IsSuccess()); + + // Out of range + EXPECT_EQ(tester.TryEncode(0x1000000, { 0 }), CHIP_IM_GLOBAL_STATUS(ConstraintError)); + // cannot encode null equivalent value - this is the minimum negative value + // for 24-bit + EXPECT_EQ(tester.TryEncode(-(1 << 24) - 1, { 0x56, 0x34, 0x12 }), CHIP_IM_GLOBAL_STATUS(ConstraintError)); + + // Out of range for signed - these are unsigned values that are larger + EXPECT_EQ(tester.TryEncode(0xFFFFFF, { 0x56, 0x34, 0x12 }), CHIP_IM_GLOBAL_STATUS(ConstraintError)); + EXPECT_EQ(tester.TryEncode(0x800000, { 0x56, 0x34, 0x12 }), CHIP_IM_GLOBAL_STATUS(ConstraintError)); + } + + { + EncodeTester tester(CreateFakeMeta(ZCL_INT40S_ATTRIBUTE_TYPE, true /* nullable */)); + + // NOTE: to generate encoded values, you an use commands like: + // + // python -c 'import struct; print(", ".join(["0x%X" % v for v in struct.pack("(0, { 0, 0, 0, 0, 0 }).IsSuccess()); + EXPECT_TRUE(tester.TryEncode(0x123456, { 0x56, 0x34, 0x12, 0, 0 }).IsSuccess()); + EXPECT_TRUE(tester.TryEncode(-1234, { 0x2E, 0xFB, 0xFF, 0xFF, 0xFF }).IsSuccess()); + EXPECT_TRUE(tester.TryEncode(-123456789, { 0xeb, 0x32, 0xa4, 0xf8, 0xFF }).IsSuccess()); + EXPECT_TRUE(tester.TryEncode(-12345678910, { 0xc2, 0xe3, 0x23, 0x20, 0xfd }).IsSuccess()); + + EXPECT_TRUE( + tester.TryEncode>(DataModel::NullNullable, { 0x00, 0x00, 0x00, 0x00, 0x80 }).IsSuccess()); + + // Out of range + EXPECT_EQ(tester.TryEncode(0x10011001100, { 0 }), CHIP_IM_GLOBAL_STATUS(ConstraintError)); + // cannot encode null equivalent value + EXPECT_EQ(tester.TryEncode(-(1LL << 40) - 1, { 0 }), CHIP_IM_GLOBAL_STATUS(ConstraintError)); + // negative out of range + EXPECT_EQ(tester.TryEncode(-0x10000000000, { 0 }), CHIP_IM_GLOBAL_STATUS(ConstraintError)); + } + + // Double-check tests, not as exhaustive, to cover all other unsigned values and get + // more test line coverage + { + EncodeTester tester(CreateFakeMeta(ZCL_INT32S_ATTRIBUTE_TYPE, true /* nullable */)); + EXPECT_TRUE(tester.TryEncode(-1234, { 0x2E, 0xFB, 0xFF, 0xFF }).IsSuccess()); + } + { + EncodeTester tester(CreateFakeMeta(ZCL_INT48S_ATTRIBUTE_TYPE, true /* nullable */)); + EXPECT_TRUE(tester.TryEncode(-1234, { 0x2E, 0xFB, 0xFF, 0xFF, 0xFF, 0xFF }).IsSuccess()); + } + { + EncodeTester tester(CreateFakeMeta(ZCL_INT56S_ATTRIBUTE_TYPE, true /* nullable */)); + EXPECT_TRUE(tester.TryEncode(-1234, { 0x2E, 0xFB, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF }).IsSuccess()); + } + + { + EncodeTester tester(CreateFakeMeta(ZCL_INT64S_ATTRIBUTE_TYPE, true /* nullable */)); + EXPECT_TRUE(tester.TryEncode(-1234, { 0x2E, 0xFB, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF }).IsSuccess()); + + // min/max ranges too + EXPECT_TRUE( + tester.TryEncode(std::numeric_limits::min() + 1, { 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80 }) + .IsSuccess()); + EXPECT_TRUE( + tester.TryEncode(std::numeric_limits::max(), { 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x7F }) + .IsSuccess()); + + // Reserved value for NULL + EXPECT_EQ( + tester.TryEncode(std::numeric_limits::min(), { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80 }), + CHIP_IM_GLOBAL_STATUS(ConstraintError)); + } + + { + EncodeTester tester(CreateFakeMeta(ZCL_INT64S_ATTRIBUTE_TYPE, false /* nullable */)); + EXPECT_TRUE(tester.TryEncode(-1234, { 0x2E, 0xFB, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF }).IsSuccess()); + + EXPECT_TRUE( + tester.TryEncode(std::numeric_limits::min(), { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80 }) + .IsSuccess()); + EXPECT_TRUE( + tester.TryEncode(std::numeric_limits::min() + 1, { 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80 }) + .IsSuccess()); + EXPECT_TRUE( + tester.TryEncode(std::numeric_limits::max(), { 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x7F }) + .IsSuccess()); + } +} + +TEST(TestEmberAttributeBuffer, TestEncodeBool) +{ + { + EncodeTester tester(CreateFakeMeta(ZCL_BOOLEAN_ATTRIBUTE_TYPE, false /* nullable */)); + + EXPECT_TRUE(tester.TryEncode(true, { 1 }).IsSuccess()); + EXPECT_TRUE(tester.TryEncode(false, { 0 }).IsSuccess()); + } + + { + EncodeTester tester(CreateFakeMeta(ZCL_BOOLEAN_ATTRIBUTE_TYPE, true /* nullable */)); + + EXPECT_TRUE(tester.TryEncode(true, { 1 }).IsSuccess()); + EXPECT_TRUE(tester.TryEncode(false, { 0 }).IsSuccess()); + EXPECT_TRUE(tester.TryEncode>(DataModel::NullNullable, { 0xFF }).IsSuccess()); + } +} + +TEST(TestEmberAttributeBuffer, TestEncodeFloatingPoint) +{ + // NOTE: to generate encoded values, you an use commands like: + // + // python -c 'import struct; print(", ".join(["0x%X" % v for v in struct.pack("(123.55f, { 0x9A, 0x19, 0xF7, 0x42 }).IsSuccess()); + } + + { + EncodeTester tester(CreateFakeMeta(ZCL_SINGLE_ATTRIBUTE_TYPE, true /* nullable */)); + EXPECT_TRUE(tester.TryEncode(123.55f, { 0x9A, 0x19, 0xF7, 0x42 }).IsSuccess()); + EXPECT_TRUE(tester.TryEncode>(DataModel::NullNullable, { 0, 0, 0xC0, 0x7F }).IsSuccess()); + } + + { + EncodeTester tester(CreateFakeMeta(ZCL_DOUBLE_ATTRIBUTE_TYPE, false /* nullable */)); + EXPECT_TRUE(tester.TryEncode(123.55, { 0x33, 0x33, 0x33, 0x33, 0x33, 0xE3, 0x5E, 0x40 }).IsSuccess()); + } + + { + EncodeTester tester(CreateFakeMeta(ZCL_DOUBLE_ATTRIBUTE_TYPE, true /* nullable */)); + EXPECT_TRUE(tester.TryEncode(123.55, { 0x33, 0x33, 0x33, 0x33, 0x33, 0xE3, 0x5E, 0x40 }).IsSuccess()); + EXPECT_TRUE( + tester.TryEncode>(DataModel::NullNullable, { 0, 0, 0, 0, 0, 0, 0xF8, 0x7F }).IsSuccess()); + } +} + +TEST(TestEmberAttributeBuffer, TestEncodeStrings) +{ + { + EncodeTester tester(CreateFakeMeta(ZCL_CHAR_STRING_ATTRIBUTE_TYPE, false /* nullable */)); + EXPECT_TRUE(tester.TryEncode(""_span, { 0 }).IsSuccess()); + EXPECT_TRUE(tester.TryEncode("test"_span, { 4, 't', 'e', 's', 't' }).IsSuccess()); + EXPECT_TRUE(tester.TryEncode("foo"_span, { 3, 'f', 'o', 'o' }).IsSuccess()); + } + + { + EncodeTester tester(CreateFakeMeta(ZCL_CHAR_STRING_ATTRIBUTE_TYPE, true /* nullable */)); + EXPECT_TRUE(tester.TryEncode(""_span, { 0 }).IsSuccess()); + EXPECT_TRUE(tester.TryEncode("test"_span, { 4, 't', 'e', 's', 't' }).IsSuccess()); + EXPECT_TRUE(tester.TryEncode>(DataModel::NullNullable, { 0xFF }).IsSuccess()); + } + + { + EncodeTester tester(CreateFakeMeta(ZCL_LONG_CHAR_STRING_ATTRIBUTE_TYPE, false /* nullable */)); + EXPECT_TRUE(tester.TryEncode(""_span, { 0, 0 }).IsSuccess()); + EXPECT_TRUE(tester.TryEncode("test"_span, { 4, 0, 't', 'e', 's', 't' }).IsSuccess()); + EXPECT_TRUE(tester.TryEncode("foo"_span, { 3, 0, 'f', 'o', 'o' }).IsSuccess()); + } + + { + EncodeTester tester(CreateFakeMeta(ZCL_LONG_CHAR_STRING_ATTRIBUTE_TYPE, true /* nullable */)); + EXPECT_TRUE(tester.TryEncode("test"_span, { 4, 0, 't', 'e', 's', 't' }).IsSuccess()); + EXPECT_TRUE(tester.TryEncode>(DataModel::NullNullable, { 0xFF, 0xFF }).IsSuccess()); + } + + const uint8_t kOctetData[] = { 1, 2, 3 }; + + // Binary data + { + EncodeTester tester(CreateFakeMeta(ZCL_OCTET_STRING_ATTRIBUTE_TYPE, false /* nullable */)); + EXPECT_TRUE(tester.TryEncode(ByteSpan({}), { 0 }).IsSuccess()); + EXPECT_TRUE(tester.TryEncode(ByteSpan(kOctetData), { 3, 1, 2, 3 }).IsSuccess()); + } + + { + EncodeTester tester(CreateFakeMeta(ZCL_OCTET_STRING_ATTRIBUTE_TYPE, true /* nullable */)); + EXPECT_TRUE(tester.TryEncode(ByteSpan({}), { 0 }).IsSuccess()); + EXPECT_TRUE(tester.TryEncode(ByteSpan(kOctetData), { 3, 1, 2, 3 }).IsSuccess()); + EXPECT_TRUE(tester.TryEncode>(DataModel::NullNullable, { 0xFF }).IsSuccess()); + } + + { + EncodeTester tester(CreateFakeMeta(ZCL_LONG_OCTET_STRING_ATTRIBUTE_TYPE, false /* nullable */)); + EXPECT_TRUE(tester.TryEncode(ByteSpan({}), { 0, 0 }).IsSuccess()); + EXPECT_TRUE(tester.TryEncode(ByteSpan(kOctetData), { 3, 0, 1, 2, 3 }).IsSuccess()); + } + + { + EncodeTester tester(CreateFakeMeta(ZCL_LONG_OCTET_STRING_ATTRIBUTE_TYPE, true /* nullable */)); + EXPECT_TRUE(tester.TryEncode(ByteSpan({}), { 0, 0 }).IsSuccess()); + EXPECT_TRUE(tester.TryEncode(ByteSpan(kOctetData), { 3, 0, 1, 2, 3 }).IsSuccess()); + EXPECT_TRUE(tester.TryEncode>(DataModel::NullNullable, { 0xFF, 0xFF }).IsSuccess()); + } +} + +TEST(TestEmberAttributeBuffer, TestFailures) +{ + { + // attribute type that is not handled + EncodeTester tester(CreateFakeMeta(ZCL_UNKNOWN_ATTRIBUTE_TYPE, true /* nullable */)); + EXPECT_EQ(tester.TryEncode>(DataModel::NullNullable, { 0 }), CHIP_IM_GLOBAL_STATUS(Failure)); + } + + { + // Insufficient space + EncodeTester<3> tester(CreateFakeMeta(ZCL_CHAR_STRING_ATTRIBUTE_TYPE, true /* nullable */)); + EXPECT_TRUE(tester.TryEncode(""_span, { 0 }).IsSuccess()); + EXPECT_EQ(tester.TryEncode("test"_span, { 0 }), CHIP_ERROR_NO_MEMORY); + EXPECT_TRUE(tester.TryEncode>(DataModel::NullNullable, { 0xFF }).IsSuccess()); + } + + // bad type casts + { + EncodeTester tester(CreateFakeMeta(ZCL_CHAR_STRING_ATTRIBUTE_TYPE, false /* nullable */)); + EXPECT_EQ(tester.TryEncode(ByteSpan({}), { 0 }), CHIP_ERROR_WRONG_TLV_TYPE); + } + { + EncodeTester tester(CreateFakeMeta(ZCL_INT32U_ATTRIBUTE_TYPE, false /* nullable */)); + EXPECT_EQ(tester.TryEncode(true, { 0 }), CHIP_ERROR_WRONG_TLV_TYPE); + } +} diff --git a/src/app/util/attribute-storage-null-handling.h b/src/app/util/attribute-storage-null-handling.h index 826e5aff531a98..22dd0e6dab5ce3 100644 --- a/src/app/util/attribute-storage-null-handling.h +++ b/src/app/util/attribute-storage-null-handling.h @@ -246,7 +246,6 @@ struct NumericAttributeTraits static uint8_t MaxValue(bool isNullable) { return 1; } -private: static constexpr StorageType kNullValue = 0xFF; }; diff --git a/src/app/util/odd-sized-integers.h b/src/app/util/odd-sized-integers.h index 32f2c8793b2e71..447f56444c0bdb 100644 --- a/src/app/util/odd-sized-integers.h +++ b/src/app/util/odd-sized-integers.h @@ -20,6 +20,7 @@ #include #include +#include #include namespace chip { @@ -91,6 +92,59 @@ struct IntegerByteIndexing }; } // namespace detail +namespace NumericLimits { + +// Generic size information for unsigned values. +// +// Assumes non-nullable types. Nullable types reserve one of the values as NULL (the max) +inline constexpr uint64_t MaxUnsignedValue(unsigned ByteSize) +{ + if (ByteSize == 8) + { + return std::numeric_limits::max(); + } + return (1ULL << (8 * ByteSize)) - 1; +} + +/// Readability-method to express that the maximum unsigned value is a null value +/// +/// Our encoding states that max int value is the NULL value +inline constexpr uint64_t UnsignedMaxValueToNullValue(uint64_t value) +{ + return value; +} + +// Generic size information for signed values. +// +// Assumes non-nullable types. Nullable types reserve one of the values as NULL (the min) +inline constexpr int64_t MaxSignedValue(unsigned ByteSize) +{ + if (ByteSize == 8) + { + return std::numeric_limits::max(); + } + return (static_cast(1) << (8 * ByteSize - 1)) - 1; +} + +inline constexpr int64_t MinSignedValue(unsigned ByteSize) +{ + if (ByteSize == 8) + { + return std::numeric_limits::min(); + } + return -(static_cast(1) << (8 * ByteSize - 1)); +} + +/// Readability-method to express that the maximum signed value is a null value +/// +/// Our encoding states that min int value is the NULL value +inline constexpr int64_t SignedMinValueToNullValue(int64_t value) +{ + return value; +} + +} // namespace NumericLimits + template struct NumericAttributeTraits, IsBigEndian> : detail::IntegerByteIndexing {