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