diff --git a/src/darwin/Framework/CHIP/MTRBaseDevice.mm b/src/darwin/Framework/CHIP/MTRBaseDevice.mm index 2f75038eda97a2..3868060175bb1b 100644 --- a/src/darwin/Framework/CHIP/MTRBaseDevice.mm +++ b/src/darwin/Framework/CHIP/MTRBaseDevice.mm @@ -23,6 +23,7 @@ #import "MTRCluster.h" #import "MTRClusterStateCacheContainer_Internal.h" #import "MTRCluster_Internal.h" +#import "MTRDeviceDataValidation.h" #import "MTRDevice_Internal.h" #import "MTRError_Internal.h" #import "MTREventTLVValueDecoder_Internal.h" diff --git a/src/darwin/Framework/CHIP/MTRBaseDevice_Internal.h b/src/darwin/Framework/CHIP/MTRBaseDevice_Internal.h index 075ee99da20990..fa766463e3e025 100644 --- a/src/darwin/Framework/CHIP/MTRBaseDevice_Internal.h +++ b/src/darwin/Framework/CHIP/MTRBaseDevice_Internal.h @@ -18,7 +18,7 @@ #import "MTRBaseDevice.h" #import -#import "MTRDeviceDataValueDictionary.h" +#import "MTRDefines_Internal.h" #include #include diff --git a/src/darwin/Framework/CHIP/MTRDefines_Internal.h b/src/darwin/Framework/CHIP/MTRDefines_Internal.h index 71a03aa09184c5..ba7d6be51d61f5 100644 --- a/src/darwin/Framework/CHIP/MTRDefines_Internal.h +++ b/src/darwin/Framework/CHIP/MTRDefines_Internal.h @@ -65,6 +65,11 @@ typedef struct {} variable_hidden_by_mtr_hide; // Default timed interaction timeout, in ms, if another one is not provided. #define MTR_DEFAULT_TIMED_INTERACTION_TIMEOUT_MS 10000 +// Useful building block for type-checking machinery. Uses C-style cast so it +// can be used in .m files as well. +#define MTR_SAFE_CAST(object, classname) \ + ([object isKindOfClass:[classname class]] ? (classname *) (object) : nil) + #pragma mark - XPC Defines #define MTR_SIMPLE_REMOTE_XPC_GETTER(XPC_CONNECTION, NAME, TYPE, DEFAULT_VALUE, GETTER_NAME, PREFIX) \ @@ -179,3 +184,15 @@ typedef struct {} variable_hidden_by_mtr_hide; #define MTR_ABSTRACT_METHOD() \ _MTR_ABSTRACT_METHOD_IMPL("%@ or some ancestor must implement %@", self.class, NSStringFromSelector(_cmd)) + +#pragma mark - Typedefs for some commonly used types. + +/** + * A data-value as defined in MTRBaseDevice.h. + */ +typedef NSDictionary * MTRDeviceDataValueDictionary; + +/** + * A response-value as defined in MTRBaseDevice.h. + */ +typedef NSDictionary * MTRDeviceResponseValueDictionary; diff --git a/src/darwin/Framework/CHIP/MTRDevice.mm b/src/darwin/Framework/CHIP/MTRDevice.mm index 8e379980578f58..2337d0eae625f5 100644 --- a/src/darwin/Framework/CHIP/MTRDevice.mm +++ b/src/darwin/Framework/CHIP/MTRDevice.mm @@ -25,7 +25,6 @@ #import "MTRConversion.h" #import "MTRDefines_Internal.h" #import "MTRDeviceController_Internal.h" -#import "MTRDeviceDataValueDictionary.h" #import "MTRDevice_Internal.h" #import "MTRError_Internal.h" #import "MTRLogging_Internal.h" diff --git a/src/darwin/Framework/CHIP/MTRDeviceClusterData.h b/src/darwin/Framework/CHIP/MTRDeviceClusterData.h index d34ada90e9a7f2..2abdc6113b44af 100644 --- a/src/darwin/Framework/CHIP/MTRDeviceClusterData.h +++ b/src/darwin/Framework/CHIP/MTRDeviceClusterData.h @@ -17,7 +17,6 @@ #import #import "MTRDefines_Internal.h" -#import "MTRDeviceDataValueDictionary.h" NS_ASSUME_NONNULL_BEGIN diff --git a/src/darwin/Framework/CHIP/MTRDeviceDataValidation.h b/src/darwin/Framework/CHIP/MTRDeviceDataValidation.h new file mode 100644 index 00000000000000..5d448d8aa4fcba --- /dev/null +++ b/src/darwin/Framework/CHIP/MTRDeviceDataValidation.h @@ -0,0 +1,42 @@ +/** + * Copyright (c) 2024 Project CHIP Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import + +#import "MTRDefines_Internal.h" + +NS_ASSUME_NONNULL_BEGIN + +// 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); + +// Returns whether the provided attribute report actually has the right sorts of +// objects in the right places. +MTR_EXTERN MTR_TESTABLE BOOL MTRAttributeReportIsWellFormed(NSArray * report); + +// Returns whether the provided event report actually has the right sorts of +// objects in the right places. +MTR_EXTERN MTR_TESTABLE BOOL MTREventReportIsWellFormed(NSArray * report); + +// Returns whether the provided invoke response actually has the right sorts of +// objects in the right places. +MTR_EXTERN MTR_TESTABLE BOOL MTRInvokeResponseIsWellFormed(NSArray * response); + +NS_ASSUME_NONNULL_END diff --git a/src/darwin/Framework/CHIP/MTRDeviceDataValidation.mm b/src/darwin/Framework/CHIP/MTRDeviceDataValidation.mm new file mode 100644 index 00000000000000..55014b453132a3 --- /dev/null +++ b/src/darwin/Framework/CHIP/MTRDeviceDataValidation.mm @@ -0,0 +1,216 @@ +/** + * Copyright (c) 2024 Project CHIP Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import "MTRDeviceDataValidation.h" + +#import "MTRBaseDevice.h" + +#import "MTRLogging_Internal.h" + +// MTRDataValueDictionaryIsWellFormed lives in MTRBaseDevice.mm, because it uses +// static functions defined in that file. + +#pragma mark - Helpers used by multiple validators + +#define MTR_CHECK_CLASS(className) \ + ^(className * arg) { return MTR_SAFE_CAST(arg, className) != nil; } + +// input is not known to be an NSDictionary yet on entry. +// +// expectedShape maps keys to value validator blocks. +static BOOL MTRDictionaryHasExpectedShape(NSDictionary * input, NSDictionary * expectedShape) +{ + if (!MTR_SAFE_CAST(input, NSDictionary)) { + return NO; + } + + for (id key in expectedShape) { + id value = input[key]; + if (!value) { + return NO; + } + auto validator = static_cast(expectedShape[key]); + if (!validator(value)) { + return NO; + } + } + + return YES; +} + +#pragma mark - Attribute report validation + +static const auto sAttributeDataShape = @{ + MTRAttributePathKey : MTR_CHECK_CLASS(MTRAttributePath), + MTRDataKey : (^(MTRDeviceDataValueDictionary arg) { + return MTRDataValueDictionaryIsWellFormed(arg); + }), +}; + +static const auto sAttributeErrorShape = @{ + MTRAttributePathKey : MTR_CHECK_CLASS(MTRAttributePath), + MTRErrorKey : MTR_CHECK_CLASS(NSError), +}; + +BOOL MTRAttributeReportIsWellFormed(NSArray * report) +{ + if (!MTR_SAFE_CAST(report, NSArray)) { + MTR_LOG_ERROR("Attribute report is not an array: %@", report); + return NO; + } + + for (MTRDeviceResponseValueDictionary item in report) { + // item can be a value report or an error report. + if (!MTRDictionaryHasExpectedShape(item, sAttributeDataShape) && !MTRDictionaryHasExpectedShape(item, sAttributeErrorShape)) { + 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. + if (item[MTRDataKey] != nil && item[MTRErrorKey] != nil) { + MTR_LOG_ERROR("Attribute report contains an entry that claims to be both data and error: %@", item); + return NO; + } + } + + return YES; +} + +#pragma mark - Event report validation + +// MTREventIsHistoricalKey is claimed to be present no matter what, as +// long as MTREventPathKey is present. +static const auto sEventDataShape = @{ + MTREventPathKey : MTR_CHECK_CLASS(MTREventPath), + MTRDataKey : (^(MTRDeviceDataValueDictionary arg) { + return MTRDataValueDictionaryIsWellFormed(arg); + }), + MTREventIsHistoricalKey : MTR_CHECK_CLASS(NSNumber), + MTREventNumberKey : MTR_CHECK_CLASS(NSNumber), + MTREventPriorityKey : MTR_CHECK_CLASS(NSNumber), + MTREventTimeTypeKey : MTR_CHECK_CLASS(NSNumber), +}; + +static const auto sEventErrorShape = @{ + MTREventPathKey : MTR_CHECK_CLASS(MTREventPath), + MTRErrorKey : MTR_CHECK_CLASS(NSError), + MTREventIsHistoricalKey : MTR_CHECK_CLASS(NSNumber), +}; + +BOOL MTREventReportIsWellFormed(NSArray * report) +{ + if (!MTR_SAFE_CAST(report, NSArray)) { + MTR_LOG_ERROR("Event report is not an array: %@", report); + return NO; + } + + for (MTRDeviceResponseValueDictionary item in report) { + // item can be a value report or an error report. + if (!MTRDictionaryHasExpectedShape(item, sEventDataShape) && !MTRDictionaryHasExpectedShape(item, sEventErrorShape)) { + 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. + if (item[MTRDataKey] != nil && item[MTRErrorKey] != nil) { + MTR_LOG_ERROR("Event report contains an entry that claims to be both data and error: %@", item); + return NO; + } + + if (item[MTRDataKey]) { + // Check well-formedness of our timestamps. Note that we have + // already validated the type of item[MTREventTimeTypeKey]. + uint64_t eventTimeType = [item[MTREventTimeTypeKey] unsignedLongLongValue]; + switch (eventTimeType) { + case MTREventTimeTypeSystemUpTime: { + if (!MTR_SAFE_CAST(item[MTREventSystemUpTimeKey], NSNumber)) { + MTR_LOG_ERROR("Event report claims system uptime timing but does not have the time: %@", item); + return NO; + } + break; + } + case MTREventTimeTypeTimestampDate: { + if (!MTR_SAFE_CAST(item[MTREventTimestampDateKey], NSDate)) { + MTR_LOG_ERROR("Event report claims epoch timing but does not have the time: %@", item); + return NO; + } + break; + } + default: + MTR_LOG_ERROR("Uknown time type for event report: %@", item); + return NO; + } + } + } + + return YES; +} + +#pragma mark - Invoke response validation + +BOOL MTRInvokeResponseIsWellFormed(NSArray * response) +{ + if (!MTR_SAFE_CAST(response, NSArray)) { + MTR_LOG_ERROR("Invoke response is not an array: %@", response); + return NO; + } + + // Input is an array with a single value that must have MTRCommandPathKey. + if (response.count != 1) { + MTR_LOG_ERROR("Invoke response is not an array with exactly one entry: %@", response); + return NO; + } + + MTRDeviceResponseValueDictionary responseValue = response[0]; + + if (!MTR_SAFE_CAST(responseValue, NSDictionary) || !MTR_SAFE_CAST(responseValue[MTRCommandPathKey], MTRCommandPath)) { + MTR_LOG_ERROR("Invoke response is not an array with the right things in it: %@", response); + return NO; + } + + MTRDeviceDataValueDictionary _Nullable data = responseValue[MTRDataKey]; + NSError * _Nullable 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 MTR_SAFE_CAST(error, NSError) != nil; + } + + if (data == nil) { + // This is valid: indicates a success status response. + return YES; + } + + if (!MTRDataValueDictionaryIsWellFormed(data)) { + 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. + if (data[MTRTypeKey] != MTRStructureValueType) { + MTR_LOG_ERROR("Invoke response data is not of structure type: %@", data); + return NO; + } + + return YES; +} diff --git a/src/darwin/Framework/CHIP/MTRDeviceDataValueDictionary.h b/src/darwin/Framework/CHIP/MTRDeviceDataValueDictionary.h deleted file mode 100644 index 53a6b2e6f914b7..00000000000000 --- a/src/darwin/Framework/CHIP/MTRDeviceDataValueDictionary.h +++ /dev/null @@ -1,26 +0,0 @@ -/** - * Copyright (c) 2024 Project CHIP Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#import - -NS_ASSUME_NONNULL_BEGIN - -/** - * A data-value as defined in MTRBaseDevice.h. - */ -typedef NSDictionary * MTRDeviceDataValueDictionary; - -NS_ASSUME_NONNULL_END diff --git a/src/darwin/Framework/CHIP/MTRDevice_Concrete.mm b/src/darwin/Framework/CHIP/MTRDevice_Concrete.mm index 2423e4e8769c45..712d0cff1e17ed 100644 --- a/src/darwin/Framework/CHIP/MTRDevice_Concrete.mm +++ b/src/darwin/Framework/CHIP/MTRDevice_Concrete.mm @@ -31,7 +31,7 @@ #import "MTRDeviceConnectivityMonitor.h" #import "MTRDeviceControllerOverXPC.h" #import "MTRDeviceController_Internal.h" -#import "MTRDeviceDataValueDictionary.h" +#import "MTRDeviceDataValidation.h" #import "MTRDevice_Concrete.h" #import "MTRDevice_Internal.h" #import "MTRError_Internal.h" @@ -1887,8 +1887,13 @@ - (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 +- (void)_injectAttributeReport:(NSArray *)attributeReport fromSubscription:(BOOL)isFromSubscription { + if (!MTRAttributeReportIsWellFormed(attributeReport)) { + MTR_LOG_ERROR("%@ injected attribute report is not well-formed: %@", self, attributeReport); + return; + } + [_deviceController asyncDispatchToMatterQueue:^{ MTR_LOG("%@ injected attribute report (%p) %@", self, attributeReport, attributeReport); [self _handleReportBegin]; @@ -1899,8 +1904,13 @@ - (void)_injectAttributeReport:(NSArray *> *)attrib } errorHandler:nil]; } -- (void)_injectEventReport:(NSArray *> *)eventReport +- (void)_injectEventReport:(NSArray *)eventReport { + if (!MTREventReportIsWellFormed(eventReport)) { + MTR_LOG_ERROR("%@ injected event report is not well-formed: %@", self, eventReport); + 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..35cc25e26949da 100644 --- a/src/darwin/Framework/CHIP/MTRDevice_Internal.h +++ b/src/darwin/Framework/CHIP/MTRDevice_Internal.h @@ -22,7 +22,6 @@ #import "MTRAsyncWorkQueue.h" #import "MTRDefines_Internal.h" -#import "MTRDeviceDataValueDictionary.h" #import "MTRDeviceStorageBehaviorConfiguration_Internal.h" NS_ASSUME_NONNULL_BEGIN @@ -176,13 +175,6 @@ MTR_DIRECT_MEMBERS - (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/CHIP/MTRDevice_XPC.mm b/src/darwin/Framework/CHIP/MTRDevice_XPC.mm index 7782509fb79bcd..8fd09f672dc90e 100644 --- a/src/darwin/Framework/CHIP/MTRDevice_XPC.mm +++ b/src/darwin/Framework/CHIP/MTRDevice_XPC.mm @@ -41,7 +41,7 @@ #import "MTRDeviceControllerStartupParams_Internal.h" #import "MTRDeviceController_Concrete.h" #import "MTRDeviceController_XPC.h" -#import "MTRDeviceDataValueDictionary.h" +#import "MTRDeviceDataValidation.h" #import "MTRDevice_Concrete.h" #import "MTRDevice_Internal.h" #import "MTRDevice_XPC_Internal.h" @@ -136,22 +136,47 @@ - (NSString *)description // required methods for MTRDeviceDelegates - (oneway void)device:(NSNumber *)nodeID stateChanged:(MTRDeviceState)state { + if (!MTR_SAFE_CAST(nodeID, NSNumber)) { + MTR_LOG_ERROR("%@ invalid device:stateChanged: nodeID: %@", self, nodeID); + return; + } + MTR_LOG("%s", __PRETTY_FUNCTION__); [self _lockAndCallDelegatesWithBlock:^(id delegate) { [delegate device:self stateChanged:state]; }]; } -- (oneway void)device:(NSNumber *)nodeID receivedAttributeReport:(NSArray *> *)attributeReport +- (oneway void)device:(NSNumber *)nodeID receivedAttributeReport:(NSArray *)attributeReport { + if (!MTR_SAFE_CAST(nodeID, NSNumber)) { + MTR_LOG_ERROR("%@ invalid device:receivedAttributeReport: nodeID: %@", self, nodeID); + return; + } + + if (!MTRAttributeReportIsWellFormed(attributeReport)) { + MTR_LOG_ERROR("%@ invalid device:receivedAttributeReport: attributeReport: %@", self, attributeReport); + return; + } + MTR_LOG("%s", __PRETTY_FUNCTION__); [self _lockAndCallDelegatesWithBlock:^(id delegate) { [delegate device:self receivedAttributeReport:attributeReport]; }]; } -- (oneway void)device:(NSNumber *)nodeID receivedEventReport:(NSArray *> *)eventReport +- (oneway void)device:(NSNumber *)nodeID receivedEventReport:(NSArray *)eventReport { + if (!MTR_SAFE_CAST(nodeID, NSNumber)) { + MTR_LOG_ERROR("%@ invalid device:receivedEventReport: nodeID: %@", self, nodeID); + return; + } + + if (!MTREventReportIsWellFormed(eventReport)) { + MTR_LOG_ERROR("%@ invalid device:receivedEventReport: eventReport: %@", self, eventReport); + return; + } + MTR_LOG("%s", __PRETTY_FUNCTION__); [self _lockAndCallDelegatesWithBlock:^(id delegate) { [delegate device:self receivedEventReport:eventReport]; @@ -161,6 +186,11 @@ - (oneway void)device:(NSNumber *)nodeID receivedEventReport:(NSArray delegate) { if ([delegate respondsToSelector:@selector(deviceBecameActive:)]) { @@ -171,6 +201,11 @@ - (oneway void)deviceBecameActive:(NSNumber *)nodeID - (oneway void)deviceCachePrimed:(NSNumber *)nodeID { + if (!MTR_SAFE_CAST(nodeID, NSNumber)) { + MTR_LOG_ERROR("%@ invalid deviceCachePrimed: nodeID: %@", self, nodeID); + return; + } + [self _lockAndCallDelegatesWithBlock:^(id delegate) { if ([delegate respondsToSelector:@selector(deviceCachePrimed:)]) { [delegate deviceCachePrimed:self]; @@ -180,6 +215,11 @@ - (oneway void)deviceCachePrimed:(NSNumber *)nodeID - (oneway void)deviceConfigurationChanged:(NSNumber *)nodeID { + if (!MTR_SAFE_CAST(nodeID, NSNumber)) { + MTR_LOG_ERROR("%@ invalid deviceConfigurationChanged: nodeID: %@", self, nodeID); + return; + } + [self _lockAndCallDelegatesWithBlock:^(id delegate) { if ([delegate respondsToSelector:@selector(deviceConfigurationChanged:)]) { [delegate deviceConfigurationChanged:self]; @@ -187,14 +227,55 @@ - (oneway void)deviceConfigurationChanged:(NSNumber *)nodeID }]; } +static const auto * requiredInternalStateKeys = @[ kMTRDeviceInternalPropertyDeviceState, kMTRDeviceInternalPropertyLastSubscriptionAttemptWait ]; +static const auto * optionalInternalStateKeys = @[ kMTRDeviceInternalPropertyKeyVendorID, kMTRDeviceInternalPropertyKeyProductID, kMTRDeviceInternalPropertyNetworkFeatures, kMTRDeviceInternalPropertyMostRecentReportTime, kMTRDeviceInternalPropertyLastSubscriptionFailureTime ]; + +- (BOOL)_internalState:(NSDictionary *)dictionary hasValidValuesForKeys:(const NSArray *)keys valueRequired:(BOOL)required +{ + // All the keys are NSNumber-valued. + for (NSString * key in keys) { + id value = dictionary[key]; + if (!value) { + if (required) { + MTR_LOG_ERROR("%@ device:internalStateUpdated: handed state with no value for \"%@\": %@", self, key, value); + return NO; + } + + continue; + } + if (!MTR_SAFE_CAST(value, NSNumber)) { + MTR_LOG_ERROR("%@ device:internalStateUpdated: handed state with invalid value for \"%@\": %@", self, key, value); + return NO; + } + } + + return YES; +} + - (oneway void)device:(NSNumber *)nodeID internalStateUpdated:(NSDictionary *)dictionary { + if (!MTR_SAFE_CAST(nodeID, NSNumber)) { + MTR_LOG_ERROR("%@ invalid device:internalStateUpdated: nodeID: %@", self, nodeID); + return; + } + + if (!MTR_SAFE_CAST(dictionary, NSDictionary)) { + MTR_LOG_ERROR("%@ invalid device:internalStateUpdated dictionary: %@", self, dictionary); + return; + } + + VerifyOrReturn([self _internalState:dictionary hasValidValuesForKeys:requiredInternalStateKeys valueRequired:YES]); + VerifyOrReturn([self _internalState:dictionary hasValidValuesForKeys:optionalInternalStateKeys valueRequired:NO]); + [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 +344,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 && !MTR_SAFE_CAST(error, NSError)) { + 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 && !MTRInvokeResponseIsWellFormed(values)) { + 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..d5faaec24b63b3 100644 --- a/src/darwin/Framework/CHIPTests/MTRDeviceTests.m +++ b/src/darwin/Framework/CHIPTests/MTRDeviceTests.m @@ -27,6 +27,7 @@ #import "MTRCommandPayloadExtensions_Internal.h" #import "MTRDeviceClusterData.h" #import "MTRDeviceControllerLocalTestStorage.h" +#import "MTRDeviceDataValidation.h" #import "MTRDeviceStorageBehaviorConfiguration.h" #import "MTRDeviceTestDelegate.h" #import "MTRDevice_Internal.h" @@ -1505,7 +1506,14 @@ - (void)test017_TestMTRDeviceBasics [device unitTestInjectEventReport:@[ @{ MTREventPathKey : [MTREventPath eventPathWithEndpointID:@(1) clusterID:@(1) eventID:@(1)], MTREventTimeTypeKey : @(MTREventTimeTypeTimestampDate), - MTREventTimestampDateKey : [NSDate date] + MTREventTimestampDateKey : [NSDate date], + MTREventIsHistoricalKey : @(NO), + MTREventPriorityKey : @(MTREventPriorityInfo), + MTREventNumberKey : @(1), // Doesn't matter, in practice + MTRDataKey : @ { + MTRTypeKey : MTRStructureValueType, + MTRValueKey : @[], + }, } ]]; #endif }; @@ -4139,7 +4147,14 @@ - (void)test037_MTRDeviceMultipleDelegatesGetReports MTREventPathKey : [MTREventPath eventPathWithEndpointID:endpointID clusterID:clusterID eventID:eventID], MTREventTimeTypeKey : @(MTREventTimeTypeTimestampDate), MTREventTimestampDateKey : [NSDate date], - // For unit test no real data is needed, but timestamp is required + MTREventIsHistoricalKey : @(NO), + MTREventPriorityKey : @(MTREventPriorityInfo), + MTREventNumberKey : @(1), // Doesn't matter, in practice + // Empty payload. + MTRDataKey : @ { + MTRTypeKey : MTRStructureValueType, + MTRValueKey : @[], + }, }; } @@ -5187,6 +5202,306 @@ - (void)test041_AttributeDataValueValidation } } +- (void)test042_AttributeReportWellFormedness +{ + __auto_type * testData = @[ + @{ + @"input" : @[], + @"valid" : @(YES), + }, + @{ + @"input" : @[ + @{ + MTRAttributePathKey : [MTRAttributePath attributePathWithEndpointID:@(0) clusterID:@(6) attributeID:@(0)], + MTRDataKey : @ { + MTRTypeKey : MTRBooleanValueType, + MTRValueKey : @(YES), + }, + }, + ], + @"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], + }, + ], + @"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], + }, + ], + // Missing both error and data + @"valid" : @(NO), + }, + @{ + @"input" : @[ + @{ + MTRAttributePathKey : [MTRAttributePath attributePathWithEndpointID:@(0) clusterID:@(6) attributeID:@(0)], + MTRDataKey : @ { + MTRTypeKey : MTRBooleanValueType, + MTRValueKey : @("abc"), + }, + }, + ], + // Data dictionary is broken. + @"valid" : @(NO), + }, + @{ + @"input" : @ {}, + // Input is not an array. + @"valid" : @(NO), + }, + ]; + + for (NSDictionary * test in testData) { + XCTAssertEqual(MTRAttributeReportIsWellFormed(test[@"input"]), [test[@"valid"] boolValue], + "input: %@", test[@"input"]); + } +} + +- (void)test043_EventReportWellFormedness +{ + __auto_type * testData = @[ + @{ + @"input" : @[ + @{ + MTREventPathKey : [MTREventPath eventPathWithEndpointID:@(0) clusterID:@(6) eventID:@(0)], + MTRErrorKey : [NSError errorWithDomain:MTRErrorDomain code:0 userInfo:nil], + MTREventIsHistoricalKey : @(NO), + }, + ], + @"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), + }, + ], + @"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), + }, + ], + @"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), + }, + ], + // 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), + }, + ], + // 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), + }, + ], + // 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), + }, + ], + // Wrong type of EventTimeType + @"valid" : @(NO), + }, + @{ + @"input" : @[ @(5) ], + // Wrong type of data entirely. + @"valid" : @(NO), + }, + @{ + @"input" : @ {}, + // Not even an array. + @"valid" : @(NO), + }, + ]; + + for (NSDictionary * test in testData) { + XCTAssertEqual(MTREventReportIsWellFormed(test[@"input"]), [test[@"valid"] boolValue], + "input: %@", test[@"input"]); + } +} + +- (void)test044_InvokeResponseWellFormedness +{ + __auto_type * testData = @[ + @{ + @"input" : @[ + @{ + MTRCommandPathKey : [MTRCommandPath commandPathWithEndpointID:@(0) clusterID:@(6) commandID:@(0)], + }, + ], + @"valid" : @(YES), + }, + @{ + @"input" : @[ + @{ + MTRCommandPathKey : [MTRCommandPath commandPathWithEndpointID:@(0) clusterID:@(6) commandID:@(0)], + }, + @{ + MTRCommandPathKey : [MTRCommandPath commandPathWithEndpointID:@(0) clusterID:@(6) commandID:@(0)], + }, + ], + // Multiple responses + @"valid" : @(NO), + }, + @{ + @"input" : @[ + @{ + MTRCommandPathKey : [MTRCommandPath commandPathWithEndpointID:@(0) clusterID:@(6) commandID:@(0)], + MTRErrorKey : [NSError errorWithDomain:MTRErrorDomain code:0 userInfo:nil], + }, + ], + @"valid" : @(YES), + }, + @{ + @"input" : @[ + @{ + MTRCommandPathKey : [MTRCommandPath commandPathWithEndpointID:@(0) clusterID:@(6) commandID:@(0)], + MTRDataKey : @ { + MTRTypeKey : MTRStructureValueType, + MTRValueKey : @[], // Empty structure, valid + }, + }, + ], + @"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], + }, + ], + // Having both data and error not valid. + @"valid" : @(NO), + }, + @{ + @"input" : @[ + @{ + MTRCommandPathKey : [MTRCommandPath commandPathWithEndpointID:@(0) clusterID:@(6) commandID:@(0)], + MTRDataKey : @ { + MTRTypeKey : MTRUnsignedIntegerValueType, + MTRValueKey : @(5), + }, + }, + ], + // Data is not a struct. + @"valid" : @(NO), + }, + @{ + @"input" : @[ + @{ + MTRCommandPathKey : [MTRCommandPath commandPathWithEndpointID:@(0) clusterID:@(6) commandID:@(0)], + MTRDataKey : @(6), + }, + ], + // Data is not a data-value at all.. + @"valid" : @(NO), + }, + ]; + + for (NSDictionary * test in testData) { + XCTAssertEqual(MTRInvokeResponseIsWellFormed(test[@"input"]), [test[@"valid"] boolValue], + "input: %@", test[@"input"]); + } +} + @end @interface MTRDeviceEncoderTests : XCTestCase diff --git a/src/darwin/Framework/CHIPTests/MTRPerControllerStorageTests.m b/src/darwin/Framework/CHIPTests/MTRPerControllerStorageTests.m index 721cb09b994e25..071566381cc69c 100644 --- a/src/darwin/Framework/CHIPTests/MTRPerControllerStorageTests.m +++ b/src/darwin/Framework/CHIPTests/MTRPerControllerStorageTests.m @@ -19,9 +19,9 @@ #import #import +#import "MTRDefines_Internal.h" #import "MTRDeviceClusterData.h" #import "MTRDeviceControllerLocalTestStorage.h" -#import "MTRDeviceDataValueDictionary.h" #import "MTRDeviceStorageBehaviorConfiguration.h" #import "MTRDeviceTestDelegate.h" #import "MTRDevice_Internal.h" diff --git a/src/darwin/Framework/CHIPTests/TestHelpers/MTRTestDeclarations.h b/src/darwin/Framework/CHIPTests/TestHelpers/MTRTestDeclarations.h index b00507d665a9f0..591110b34ea712 100644 --- a/src/darwin/Framework/CHIPTests/TestHelpers/MTRTestDeclarations.h +++ b/src/darwin/Framework/CHIPTests/TestHelpers/MTRTestDeclarations.h @@ -18,8 +18,8 @@ #import #import +#import "MTRDefines_Internal.h" #import "MTRDeviceClusterData.h" -#import "MTRDeviceDataValueDictionary.h" #import "MTRDevice_Internal.h" NS_ASSUME_NONNULL_BEGIN diff --git a/src/darwin/Framework/Matter.xcodeproj/project.pbxproj b/src/darwin/Framework/Matter.xcodeproj/project.pbxproj index bd05397c05c67e..a666b8830654db 100644 --- a/src/darwin/Framework/Matter.xcodeproj/project.pbxproj +++ b/src/darwin/Framework/Matter.xcodeproj/project.pbxproj @@ -134,7 +134,8 @@ 5109E9B72CB8B83D0006884B /* MTRDeviceTypeTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 5109E9B62CB8B83D0006884B /* MTRDeviceTypeTests.m */; }; 5109E9BA2CC1F23E0006884B /* MTRDeviceClusterData.h in Headers */ = {isa = PBXBuildFile; fileRef = 5109E9B82CC1F23E0006884B /* MTRDeviceClusterData.h */; }; 5109E9BB2CC1F23E0006884B /* MTRDeviceClusterData.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5109E9B92CC1F23E0006884B /* MTRDeviceClusterData.mm */; }; - 5109E9BD2CC1F25C0006884B /* MTRDeviceDataValueDictionary.h in Headers */ = {isa = PBXBuildFile; fileRef = 5109E9BC2CC1F25C0006884B /* MTRDeviceDataValueDictionary.h */; }; + 5109E9C02CCAD64F0006884B /* MTRDeviceDataValidation.h in Headers */ = {isa = PBXBuildFile; fileRef = 5109E9BE2CCAD64F0006884B /* MTRDeviceDataValidation.h */; }; + 5109E9C12CCAD64F0006884B /* MTRDeviceDataValidation.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5109E9BF2CCAD64F0006884B /* MTRDeviceDataValidation.mm */; }; 510A07492A685D3900A9241C /* Matter.apinotes in Headers */ = {isa = PBXBuildFile; fileRef = 510A07482A685D3900A9241C /* Matter.apinotes */; settings = {ATTRIBUTES = (Public, ); }; }; 510CECA8297F72970064E0B3 /* MTROperationalCertificateIssuerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 510CECA6297F72470064E0B3 /* MTROperationalCertificateIssuerTests.m */; }; 5117DD3829A931AE00FFA1AA /* MTROperationalBrowser.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5117DD3629A931AD00FFA1AA /* MTROperationalBrowser.mm */; }; @@ -589,7 +590,8 @@ 5109E9B62CB8B83D0006884B /* MTRDeviceTypeTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MTRDeviceTypeTests.m; sourceTree = ""; }; 5109E9B82CC1F23E0006884B /* MTRDeviceClusterData.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MTRDeviceClusterData.h; sourceTree = ""; }; 5109E9B92CC1F23E0006884B /* MTRDeviceClusterData.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = MTRDeviceClusterData.mm; sourceTree = ""; }; - 5109E9BC2CC1F25C0006884B /* MTRDeviceDataValueDictionary.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MTRDeviceDataValueDictionary.h; sourceTree = ""; }; + 5109E9BE2CCAD64F0006884B /* MTRDeviceDataValidation.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MTRDeviceDataValidation.h; sourceTree = ""; }; + 5109E9BF2CCAD64F0006884B /* MTRDeviceDataValidation.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = MTRDeviceDataValidation.mm; sourceTree = ""; }; 510A07482A685D3900A9241C /* Matter.apinotes */ = {isa = PBXFileReference; lastKnownFileType = text.apinotes; name = Matter.apinotes; path = CHIP/Matter.apinotes; sourceTree = ""; }; 510CECA6297F72470064E0B3 /* MTROperationalCertificateIssuerTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MTROperationalCertificateIssuerTests.m; sourceTree = ""; }; 5117DD3629A931AD00FFA1AA /* MTROperationalBrowser.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = MTROperationalBrowser.mm; sourceTree = ""; }; @@ -1425,7 +1427,8 @@ 51565CB32A7AD78D00469F18 /* MTRDeviceControllerStorageDelegate.h */, 5A6FEC9427B5976200F25F42 /* MTRDeviceControllerXPCConnection.h */, 5A6FEC9527B5983000F25F42 /* MTRDeviceControllerXPCConnection.mm */, - 5109E9BC2CC1F25C0006884B /* MTRDeviceDataValueDictionary.h */, + 5109E9BE2CCAD64F0006884B /* MTRDeviceDataValidation.h */, + 5109E9BF2CCAD64F0006884B /* MTRDeviceDataValidation.mm */, 5A6FEC8B27B5609C00F25F42 /* MTRDeviceOverXPC.h */, 5A6FEC9727B5C6AF00F25F42 /* MTRDeviceOverXPC.mm */, 754784632BFE65B70089C372 /* MTRDeviceStorageBehaviorConfiguration.h */, @@ -1707,7 +1710,6 @@ isa = PBXHeadersBuildPhase; buildActionMask = 2147483647; files = ( - 5109E9BD2CC1F25C0006884B /* MTRDeviceDataValueDictionary.h in Headers */, 51D0B1282B617246006E3511 /* MTRServerEndpoint.h in Headers */, 51565CB62A7B0D6600469F18 /* MTRDeviceControllerParameters.h in Headers */, 51565CB42A7AD78D00469F18 /* MTRDeviceControllerStorageDelegate.h in Headers */, @@ -1767,6 +1769,7 @@ 516415FD2B6ACA8300D5CE11 /* MTRServerAccessControl.h in Headers */, 3CF134AB289D8DF70017A19E /* MTRDeviceAttestationInfo.h in Headers */, B2E0D7B2245B0B5C003C5B48 /* MTRManualSetupPayloadParser.h in Headers */, + 5109E9C02CCAD64F0006884B /* MTRDeviceDataValidation.h in Headers */, 3CF134A7289D8ADA0017A19E /* MTRCSRInfo.h in Headers */, 88E07D612B9A89A4005FD53E /* MTRMetricKeys.h in Headers */, 3D4733B32BE2D1DA003DC19B /* MTRUtilities.h in Headers */, @@ -2157,6 +2160,7 @@ 7596A85528788557004DAE0E /* MTRClusters.mm in Sources */, 88EBF8CF27FABDD500686BC1 /* MTRDeviceAttestationDelegateBridge.mm in Sources */, 5A6FEC9827B5C6AF00F25F42 /* MTRDeviceOverXPC.mm in Sources */, + 5109E9C12CCAD64F0006884B /* MTRDeviceDataValidation.mm in Sources */, 9BDA2A062C5D9AF800A32BDD /* MTRDevice_Concrete.mm in Sources */, 514654492A72F9DF00904E61 /* MTRDemuxingStorage.mm in Sources */, 1E4D655229C30A8700BC3478 /* MTRCommissionableBrowser.mm in Sources */,