From 15156f661579e92ae12ee2f2841157a59f71c286 Mon Sep 17 00:00:00 2001 From: Boris Zbarsky Date: Wed, 23 Oct 2024 16:51:48 -0400 Subject: [PATCH] Add some machinery for structural validation in MTRDevice_XPC and MTRDevice_Concrete. Validate things that get injected (that we got from the XPC transport) as well as various bits in MTRDevice_XPC. --- src/darwin/Framework/CHIP/MTRDevice.mm | 245 ++++++++++++ .../Framework/CHIP/MTRDevice_Concrete.mm | 8 + .../Framework/CHIP/MTRDevice_Internal.h | 31 ++ src/darwin/Framework/CHIP/MTRDevice_XPC.mm | 77 +++- .../Framework/CHIPTests/MTRDeviceTests.m | 372 ++++++++++++++++++ 5 files changed, 732 insertions(+), 1 deletion(-) diff --git a/src/darwin/Framework/CHIP/MTRDevice.mm b/src/darwin/Framework/CHIP/MTRDevice.mm index 8e379980578f58..e887e360e57196 100644 --- a/src/darwin/Framework/CHIP/MTRDevice.mm +++ b/src/darwin/Framework/CHIP/MTRDevice.mm @@ -759,3 +759,248 @@ - (void)invokeCommandWithEndpointID:(NSNumber *)endpointID } @end + +static BOOL MTRArrayIsStructurallyValid(NSArray * input, NSArray * structure) +{ + if (structure.count != 1) { + // Unexpected structure. Just claim NO; later we might give meaning to + // such things. + MTR_LOG_ERROR("Don't know how to make sense of array structure with more than one element: %@", structure); + return NO; + } + + id elementStructure = structure[0]; + for (id inputElement in input) { + if (!MTRInputIsStructurallyValid(inputElement, elementStructure)) { + MTR_LOG_ERROR("Array element not structurally valid: %@, %@", inputElement, elementStructure); + return NO; + } + } + return YES; +} + +static BOOL MTRDictionaryIsStructurallyValid(NSDictionary * input, NSDictionary * structure) +{ + for (id key in structure) { + id inputValue = input[key]; + if (!inputValue) { + MTR_LOG_ERROR("Input does not have key %@: %@, %@", key, input, structure); + return NO; + } + id valueStructure = structure[key]; + if (!MTRInputIsStructurallyValid(inputValue, valueStructure)) { + MTR_LOG_ERROR("Dictionary value not structurally valid: %@, %@", inputValue, valueStructure); + return NO; + } + } + + return YES; +} + +static BOOL MTRAttributeReportIsStructurallyValid(id input) +{ + // Input is an array of values, but they could have slightly different + // structures, so we can't do a single MTRInputIsStructurallyValid to check + // that. Check the items one at a time. + if (![input isKindOfClass:NSArray.class]) { + MTR_LOG_ERROR("Attribute report is not an array: %@", input); + return NO; + } + + NSArray * inputArray = input; + for (id item in inputArray) { + // item can be a value report or an error report. + if (!MTRInputIsStructurallyValid(item, @ { + MTRAttributePathKey : MTRAttributePath.class, + MTRDataKey : MTRInputStructureDataValue, + }) + && !MTRInputIsStructurallyValid(item, @ { + MTRAttributePathKey : MTRAttributePath.class, + MTRErrorKey : NSError.class, + })) { + MTR_LOG_ERROR("Attribute report contains a weird entry: %@", item); + return NO; + } + + // Now we know item is in fact a dictionary, and it has at least one of MTRDataKey and MTRErrorKey. Make sure it's + // not claiming both, which could confuse code that examines it. + NSDictionary * itemDictionary = item; + if (itemDictionary[MTRDataKey] != nil && itemDictionary[MTRErrorKey] != nil) { + MTR_LOG_ERROR("Attribute report contains an entry that claims to be both data and error: %@", item); + return NO; + } + } + + return YES; +} + +static BOOL MTREventReportIsStructurallyValid(id input) +{ + // Input is an array of values, but they could have slightly different + // structures, so we can't do a single MTRInputIsStructurallyValid to check + // that. Check the items one at a time. + if (![input isKindOfClass:NSArray.class]) { + MTR_LOG_ERROR("Event report is not an array: %@", input); + return NO; + } + + NSArray * inputArray = input; + for (id item in inputArray) { + // item can be a value report or an error report. + // MTREventIsHistoricalKey is claimed to be present no matter what, as + // long as MTREventPathKey is present. + if (!MTRInputIsStructurallyValid(item, @ { + MTREventPathKey : MTREventPath.class, + MTRDataKey : MTRInputStructureDataValue, + MTREventIsHistoricalKey : NSNumber.class, + }) + && !MTRInputIsStructurallyValid(item, @ { + MTREventPathKey : MTREventPath.class, + MTRErrorKey : NSError.class, + MTREventIsHistoricalKey : NSNumber.class, + })) { + MTR_LOG_ERROR("Event report contains a weird entry: %@", item); + return NO; + } + + // Now we know item is in fact a dictionary, and it has at least one of MTRDataKey and MTRErrorKey. Make sure it's + // not claiming both, which could confuse code that examines it. + NSDictionary * itemDictionary = item; + if (itemDictionary[MTRDataKey] != nil && itemDictionary[MTRErrorKey] != nil) { + MTR_LOG_ERROR("Event report contains an entry that claims to be both data and error: %@", item); + return NO; + } + + if (itemDictionary[MTRDataKey]) { + // In this case, the structure is supposed to contain some more + // values. + if (!MTRInputIsStructurallyValid(itemDictionary, @ { + MTREventNumberKey : NSNumber.class, + MTREventPriorityKey : NSNumber.class, + MTREventTimeTypeKey : NSNumber.class, + })) { + MTR_LOG_ERROR("Event report fields that depend on MTRDataKey failed validation: %@", itemDictionary); + return NO; + } + + // And check well-formedness of our timestamps. + uint64_t eventTimeType = [itemDictionary[MTREventTimeTypeKey] unsignedLongLongValue]; + switch (eventTimeType) { + case MTREventTimeTypeSystemUpTime: { + if (!MTRInputIsStructurallyValid(itemDictionary, @ { MTREventSystemUpTimeKey : NSNumber.class })) { + MTR_LOG_ERROR("Event report claims system uptime timing but does not have the time: %@", itemDictionary); + return NO; + } + break; + } + case MTREventTimeTypeTimestampDate: { + if (!MTRInputIsStructurallyValid(itemDictionary, @ { MTREventTimestampDateKey : NSDate.class })) { + MTR_LOG_ERROR("Event report claims epoch timing but does not have the time: %@", itemDictionary); + return NO; + } + break; + } + default: + MTR_LOG_ERROR("Uknown time type for event report: %@", itemDictionary); + return NO; + } + } + } + + return YES; +} + +static BOOL MTRInvokeResponseIsStructurallyValid(id input) +{ + // Input is an array with a single value that must have MTRCommandPathKey. + if (!MTRInputIsStructurallyValid(input, @[ @ { MTRCommandPathKey : MTRCommandPath.class } ])) { + MTR_LOG_ERROR("Invoke response is not an array with the right things in it: %@", input); + return NO; + } + + NSArray * inputArray = input; + if (inputArray.count != 1) { + MTR_LOG_ERROR("Invoke response is not an array with exactly one entry: %@", input); + return NO; + } + + // Note that we have already checked the one entry is a dictionary that has + // MTRCommandPathKey. + NSDictionary * responseValue = inputArray[0]; + id data = responseValue[MTRDataKey]; + id error = responseValue[MTRErrorKey]; + + if (data != nil && error != nil) { + MTR_LOG_ERROR("Invoke response claims to have both data and error: %@", responseValue); + return NO; + } + + if (error != nil) { + return MTRInputIsStructurallyValid(error, NSError.class); + } + + if (data == nil) { + // This is valid: indicates a success status response. + return YES; + } + + if (!MTRInputIsStructurallyValid(data, MTRInputStructureDataValue)) { + MTR_LOG_ERROR("Invoke response claims to have data that is not a data-value: %@", data); + return NO; + } + + // Now we know data is a dictionary (in fact a data-value). The only thing + // we promise about it is that it has type MTRStructureValueType. + NSDictionary * dataDictionary = data; + if (dataDictionary[MTRTypeKey] != MTRStructureValueType) { + MTR_LOG_ERROR("Invoke response data is not of structure type: %@", data); + return NO; + } + + return YES; +} + +BOOL MTRInputIsStructurallyValid(id input, id structure) +{ + if ([structure isEqual:MTRInputStructureDataValue]) { + return MTRDataValueDictionaryIsWellFormed(input); + } + + if ([structure isEqual:MTRInputStructureAttributeReport]) { + return MTRAttributeReportIsStructurallyValid(input); + } + + if ([structure isEqual:MTRInputStructureEventReport]) { + return MTREventReportIsStructurallyValid(input); + } + + if ([structure isEqual:MTRInputStructureInvokeResponse]) { + return MTRInvokeResponseIsStructurallyValid(input); + } + + // Literal dictionaries and arrays actually have classes that are not + // exactly NSArray.class and NSDictionary.class, so checking isKindOfClass + // against [structure class] needs to be avoided. + if ([structure isKindOfClass:NSDictionary.class] && + [input isKindOfClass:NSDictionary.class]) { + return MTRDictionaryIsStructurallyValid(input, structure); + } + + if ([structure isKindOfClass:NSArray.class] && + [input isKindOfClass:NSArray.class]) { + return MTRArrayIsStructurallyValid(input, structure); + } + + if ([structure class] != structure) { + // Not a class object, not sure what to do with it. + MTR_LOG_ERROR("Unknown structure value: %@", structure); + return NO; + } + + if (![input isKindOfClass:structure]) { + MTR_LOG_ERROR("Input not of the right class: %@, %@", input, structure); + return NO; + } + + return YES; +} diff --git a/src/darwin/Framework/CHIP/MTRDevice_Concrete.mm b/src/darwin/Framework/CHIP/MTRDevice_Concrete.mm index 2423e4e8769c45..7e698f0df5dcfb 100644 --- a/src/darwin/Framework/CHIP/MTRDevice_Concrete.mm +++ b/src/darwin/Framework/CHIP/MTRDevice_Concrete.mm @@ -1889,6 +1889,10 @@ - (void)_handleAttributeReport:(NSArray *> *)attrib // BEGIN DRAGON: This is used by the XPC Server to inject reports into local cache and broadcast them - (void)_injectAttributeReport:(NSArray *> *)attributeReport fromSubscription:(BOOL)isFromSubscription { + if (!MTRInputIsStructurallyValid(attributeReport, @"attribute-report")) { + return; + } + [_deviceController asyncDispatchToMatterQueue:^{ MTR_LOG("%@ injected attribute report (%p) %@", self, attributeReport, attributeReport); [self _handleReportBegin]; @@ -1901,6 +1905,10 @@ - (void)_injectAttributeReport:(NSArray *> *)attrib - (void)_injectEventReport:(NSArray *> *)eventReport { + if (!MTRInputIsStructurallyValid(eventReport, @"event-report")) { + return; + } + // [_deviceController asyncDispatchToMatterQueue:^{ // TODO: This wasn't used previously, not sure why, so keeping it here for thought, but preserving existing behavior dispatch_async(self.queue, ^{ [self _handleEventReport:eventReport]; diff --git a/src/darwin/Framework/CHIP/MTRDevice_Internal.h b/src/darwin/Framework/CHIP/MTRDevice_Internal.h index 4414b3c6133b07..a1ba023c1b5f1c 100644 --- a/src/darwin/Framework/CHIP/MTRDevice_Internal.h +++ b/src/darwin/Framework/CHIP/MTRDevice_Internal.h @@ -183,6 +183,31 @@ MTR_DIRECT_MEMBERS // can see it. MTR_EXTERN MTR_TESTABLE BOOL MTRDataValueDictionaryIsWellFormed(MTRDeviceDataValueDictionary value); +// Checks whether the input's structure corresponds to the expected structure, +// using the following rules: +// +// 1) If structure is MTRInputStructureDataValue, checks that input is a +// well-formed data-value. +// 2) If structure is the MTRInputStructureAttributeReport, checks that input is +// a well-formed array of response-values representing attribute reports. +// 3) If structure is MTRInputStructureEventReport, checks that input is a +// well-formed array of response-values representing attribute reports. +// 4) If the structure is MTRInputStructureInvokeResponse, checks that the input is +// a well-formed array of a single response-value representing an invoke response. +// 5) If structure is a dictionary, checks that its keys are all present in +// the input, and have values that match the structure of the provided values. +// 6) If structure is an array, that array is expected to contain a single +// element, which is the structure expected from the array elements. Using +// multiple elements in structure will return NO, to allow adding new +// functionality here in the future as needed. +// 7) If structure is a Class object, checks that the input's class matches that. +// 8) In all other cases returns NO for now, until we decide on what +// semantics those structures should have. +// +// For example a structure of @{ @"abc": NSNumber.class } will check that input is +// a dictionary, that it has "abc" as a key, and that the value is an NSNumber. +MTR_EXTERN MTR_TESTABLE BOOL MTRInputIsStructurallyValid(id input, id structure); + #pragma mark - Constants static NSString * const kDefaultSubscriptionPoolSizeOverrideKey = @"subscriptionPoolSizeOverride"; @@ -200,4 +225,10 @@ static NSString * const kMTRDeviceInternalPropertyLastSubscriptionAttemptWait = static NSString * const kMTRDeviceInternalPropertyMostRecentReportTime = @"MTRDeviceInternalPropertyMostRecentReportTime"; static NSString * const kMTRDeviceInternalPropertyLastSubscriptionFailureTime = @"MTRDeviceInternalPropertyLastSubscriptionFailureTime"; +// Constants used by MTRInputIsStructurallyValid +static NSString * const MTRInputStructureDataValue = @"data-value"; +static NSString * const MTRInputStructureAttributeReport = @"attribute-report"; +static NSString * const MTRInputStructureEventReport = @"event-report"; +static NSString * const MTRInputStructureInvokeResponse = @"invoke-response"; + NS_ASSUME_NONNULL_END diff --git a/src/darwin/Framework/CHIP/MTRDevice_XPC.mm b/src/darwin/Framework/CHIP/MTRDevice_XPC.mm index 7782509fb79bcd..f24b1ae0bf0cfb 100644 --- a/src/darwin/Framework/CHIP/MTRDevice_XPC.mm +++ b/src/darwin/Framework/CHIP/MTRDevice_XPC.mm @@ -136,6 +136,10 @@ - (NSString *)description // required methods for MTRDeviceDelegates - (oneway void)device:(NSNumber *)nodeID stateChanged:(MTRDeviceState)state { + if (!MTRInputIsStructurallyValid(nodeID, NSNumber.class)) { + return; + } + MTR_LOG("%s", __PRETTY_FUNCTION__); [self _lockAndCallDelegatesWithBlock:^(id delegate) { [delegate device:self stateChanged:state]; @@ -144,6 +148,10 @@ - (oneway void)device:(NSNumber *)nodeID stateChanged:(MTRDeviceState)state - (oneway void)device:(NSNumber *)nodeID receivedAttributeReport:(NSArray *> *)attributeReport { + if (!MTRInputIsStructurallyValid(nodeID, NSNumber.class) || !MTRInputIsStructurallyValid(attributeReport, MTRInputStructureAttributeReport)) { + return; + } + MTR_LOG("%s", __PRETTY_FUNCTION__); [self _lockAndCallDelegatesWithBlock:^(id delegate) { [delegate device:self receivedAttributeReport:attributeReport]; @@ -152,6 +160,10 @@ - (oneway void)device:(NSNumber *)nodeID receivedAttributeReport:(NSArray *> *)eventReport { + if (!MTRInputIsStructurallyValid(nodeID, NSNumber.class) || !MTRInputIsStructurallyValid(eventReport, MTRInputStructureEventReport)) { + return; + } + MTR_LOG("%s", __PRETTY_FUNCTION__); [self _lockAndCallDelegatesWithBlock:^(id delegate) { [delegate device:self receivedEventReport:eventReport]; @@ -161,6 +173,10 @@ - (oneway void)device:(NSNumber *)nodeID receivedEventReport:(NSArray delegate) { if ([delegate respondsToSelector:@selector(deviceBecameActive:)]) { @@ -171,6 +187,10 @@ - (oneway void)deviceBecameActive:(NSNumber *)nodeID - (oneway void)deviceCachePrimed:(NSNumber *)nodeID { + if (!MTRInputIsStructurallyValid(nodeID, NSNumber.class)) { + return; + } + [self _lockAndCallDelegatesWithBlock:^(id delegate) { if ([delegate respondsToSelector:@selector(deviceCachePrimed:)]) { [delegate deviceCachePrimed:self]; @@ -180,6 +200,10 @@ - (oneway void)deviceCachePrimed:(NSNumber *)nodeID - (oneway void)deviceConfigurationChanged:(NSNumber *)nodeID { + if (!MTRInputIsStructurallyValid(nodeID, NSNumber.class)) { + return; + } + [self _lockAndCallDelegatesWithBlock:^(id delegate) { if ([delegate respondsToSelector:@selector(deviceConfigurationChanged:)]) { [delegate deviceConfigurationChanged:self]; @@ -189,12 +213,36 @@ - (oneway void)deviceConfigurationChanged:(NSNumber *)nodeID - (oneway void)device:(NSNumber *)nodeID internalStateUpdated:(NSDictionary *)dictionary { + if (!MTRInputIsStructurallyValid(nodeID, NSNumber.class) || + // Check always-present properties. + !MTRInputIsStructurallyValid(dictionary, @{ + kMTRDeviceInternalPropertyDeviceState : NSNumber.class, + kMTRDeviceInternalPropertyLastSubscriptionAttemptWait : NSNumber.class, + })) { + return; + } + + // There are several optional keys, all of which are NSNumber-valued. + for (NSString * key in @[ kMTRDeviceInternalPropertyKeyVendorID, kMTRDeviceInternalPropertyKeyProductID, kMTRDeviceInternalPropertyNetworkFeatures, kMTRDeviceInternalPropertyMostRecentReportTime, kMTRDeviceInternalPropertyLastSubscriptionFailureTime ]) { + id value = dictionary[key]; + if (!value) { + continue; + } + if (!MTRInputIsStructurallyValid(value, NSNumber.class)) { + MTR_LOG_ERROR("%@ device:internalStateUpdated: handed state with invalid value for \"%@\": %@", self, key, value); + return; + } + } + [self _setInternalState:dictionary]; MTR_LOG("%@ internal state updated", self); } #pragma mark - Remote Commands +// TODO: Figure out how to validate the return values for the various +// MTR_DEVICE_*_XPC macros below. + MTR_DEVICE_SIMPLE_REMOTE_XPC_GETTER(state, MTRDeviceState, MTRDeviceStateUnknown, getStateWithReply) MTR_DEVICE_SIMPLE_REMOTE_XPC_GETTER(deviceCachePrimed, BOOL, NO, getDeviceCachePrimedWithReply) MTR_DEVICE_SIMPLE_REMOTE_XPC_GETTER(estimatedStartTime, NSDate * _Nullable, nil, getEstimatedStartTimeWithReply) @@ -263,7 +311,34 @@ - (void)_invokeCommandWithEndpointID:(NSNumber *)endpointID expectedValueInterval:expectedValueInterval timedInvokeTimeout:timeout serverSideProcessingTimeout:serverSideProcessingTimeout - completion:completion]; + completion:^(NSArray *> * _Nullable values, NSError * _Nullable error) { + if (values == nil && error == nil) { + MTR_LOG_ERROR("%@ got invoke response for (%@, %@, %@) without values or error", self, endpointID, clusterID, commandID); + completion(nil, [MTRError errorForCHIPErrorCode:CHIP_ERROR_INVALID_ARGUMENT]); + return; + } + + if (error != nil && !MTRInputIsStructurallyValid(error, NSError.class)) { + MTR_LOG_ERROR("%@ got invoke response for (%@, %@, %@) that has invalid error object: %@", self, endpointID, clusterID, commandID, error); + completion(nil, [MTRError errorForCHIPErrorCode:CHIP_ERROR_INVALID_ARGUMENT]); + return; + } + + if (values != nil && !MTRInputIsStructurallyValid(values, MTRInputStructureInvokeResponse)) { + MTR_LOG_ERROR("%@ got invoke response for (%@, %@, %@) that has invalid data: %@", self, clusterID, commandID, values, values); + completion(nil, [MTRError errorForCHIPErrorCode:CHIP_ERROR_INVALID_ARGUMENT]); + return; + } + + if (values != nil && error != nil) { + MTR_LOG_ERROR("%@ got invoke response for (%@, %@, %@) with both values and error: %@, %@", self, endpointID, clusterID, commandID, values, error); + // Just propagate through the error. + completion(nil, error); + return; + } + + completion(values, error); + }]; } @catch (NSException * exception) { MTR_LOG_ERROR("Exception sending XPC message: %@", exception); completion(nil, [NSError errorWithDomain:MTRErrorDomain code:MTRErrorCodeGeneralError userInfo:nil]); diff --git a/src/darwin/Framework/CHIPTests/MTRDeviceTests.m b/src/darwin/Framework/CHIPTests/MTRDeviceTests.m index 3fa4ff45449f89..486c58dcb38c1b 100644 --- a/src/darwin/Framework/CHIPTests/MTRDeviceTests.m +++ b/src/darwin/Framework/CHIPTests/MTRDeviceTests.m @@ -5187,6 +5187,378 @@ - (void)test041_AttributeDataValueValidation } } +- (void)test042_StructuralValidityChecker +{ + __auto_type * testData = @[ + @{ + @"input" : @ { + MTRTypeKey : MTRSignedIntegerValueType, + MTRValueKey : @(-5), + }, + @"structure" : MTRInputStructureDataValue, + @"valid" : @(YES), + }, + @{ + @"input" : @(5), + @"structure" : NSNumber.class, + @"valid" : @(YES), + }, + @{ + @"input" : @("abc"), + @"structure" : NSNumber.class, + @"valid" : @(NO), + }, + @{ + @"input" : @("abc"), + @"structure" : NSString.class, + @"valid" : @(YES), + }, + @{ + @"input" : @[ + @("abc"), + @("def"), + ], + @"structure" : @[ NSString.class ], + @"valid" : @(YES), + }, + @{ + @"input" : @[ + @("abc"), + @(5), + ], + @"structure" : @[ NSString.class ], + @"valid" : @(NO), + }, + @{ + @"input" : @ { + @"a" : @(5), + @"b" : @("def"), + }, + @"structure" : @ { + @"a" : NSNumber.class, + @"b" : NSString.class, + }, + @"valid" : @(YES), + }, + @{ + @"input" : @ { + @"a" : @(5), + }, + @"structure" : @ { + @"a" : NSNumber.class, + @"b" : NSString.class, + }, + // Validation for "b" fails. + @"valid" : @(NO), + }, + @{ + @"input" : @ { + @"a" : @(5), + @"b" : @("def"), + @"c" : [NSData data], + }, + @"structure" : @ { + @"a" : NSNumber.class, + @"b" : NSString.class, + }, + // Extra keys in input are OK, if we are not planning to touch them. + @"valid" : @(YES), + }, + @{ + @"input" : @ { + MTRAttributePathKey : [MTRAttributePath attributePathWithEndpointID:@(0) clusterID:@(6) attributeID:@(0)], + MTRDataKey : @ { + MTRTypeKey : MTRBooleanValueType, + MTRValueKey : @(YES), + }, + }, + @"structure" : @ { + MTRAttributePathKey : MTRAttributePath.class, + MTRDataKey : MTRInputStructureDataValue + }, + @"valid" : @(YES), + }, + @{ + @"input" : @ { + MTRAttributePathKey : [MTRAttributePath attributePathWithEndpointID:@(0) clusterID:@(6) attributeID:@(0)], + MTRDataKey : @ { + MTRTypeKey : MTRBooleanValueType, + MTRValueKey : @(YES), + }, + }, + @"structure" : @ { + MTREventPathKey : MTREventPath.class, + MTRDataKey : MTRInputStructureDataValue + }, + // No MTREventPathKey in input. + @"valid" : @(NO), + }, + @{ + @"input" : @[ + @{ + MTRAttributePathKey : [MTRAttributePath attributePathWithEndpointID:@(0) clusterID:@(6) attributeID:@(0)], + MTRDataKey : @ { + MTRTypeKey : MTRBooleanValueType, + MTRValueKey : @(YES), + }, + }, + ], + @"structure" : MTRInputStructureAttributeReport, + @"valid" : @(YES), + }, + @{ + @"input" : @[ + @{ + MTRAttributePathKey : [MTRAttributePath attributePathWithEndpointID:@(0) clusterID:@(6) attributeID:@(0)], + MTRDataKey : @ { + MTRTypeKey : MTRBooleanValueType, + MTRValueKey : @(YES), + }, + }, + @{ + MTRAttributePathKey : [MTRAttributePath attributePathWithEndpointID:@(0) clusterID:@(6) attributeID:@(1)], + MTRErrorKey : [NSError errorWithDomain:MTRErrorDomain code:0 userInfo:nil], + }, + ], + @"structure" : MTRInputStructureAttributeReport, + @"valid" : @(YES), + }, + @{ + @"input" : @[ + @{ + MTRAttributePathKey : [MTRAttributePath attributePathWithEndpointID:@(0) clusterID:@(6) attributeID:@(0)], + }, + @{ + MTRAttributePathKey : [MTRAttributePath attributePathWithEndpointID:@(0) clusterID:@(6) attributeID:@(1)], + MTRErrorKey : [NSError errorWithDomain:MTRErrorDomain code:0 userInfo:nil], + }, + ], + @"structure" : MTRInputStructureAttributeReport, + // Missing error or data + @"valid" : @(NO), + }, + @{ + @"input" : @[ + @{ + MTREventPathKey : [MTREventPath eventPathWithEndpointID:@(0) clusterID:@(6) eventID:@(0)], + MTRErrorKey : [NSError errorWithDomain:MTRErrorDomain code:0 userInfo:nil], + MTREventIsHistoricalKey : @(NO), + }, + ], + @"structure" : MTRInputStructureEventReport, + @"valid" : @(YES), + }, + @{ + @"input" : @[ + @{ + MTREventPathKey : [MTREventPath eventPathWithEndpointID:@(0) clusterID:@(6) eventID:@(0)], + MTRDataKey : @ { + MTRTypeKey : MTRStructureValueType, + MTRValueKey : @[], // No fields + }, + MTREventNumberKey : @(5), + MTREventPriorityKey : @(MTREventPriorityInfo), + MTREventTimeTypeKey : @(MTREventTimeTypeTimestampDate), + MTREventTimestampDateKey : [NSDate now], + MTREventIsHistoricalKey : @(NO), + }, + ], + @"structure" : MTRInputStructureEventReport, + @"valid" : @(YES), + }, + @{ + @"input" : @[ + @{ + MTREventPathKey : [MTREventPath eventPathWithEndpointID:@(0) clusterID:@(6) eventID:@(0)], + MTRDataKey : @ { + MTRTypeKey : MTRStructureValueType, + MTRValueKey : @[], // No fields + }, + MTREventNumberKey : @(5), + MTREventPriorityKey : @(MTREventPriorityInfo), + MTREventTimeTypeKey : @(MTREventTimeTypeSystemUpTime), + MTREventSystemUpTimeKey : @(5), + MTREventIsHistoricalKey : @(NO), + }, + ], + @"structure" : MTRInputStructureEventReport, + @"valid" : @(YES), + }, + @{ + @"input" : @[ + @{ + MTREventPathKey : [MTREventPath eventPathWithEndpointID:@(0) clusterID:@(6) eventID:@(0)], + MTRDataKey : @ { + MTRTypeKey : MTRStructureValueType, + MTRValueKey : @[], // No fields + }, + MTREventNumberKey : @(5), + MTREventPriorityKey : @(MTREventPriorityInfo), + MTREventTimeTypeKey : @(MTREventTimeTypeTimestampDate), + MTREventTimestampDateKey : @(5), + MTREventIsHistoricalKey : @(NO), + }, + ], + @"structure" : MTRInputStructureEventReport, + // Wrong date type + @"valid" : @(NO), + }, + @{ + @"input" : @[ + @{ + MTREventPathKey : [MTREventPath eventPathWithEndpointID:@(0) clusterID:@(6) eventID:@(0)], + MTRDataKey : @ { + MTRTypeKey : MTRStructureValueType, + MTRValueKey : @[], // No fields + }, + MTREventNumberKey : @("abc"), + MTREventPriorityKey : @(MTREventPriorityInfo), + MTREventTimeTypeKey : @(MTREventTimeTypeSystemUpTime), + MTREventSystemUpTimeKey : @(5), + MTREventIsHistoricalKey : @(NO), + }, + ], + @"structure" : MTRInputStructureEventReport, + // Wrong type of EventNumber + @"valid" : @(NO), + }, + @{ + @"input" : @[ + @{ + MTREventPathKey : [MTREventPath eventPathWithEndpointID:@(0) clusterID:@(6) eventID:@(0)], + MTRDataKey : @ { + MTRTypeKey : MTRStructureValueType, + MTRValueKey : @[], // No fields + }, + MTREventNumberKey : @(5), + MTREventPriorityKey : @("abc"), + MTREventTimeTypeKey : @(MTREventTimeTypeSystemUpTime), + MTREventSystemUpTimeKey : @(5), + MTREventIsHistoricalKey : @(NO), + }, + ], + @"structure" : MTRInputStructureEventReport, + // Wrong type of EventPriority + @"valid" : @(NO), + }, + @{ + @"input" : @[ + @{ + MTREventPathKey : [MTREventPath eventPathWithEndpointID:@(0) clusterID:@(6) eventID:@(0)], + MTRDataKey : @ { + MTRTypeKey : MTRStructureValueType, + MTRValueKey : @[], // No fields + }, + MTREventNumberKey : @(5), + MTREventPriorityKey : @(MTREventPriorityInfo), + MTREventTimeTypeKey : @("abc"), + MTREventSystemUpTimeKey : @(5), + MTREventIsHistoricalKey : @(NO), + }, + ], + @"structure" : MTRInputStructureEventReport, + // Wrong type of EventTimeType + @"valid" : @(NO), + }, + @{ + @"input" : @[ @(5) ], + @"structure" : MTRInputStructureEventReport, + // Wrong type of data entirely. + @"valid" : @(NO), + }, + @{ + @"input" : @[ + @{ + MTRCommandPathKey : [MTRCommandPath commandPathWithEndpointID:@(0) clusterID:@(6) commandID:@(0)], + }, + ], + @"structure" : MTRInputStructureInvokeResponse, + @"valid" : @(YES), + }, + @{ + @"input" : @[ + @{ + MTRCommandPathKey : [MTRCommandPath commandPathWithEndpointID:@(0) clusterID:@(6) commandID:@(0)], + }, + @{ + MTRCommandPathKey : [MTRCommandPath commandPathWithEndpointID:@(0) clusterID:@(6) commandID:@(0)], + }, + ], + @"structure" : MTRInputStructureInvokeResponse, + // Multiple responses + @"valid" : @(NO), + }, + @{ + @"input" : @[ + @{ + MTRCommandPathKey : [MTRCommandPath commandPathWithEndpointID:@(0) clusterID:@(6) commandID:@(0)], + MTRErrorKey : [NSError errorWithDomain:MTRErrorDomain code:0 userInfo:nil], + }, + ], + @"structure" : MTRInputStructureInvokeResponse, + @"valid" : @(YES), + }, + @{ + @"input" : @[ + @{ + MTRCommandPathKey : [MTRCommandPath commandPathWithEndpointID:@(0) clusterID:@(6) commandID:@(0)], + MTRDataKey : @ { + MTRTypeKey : MTRStructureValueType, + MTRValueKey : @[], // Empty structure, valid + }, + }, + ], + @"structure" : MTRInputStructureInvokeResponse, + @"valid" : @(YES), + }, + @{ + @"input" : @[ + @{ + MTRCommandPathKey : [MTRCommandPath commandPathWithEndpointID:@(0) clusterID:@(6) commandID:@(0)], + MTRDataKey : @ { + MTRTypeKey : MTRStructureValueType, + MTRValueKey : @[], // Empty structure, valid + }, + MTRErrorKey : [NSError errorWithDomain:MTRErrorDomain code:0 userInfo:nil], + }, + ], + @"structure" : MTRInputStructureInvokeResponse, + // Having both data and error not valid. + @"valid" : @(NO), + }, + @{ + @"input" : @[ + @{ + MTRCommandPathKey : [MTRCommandPath commandPathWithEndpointID:@(0) clusterID:@(6) commandID:@(0)], + MTRDataKey : @ { + MTRTypeKey : MTRUnsignedIntegerValueType, + MTRValueKey : @(5), + }, + }, + ], + @"structure" : MTRInputStructureInvokeResponse, + // Data is not a struct. + @"valid" : @(NO), + }, + @{ + @"input" : @[ + @{ + MTRCommandPathKey : [MTRCommandPath commandPathWithEndpointID:@(0) clusterID:@(6) commandID:@(0)], + MTRDataKey : @(6), + }, + ], + @"structure" : MTRInputStructureInvokeResponse, + // Data is not a data-value at all.. + @"valid" : @(NO), + }, + ]; + + for (NSDictionary * test in testData) { + XCTAssertEqual(MTRInputIsStructurallyValid(test[@"input"], test[@"structure"]), [test[@"valid"] boolValue], + "input: %@, structure: %@", test[@"input"], test[@"structure"]); + } +} + @end @interface MTRDeviceEncoderTests : XCTestCase