Add a method for validating data-value dictionaries. (#36207)
diff --git a/src/darwin/Framework/CHIP/MTRBaseDevice.mm b/src/darwin/Framework/CHIP/MTRBaseDevice.mm
index a298b29..2f75038 100644
--- a/src/darwin/Framework/CHIP/MTRBaseDevice.mm
+++ b/src/darwin/Framework/CHIP/MTRBaseDevice.mm
@@ -613,7 +613,9 @@
}
}
-static CHIP_ERROR MTREncodeTLVFromDataValueDictionaryInternal(id object, chip::TLV::TLVWriter & writer, chip::TLV::Tag tag)
+// writer is allowed to be null to just validate the incoming object without
+// actually encoding.
+static CHIP_ERROR MTREncodeTLVFromDataValueDictionaryInternal(id object, chip::TLV::TLVWriter * writer, chip::TLV::Tag tag)
{
if (![object isKindOfClass:[NSDictionary class]]) {
MTR_LOG_ERROR("Error: Unsupported object to encode: %@", [object class]);
@@ -631,60 +633,62 @@
MTR_LOG_ERROR("Error: Object to encode has corrupt signed integer type: %@", [value class]);
return CHIP_ERROR_INVALID_ARGUMENT;
}
- return writer.Put(tag, [value longLongValue]);
+ return writer ? writer->Put(tag, [value longLongValue]) : CHIP_NO_ERROR;
}
if ([typeName isEqualToString:MTRUnsignedIntegerValueType]) {
if (![value isKindOfClass:[NSNumber class]]) {
MTR_LOG_ERROR("Error: Object to encode has corrupt unsigned integer type: %@", [value class]);
return CHIP_ERROR_INVALID_ARGUMENT;
}
- return writer.Put(tag, [value unsignedLongLongValue]);
+ return writer ? writer->Put(tag, [value unsignedLongLongValue]) : CHIP_NO_ERROR;
}
if ([typeName isEqualToString:MTRBooleanValueType]) {
if (![value isKindOfClass:[NSNumber class]]) {
MTR_LOG_ERROR("Error: Object to encode has corrupt boolean type: %@", [value class]);
return CHIP_ERROR_INVALID_ARGUMENT;
}
- return writer.Put(tag, static_cast<bool>([value boolValue]));
+ return writer ? writer->Put(tag, static_cast<bool>([value boolValue])) : CHIP_NO_ERROR;
}
if ([typeName isEqualToString:MTRFloatValueType]) {
if (![value isKindOfClass:[NSNumber class]]) {
MTR_LOG_ERROR("Error: Object to encode has corrupt float type: %@", [value class]);
return CHIP_ERROR_INVALID_ARGUMENT;
}
- return writer.Put(tag, [value floatValue]);
+ return writer ? writer->Put(tag, [value floatValue]) : CHIP_NO_ERROR;
}
if ([typeName isEqualToString:MTRDoubleValueType]) {
if (![value isKindOfClass:[NSNumber class]]) {
MTR_LOG_ERROR("Error: Object to encode has corrupt double type: %@", [value class]);
return CHIP_ERROR_INVALID_ARGUMENT;
}
- return writer.Put(tag, [value doubleValue]);
+ return writer ? writer->Put(tag, [value doubleValue]) : CHIP_NO_ERROR;
}
if ([typeName isEqualToString:MTRNullValueType]) {
- return writer.PutNull(tag);
+ return writer ? writer->PutNull(tag) : CHIP_NO_ERROR;
}
if ([typeName isEqualToString:MTRUTF8StringValueType]) {
if (![value isKindOfClass:[NSString class]]) {
MTR_LOG_ERROR("Error: Object to encode has corrupt UTF8 string type: %@", [value class]);
return CHIP_ERROR_INVALID_ARGUMENT;
}
- return writer.PutString(tag, AsCharSpan(value));
+ return writer ? writer->PutString(tag, AsCharSpan(value)) : CHIP_NO_ERROR;
}
if ([typeName isEqualToString:MTROctetStringValueType]) {
if (![value isKindOfClass:[NSData class]]) {
MTR_LOG_ERROR("Error: Object to encode has corrupt octet string type: %@", [value class]);
return CHIP_ERROR_INVALID_ARGUMENT;
}
- return writer.Put(tag, AsByteSpan(value));
+ return writer ? writer->Put(tag, AsByteSpan(value)) : CHIP_NO_ERROR;
}
if ([typeName isEqualToString:MTRStructureValueType]) {
if (![value isKindOfClass:[NSArray class]]) {
MTR_LOG_ERROR("Error: Object to encode has corrupt structure type: %@", [value class]);
return CHIP_ERROR_INVALID_ARGUMENT;
}
- TLV::TLVType outer;
- ReturnErrorOnFailure(writer.StartContainer(tag, chip::TLV::kTLVType_Structure, outer));
+ TLV::TLVType outer = TLV::kTLVType_NotSpecified;
+ if (writer) {
+ ReturnErrorOnFailure(writer->StartContainer(tag, chip::TLV::kTLVType_Structure, outer));
+ }
for (id element in value) {
if (![element isKindOfClass:[NSDictionary class]]) {
MTR_LOG_ERROR("Error: Structure element to encode has corrupt type: %@", [element class]);
@@ -713,7 +717,9 @@
ReturnErrorOnFailure(
MTREncodeTLVFromDataValueDictionaryInternal(elementValue, writer, tag));
}
- ReturnErrorOnFailure(writer.EndContainer(outer));
+ if (writer) {
+ ReturnErrorOnFailure(writer->EndContainer(outer));
+ }
return CHIP_NO_ERROR;
}
if ([typeName isEqualToString:MTRArrayValueType]) {
@@ -721,8 +727,10 @@
MTR_LOG_ERROR("Error: Object to encode has corrupt array type: %@", [value class]);
return CHIP_ERROR_INVALID_ARGUMENT;
}
- TLV::TLVType outer;
- ReturnErrorOnFailure(writer.StartContainer(tag, chip::TLV::kTLVType_Array, outer));
+ TLV::TLVType outer = TLV::kTLVType_NotSpecified;
+ if (writer) {
+ ReturnErrorOnFailure(writer->StartContainer(tag, chip::TLV::kTLVType_Array, outer));
+ }
for (id element in value) {
if (![element isKindOfClass:[NSDictionary class]]) {
MTR_LOG_ERROR("Error: Array element to encode has corrupt type: %@", [element class]);
@@ -735,14 +743,16 @@
}
ReturnErrorOnFailure(MTREncodeTLVFromDataValueDictionaryInternal(elementValue, writer, chip::TLV::AnonymousTag()));
}
- ReturnErrorOnFailure(writer.EndContainer(outer));
+ if (writer) {
+ ReturnErrorOnFailure(writer->EndContainer(outer));
+ }
return CHIP_NO_ERROR;
}
MTR_LOG_ERROR("Error: Unsupported type to encode: %@", typeName);
return CHIP_ERROR_INVALID_ARGUMENT;
}
-static CHIP_ERROR MTREncodeTLVFromDataValueDictionary(id object, chip::TLV::TLVWriter & writer, chip::TLV::Tag tag)
+static CHIP_ERROR MTREncodeTLVFromDataValueDictionary(id object, chip::TLV::TLVWriter * writer, chip::TLV::Tag tag)
{
CHIP_ERROR err = MTREncodeTLVFromDataValueDictionaryInternal(object, writer, tag);
if (err != CHIP_NO_ERROR) {
@@ -761,7 +771,7 @@
TLV::TLVWriter writer;
writer.Init(buffer);
- CHIP_ERROR err = MTREncodeTLVFromDataValueDictionary(value, writer, TLV::AnonymousTag());
+ CHIP_ERROR err = MTREncodeTLVFromDataValueDictionary(value, &writer, TLV::AnonymousTag());
if (err != CHIP_NO_ERROR) {
if (error) {
*error = [MTRError errorForCHIPErrorCode:err];
@@ -772,6 +782,11 @@
return AsData(ByteSpan(buffer, writer.GetLengthWritten()));
}
+BOOL MTRDataValueDictionaryIsWellFormed(MTRDeviceDataValueDictionary value)
+{
+ return MTREncodeTLVFromDataValueDictionary(value, nullptr, TLV::AnonymousTag()) == CHIP_NO_ERROR;
+}
+
// Callback type to pass data value as an NSObject
typedef void (*MTRDataValueDictionaryCallback)(void * context, id value);
@@ -798,7 +813,7 @@
CHIP_ERROR Encode(chip::TLV::TLVWriter & writer, chip::TLV::Tag tag) const
{
- return MTREncodeTLVFromDataValueDictionary(decodedObj, writer, tag);
+ return MTREncodeTLVFromDataValueDictionary(decodedObj, &writer, tag);
}
static constexpr bool kIsFabricScoped = false;
@@ -2212,7 +2227,7 @@
// Commands never need chained buffers, since they cannot be chunked.
writer.Init(std::move(buffer), /* useChainedBuffers = */ false);
- CHIP_ERROR errorCode = MTREncodeTLVFromDataValueDictionary(data, writer, TLV::AnonymousTag());
+ CHIP_ERROR errorCode = MTREncodeTLVFromDataValueDictionary(data, &writer, TLV::AnonymousTag());
if (errorCode != CHIP_NO_ERROR) {
LogStringAndReturnError(@"Unable to encode data-value to TLV", errorCode, error);
return System::PacketBufferHandle();
@@ -3082,7 +3097,7 @@
System::PacketBufferTLVWriter writer;
writer.Init(std::move(buffer), /* useChainedBuffers = */ true);
- CHIP_ERROR errorCode = MTREncodeTLVFromDataValueDictionary(data, writer, TLV::AnonymousTag());
+ CHIP_ERROR errorCode = MTREncodeTLVFromDataValueDictionary(data, &writer, TLV::AnonymousTag());
if (errorCode != CHIP_NO_ERROR) {
LogStringAndReturnError(@"Unable to encode data-value to TLV", errorCode, error);
return false;
diff --git a/src/darwin/Framework/CHIP/MTRBaseDevice_Internal.h b/src/darwin/Framework/CHIP/MTRBaseDevice_Internal.h
index 1482d80..075ee99 100644
--- a/src/darwin/Framework/CHIP/MTRBaseDevice_Internal.h
+++ b/src/darwin/Framework/CHIP/MTRBaseDevice_Internal.h
@@ -18,6 +18,8 @@
#import "MTRBaseDevice.h"
#import <Foundation/Foundation.h>
+#import "MTRDeviceDataValueDictionary.h"
+
#include <app/AttributePathParams.h>
#include <app/ConcreteAttributePath.h>
#include <app/ConcreteCommandPath.h>
@@ -257,6 +259,6 @@
// TLV Data with an anonymous tag. This method assumes the encoding of the
// value fits in a single UDP MTU; for lists this method might need to be used
// on each list item separately.
-NSData * _Nullable MTREncodeTLVFromDataValueDictionary(NSDictionary<NSString *, id> * value, NSError * __autoreleasing * error);
+NSData * _Nullable MTREncodeTLVFromDataValueDictionary(MTRDeviceDataValueDictionary value, NSError * __autoreleasing * error);
NS_ASSUME_NONNULL_END
diff --git a/src/darwin/Framework/CHIP/MTRDevice_Internal.h b/src/darwin/Framework/CHIP/MTRDevice_Internal.h
index df4a826..4414b3c 100644
--- a/src/darwin/Framework/CHIP/MTRDevice_Internal.h
+++ b/src/darwin/Framework/CHIP/MTRDevice_Internal.h
@@ -176,6 +176,13 @@
- (void)devicePrivateInternalStateChanged:(MTRDevice *)device internalState:(NSDictionary *)state;
@end
+// Returns whether a data-value dictionary is well-formed (in the sense that all
+// the types of the objects inside are as expected, so it's actually a valid
+// representation of some TLV). Implemented in MTRBaseDevice.mm because that's
+// where the pieces needed to implement it are, but declared here so our tests
+// can see it.
+MTR_EXTERN MTR_TESTABLE BOOL MTRDataValueDictionaryIsWellFormed(MTRDeviceDataValueDictionary value);
+
#pragma mark - Constants
static NSString * const kDefaultSubscriptionPoolSizeOverrideKey = @"subscriptionPoolSizeOverride";
diff --git a/src/darwin/Framework/CHIPTests/MTRDeviceTests.m b/src/darwin/Framework/CHIPTests/MTRDeviceTests.m
index 9d33649..3fa4ff4 100644
--- a/src/darwin/Framework/CHIPTests/MTRDeviceTests.m
+++ b/src/darwin/Framework/CHIPTests/MTRDeviceTests.m
@@ -4819,6 +4819,374 @@
}
}
+- (void)test041_AttributeDataValueValidation
+{
+ __auto_type * testData = @[
+ @{
+ @"input" : @ {
+ MTRTypeKey : MTRSignedIntegerValueType,
+ MTRValueKey : @(-5),
+ },
+ // -5 is a valid signed integer.
+ @"valid" : @(YES),
+ },
+ @{
+ @"input" : @ {
+ MTRTypeKey : MTRSignedIntegerValueType,
+ MTRValueKey : @ {},
+ },
+ // A dictionary is not a valid signed integer.
+ @"valid" : @(NO),
+ },
+ @{
+ @"input" : @ {
+ MTRTypeKey : MTRUnsignedIntegerValueType,
+ MTRValueKey : @(7),
+ },
+ // 7 is a valid unsigned integer.
+ @"valid" : @(YES),
+ },
+ @{
+ @"input" : @ {
+ MTRTypeKey : MTRUnsignedIntegerValueType,
+ MTRValueKey : @("abc"),
+ },
+ // "abc" is not an unsigned integer.
+ @"valid" : @(NO),
+ },
+ @{
+ @"input" : @ {
+ MTRTypeKey : MTRBooleanValueType,
+ MTRValueKey : @(YES),
+ },
+ // YES is a boolean.
+ @"valid" : @(YES),
+ },
+ @{
+ @"input" : @ {
+ MTRTypeKey : MTRBooleanValueType,
+ MTRValueKey : [NSData data],
+ },
+ // NSData is not a boolean integer.
+ @"valid" : @(NO),
+ },
+ @{
+ @"input" : @ {
+ MTRTypeKey : MTRFloatValueType,
+ MTRValueKey : @(8),
+ },
+ // 8 is a valid float.
+ @"valid" : @(YES),
+ },
+ @{
+ @"input" : @ {
+ MTRTypeKey : MTRFloatValueType,
+ MTRValueKey : @(8.5),
+ },
+ // 8.5 is a valid float.
+ @"valid" : @(YES),
+ },
+ @{
+ @"input" : @ {
+ MTRTypeKey : MTRFloatValueType,
+ MTRValueKey : @[],
+ },
+ // An array is not a float.
+ @"valid" : @(NO),
+ },
+ @{
+ @"input" : @ {
+ MTRTypeKey : MTRDoubleValueType,
+ MTRValueKey : @(180),
+ },
+ // 180 is a valid double.
+ @"valid" : @(YES),
+ },
+ @{
+ @"input" : @ {
+ MTRTypeKey : MTRDoubleValueType,
+ MTRValueKey : @(9.5),
+ },
+ // 9.5 is a valid double.
+ @"valid" : @(YES),
+ },
+ @{
+ @"input" : @ {
+ MTRTypeKey : MTRDoubleValueType,
+ MTRValueKey : [NSDate date],
+ },
+ // A date is not a double.
+ @"valid" : @(NO),
+ },
+ @{
+ @"input" : @ {
+ MTRTypeKey : MTRNullValueType,
+ },
+ // This is a valid null value.
+ @"valid" : @(YES),
+ },
+ @{
+ @"input" : @ {
+ MTRTypeKey : MTRUTF8StringValueType,
+ MTRValueKey : @("def"),
+ },
+ // "def" is a valid string.
+ @"valid" : @(YES),
+ },
+ @{
+ @"input" : @ {
+ MTRTypeKey : MTRUTF8StringValueType,
+ MTRValueKey : [NSData data],
+ },
+ // NSData is not a string.
+ @"valid" : @(NO),
+ },
+ @{
+ @"input" : @ {
+ MTRTypeKey : MTROctetStringValueType,
+ MTRValueKey : [NSData data],
+ },
+ // NSData is an octet string.
+ @"valid" : @(YES),
+ },
+ @{
+ @"input" : @ {
+ MTRTypeKey : MTROctetStringValueType,
+ MTRValueKey : @(7),
+ },
+ // 7 is not an octet string.
+ @"valid" : @(NO),
+ },
+ @{
+ @"input" : @ {
+ MTRTypeKey : MTROctetStringValueType,
+ MTRValueKey : @("abc"),
+ },
+ // "abc" is not an octet string.
+ @"valid" : @(NO),
+ },
+ @{
+ @"input" : @ {
+ MTRTypeKey : MTRStructureValueType,
+ MTRValueKey : @[],
+ },
+ // This is a valid empty structure.
+ @"valid" : @(YES),
+ },
+ @{
+ @"input" : @ {
+ MTRTypeKey : MTRStructureValueType,
+ MTRValueKey : @[],
+ },
+ // This is a valid empty structure.
+ @"valid" : @(YES),
+ },
+ @{
+ @"input" : @ {
+ MTRTypeKey : MTRStructureValueType,
+ MTRValueKey : @[
+ @{
+ MTRContextTagKey : @(7),
+ MTRDataKey : @ {
+ MTRTypeKey : MTRNullValueType
+ },
+ },
+ ],
+ },
+ // This is a valid structure, one null field.
+ @"valid" : @(YES),
+ },
+ @{
+ @"input" : @ {
+ MTRTypeKey : MTRStructureValueType,
+ MTRValueKey : @[
+ @{
+ MTRContextTagKey : @(1),
+ MTRDataKey : @ {
+ MTRTypeKey : MTRNullValueType
+ },
+ },
+ @{
+ MTRContextTagKey : @(2),
+ MTRDataKey : @ {
+ MTRTypeKey : MTRUnsignedIntegerValueType,
+ MTRValueKey : @(9)
+ },
+ },
+ ],
+ },
+ // This is a valid structure with two fields.
+ @"valid" : @(YES),
+ },
+ @{
+ @"input" : @ {
+ MTRTypeKey : MTRStructureValueType,
+ MTRValueKey : @(19),
+ },
+ // 19 is not a structure.
+ @"valid" : @(NO),
+ },
+ @{
+ @"input" : @ {
+ MTRTypeKey : MTRStructureValueType,
+ MTRValueKey : @[
+ @{
+ MTRDataKey : @ {
+ MTRTypeKey : MTRNullValueType
+ },
+ },
+ ],
+ },
+ // Field does not have a context tag.
+ @"valid" : @(NO),
+ },
+ @{
+ @"input" : @ {
+ MTRTypeKey : MTRStructureValueType,
+ MTRValueKey : @[
+ @{
+ MTRContextTagKey : @(7),
+ },
+ ],
+ },
+ // Field does not have a value.
+ @"valid" : @(NO),
+ },
+ @{
+ @"input" : @ {
+ MTRTypeKey : MTRStructureValueType,
+ MTRValueKey : @[
+ @{
+ MTRContextTagKey : @(7),
+ MTRDataKey : @(5),
+ },
+ ],
+ },
+ // Field value is a number, not a data-value
+ @"valid" : @(NO),
+ },
+ @{
+ @"input" : @ {
+ MTRTypeKey : MTRStructureValueType,
+ MTRValueKey : @[
+ @{
+ MTRContextTagKey : @(7),
+ MTRDataKey : @[],
+ },
+ ],
+ },
+ // Field value is an array, not a data-value
+ @"valid" : @(NO),
+ },
+ @{
+ @"input" : @ {
+ MTRTypeKey : MTRStructureValueType,
+ MTRValueKey : @[
+ @{
+ MTRContextTagKey : @(7),
+ MTRDataKey : @ {},
+ },
+ ],
+ },
+ // Field value is an invalid data-value
+ @"valid" : @(NO),
+ },
+ @{
+ @"input" : @ {
+ MTRTypeKey : MTRStructureValueType,
+ MTRValueKey : @[
+ @{
+ MTRContextTagKey : @("abc"),
+ MTRDataKey : @ {
+ MTRTypeKey : MTRNullValueType
+ },
+ },
+ ],
+ },
+ // Tag is not a number.
+ @"valid" : @(NO),
+ },
+ @{
+ @"input" : @ {
+ MTRTypeKey : MTRArrayValueType,
+ MTRValueKey : @[],
+ },
+ // This is a valid empty array.
+ @"valid" : @(YES),
+ },
+ @{
+ @"input" : @ {
+ MTRTypeKey : MTRArrayValueType,
+ MTRValueKey : @[
+ @{
+ MTRDataKey : @ {
+ MTRTypeKey : MTRNullValueType
+ },
+ },
+ ],
+ },
+ // This is an array with a single null value in it.
+ @"valid" : @(YES),
+ },
+ @{
+ @"input" : @ {
+ MTRTypeKey : MTRArrayValueType,
+ MTRValueKey : @[
+ @{
+ MTRDataKey : @ {
+ MTRTypeKey : MTRUnsignedIntegerValueType,
+ MTRValueKey : @(8),
+ },
+ },
+ @{
+ MTRDataKey : @ {
+ MTRTypeKey : MTRUnsignedIntegerValueType,
+ MTRValueKey : @(10),
+ },
+ },
+ ],
+ },
+ // This is an array with two integers in it.
+ @"valid" : @(YES),
+ },
+ @{
+ @"input" : @ {
+ MTRTypeKey : MTRArrayValueType,
+ MTRValueKey : @[
+ @{
+ MTRTypeKey : MTRUnsignedIntegerValueType,
+ MTRValueKey : @(8),
+ },
+ ],
+ },
+ // This does not have a proper array-value in the array: missing MTRDataKey.
+ @"valid" : @(NO),
+ },
+ @{
+ @"input" : @ {
+ MTRTypeKey : MTRArrayValueType,
+ MTRValueKey : @[ @(7) ],
+ },
+ // This does not have a proper array-value in the array: not a dictionary.
+ @"valid" : @(NO),
+ },
+ @{
+ @"input" : @ {
+ MTRTypeKey : MTRArrayValueType,
+ MTRValueKey : @[ @{} ],
+ },
+ // This does not have a proper array-value in the array: empty
+ // dictionary, so no MTRDataKey.
+ @"valid" : @(NO),
+ },
+ ];
+
+ for (NSDictionary * test in testData) {
+ XCTAssertEqual(MTRDataValueDictionaryIsWellFormed(test[@"input"]), [test[@"valid"] boolValue],
+ "input: %@", test[@"input"]);
+ }
+}
+
@end
@interface MTRDeviceEncoderTests : XCTestCase