From 0d67568a3f2938d1cdcc8850ace47e54416476c7 Mon Sep 17 00:00:00 2001 From: Karsten Sperling <113487422+ksperling-apple@users.noreply.github.com> Date: Fri, 10 May 2024 11:06:43 +1200 Subject: [PATCH] Darwin: Address various MTRSetupPayload issues (#33286) * Darwin: Address various MTRSetupPayload issues API: - Conform to NSCopying and implement isEqual / hash - Simplify a number of methods to not return NSError - Add initWithPayload: as a replacement for setupPayloadWithOnboardingPayload:error: - Add initWithQRCode: and initWithManualPairingCode: for cases where payload type is known. - Add vendorElements as a replacement for getAllOptionalVendorData: - Add vendorElementWithTag: - Add removeVendorElementWithTag: - Add addOrReplaceVendorElement: Behaviour fixes: - Allow QRCodes with unknown discovery methods to be parsed - Correctly generate a long manualEntryCode when a non-standard flow is indicated - Include vendor elements in qrCodeString - Preserve vendor elements in encodeWithCoder / initWithCoder Also simplify the implementation by acting as a direct wrapper of chip::SetupPayload. Implement parsing in MTRSetupPayload directly and implement the deprecated MTR*PayloadParser classes in terms of it, instead of the other way around. * Fix lint check for 0x%[^x] to allow "*" * Address review comments * Address review comments around NSNumber * Hide type-specific initializers as per review * Misc tweaks --- .github/workflows/lint.yml | 2 +- .../Framework/CHIP/MTRDefines_Internal.h | 4 + src/darwin/Framework/CHIP/MTRError.mm | 12 + src/darwin/Framework/CHIP/MTRError_Internal.h | 15 +- .../CHIP/MTRManualSetupPayloadParser.h | 2 +- .../CHIP/MTRManualSetupPayloadParser.mm | 40 +- .../CHIP/MTROnboardingPayloadParser.h | 2 +- .../CHIP/MTROnboardingPayloadParser.mm | 41 +- .../CHIP/MTRQRCodeSetupPayloadParser.h | 2 +- .../CHIP/MTRQRCodeSetupPayloadParser.mm | 40 +- src/darwin/Framework/CHIP/MTRSetupPayload.h | 176 +++- src/darwin/Framework/CHIP/MTRSetupPayload.mm | 844 +++++++++++------- .../Framework/CHIP/MTRSetupPayload_Internal.h | 13 +- src/darwin/Framework/CHIP/MTRUtilities.h | 27 + src/darwin/Framework/CHIP/MTRUtilities.mm | 23 + .../CHIPTests/MTRSetupPayloadTests.m | 148 ++- .../Matter.xcodeproj/project.pbxproj | 8 + 17 files changed, 906 insertions(+), 493 deletions(-) create mode 100644 src/darwin/Framework/CHIP/MTRUtilities.h create mode 100644 src/darwin/Framework/CHIP/MTRUtilities.mm diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 5f7a956c1ec25e..c810884ed98d30 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -242,7 +242,7 @@ jobs: output. if: always() run: | - git grep -I -n '0x%[0-9l.-]*[^0-9lxX".-]' -- './*' ':(exclude).github/workflows/lint.yml' && exit 1 || exit 0 + git grep -I -n '0x%[0-9l.*-]*[^xX"0-9l.*-]' -- './*' ':(exclude).github/workflows/lint.yml' && exit 1 || exit 0 # git grep exits with 0 if it finds a match, but we want # to fail (exit nonzero) on match. And we want to exclude this file, diff --git a/src/darwin/Framework/CHIP/MTRDefines_Internal.h b/src/darwin/Framework/CHIP/MTRDefines_Internal.h index 6a684f5f879e04..da31ce1b54dda7 100644 --- a/src/darwin/Framework/CHIP/MTRDefines_Internal.h +++ b/src/darwin/Framework/CHIP/MTRDefines_Internal.h @@ -58,5 +58,9 @@ typedef struct {} variable_hidden_by_mtr_hide; // clang-format on +// mtr_[un]likely(expr): Evaluates a boolean expression and hints to the compiler that it is [un]likely to be true. +#define mtr_likely(expr) __builtin_expect(!!(expr), 1) +#define mtr_unlikely(expr) __builtin_expect(!!(expr), 0) + // Default timed interaction timeout, in ms, if another one is not provided. #define MTR_DEFAULT_TIMED_INTERACTION_TIMEOUT_MS 10000 diff --git a/src/darwin/Framework/CHIP/MTRError.mm b/src/darwin/Framework/CHIP/MTRError.mm index d607f00400d158..e268ab49e32cc9 100644 --- a/src/darwin/Framework/CHIP/MTRError.mm +++ b/src/darwin/Framework/CHIP/MTRError.mm @@ -19,6 +19,7 @@ #import "MTRError.h" #import "MTRError_Internal.h" +#import "MTRLogging_Internal.h" #import #import @@ -43,6 +44,11 @@ - (instancetype)initWithError:(CHIP_ERROR)error; @implementation MTRError ++ (NSError *)errorWithCode:(MTRErrorCode)code +{ + return [NSError errorWithDomain:MTRErrorDomain code:code userInfo:nil]; +} + + (NSError *)errorForCHIPErrorCode:(CHIP_ERROR)errorCode { return [MTRError errorForCHIPErrorCode:errorCode logContext:nil]; @@ -337,3 +343,9 @@ - (instancetype)initWithError:(CHIP_ERROR)error } @end + +void MTRThrowInvalidArgument(NSString * reason) +{ + MTR_LOG_ERROR("Invalid argument: %@", reason); + @throw [NSException exceptionWithName:NSInvalidArgumentException reason:reason userInfo:nil]; +} diff --git a/src/darwin/Framework/CHIP/MTRError_Internal.h b/src/darwin/Framework/CHIP/MTRError_Internal.h index 24916082835a6e..c79cc17d95f198 100644 --- a/src/darwin/Framework/CHIP/MTRError_Internal.h +++ b/src/darwin/Framework/CHIP/MTRError_Internal.h @@ -16,16 +16,19 @@ */ #import -#import #import +#import "MTRDefines_Internal.h" + #include #include #include NS_ASSUME_NONNULL_BEGIN +MTR_DIRECT_MEMBERS @interface MTRError : NSObject ++ (NSError *)errorWithCode:(MTRErrorCode)code; + (NSError * _Nullable)errorForCHIPErrorCode:(CHIP_ERROR)errorCode; + (NSError * _Nullable)errorForCHIPErrorCode:(CHIP_ERROR)errorCode logContext:(id _Nullable)contextToLog; + (NSError * _Nullable)errorForIMStatus:(const chip::app::StatusIB &)status; @@ -33,4 +36,14 @@ NS_ASSUME_NONNULL_BEGIN + (CHIP_ERROR)errorToCHIPErrorCode:(NSError * _Nullable)error; @end +// Similar to VerifyOrDie, but throws an NSInvalidArgumentException +#define MTRVerifyArgumentOrDie(cond, reason) \ + do { \ + if (mtr_unlikely(!(cond))) { \ + MTRThrowInvalidArgument(reason); \ + } \ + } while (0) + +MTR_EXTERN _Noreturn void MTRThrowInvalidArgument(NSString * reason); + NS_ASSUME_NONNULL_END diff --git a/src/darwin/Framework/CHIP/MTRManualSetupPayloadParser.h b/src/darwin/Framework/CHIP/MTRManualSetupPayloadParser.h index 9dc5d702a3c2a4..34a5054cc7bab7 100644 --- a/src/darwin/Framework/CHIP/MTRManualSetupPayloadParser.h +++ b/src/darwin/Framework/CHIP/MTRManualSetupPayloadParser.h @@ -21,7 +21,7 @@ NS_ASSUME_NONNULL_BEGIN -MTR_DEPRECATED("Please use [MTRSetupPayload setupPayloadWithOnboardingPayload:error:]", ios(16.1, 16.4), macos(13.0, 13.3), +MTR_DEPRECATED("Please use -[MTRSetupPayload initWithPayload:]", ios(16.1, 16.4), macos(13.0, 13.3), watchos(9.1, 9.4), tvos(16.1, 16.4)) @interface MTRManualSetupPayloadParser : NSObject - (instancetype)initWithDecimalStringRepresentation:(NSString *)decimalStringRepresentation; diff --git a/src/darwin/Framework/CHIP/MTRManualSetupPayloadParser.mm b/src/darwin/Framework/CHIP/MTRManualSetupPayloadParser.mm index c1716b513b38a6..a8a4c6bec96c40 100644 --- a/src/darwin/Framework/CHIP/MTRManualSetupPayloadParser.mm +++ b/src/darwin/Framework/CHIP/MTRManualSetupPayloadParser.mm @@ -1,6 +1,6 @@ /** * - * Copyright (c) 2020 Project CHIP Authors + * Copyright (c) 2020-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. @@ -14,56 +14,30 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + #import "MTRManualSetupPayloadParser.h" #import "MTRError_Internal.h" -#import "MTRFramework.h" -#import "MTRLogging_Internal.h" #import "MTRSetupPayload_Internal.h" -#import -#import - -#include - @implementation MTRManualSetupPayloadParser { NSString * _decimalStringRepresentation; - chip::ManualSetupPayloadParser * _chipManualSetupPayloadParser; -} - -+ (void)initialize -{ - MTRFrameworkInit(); } - (id)initWithDecimalStringRepresentation:(NSString *)decimalStringRepresentation { - if (self = [super init]) { - _decimalStringRepresentation = decimalStringRepresentation; - _chipManualSetupPayloadParser = new chip::ManualSetupPayloadParser(std::string([decimalStringRepresentation UTF8String])); - } + self = [super init]; + _decimalStringRepresentation = decimalStringRepresentation; return self; } - (MTRSetupPayload *)populatePayload:(NSError * __autoreleasing *)error { - chip::SetupPayload cPlusPluspayload; - MTRSetupPayload * payload; - - CHIP_ERROR chipError = _chipManualSetupPayloadParser->populatePayload(cPlusPluspayload); - if (chipError == CHIP_NO_ERROR) { - payload = [[MTRSetupPayload alloc] initWithSetupPayload:cPlusPluspayload]; - } else if (error) { - *error = [MTRError errorForCHIPErrorCode:chipError]; + MTRSetupPayload * payload = [[MTRSetupPayload alloc] initWithManualPairingCode:_decimalStringRepresentation]; + if (!payload && error) { + *error = [MTRError errorWithCode:MTRErrorCodeInvalidArgument]; } - return payload; } -- (void)dealloc -{ - delete _chipManualSetupPayloadParser; - _chipManualSetupPayloadParser = nullptr; -} - @end diff --git a/src/darwin/Framework/CHIP/MTROnboardingPayloadParser.h b/src/darwin/Framework/CHIP/MTROnboardingPayloadParser.h index f607abb89fe616..a875e36db9fb1b 100644 --- a/src/darwin/Framework/CHIP/MTROnboardingPayloadParser.h +++ b/src/darwin/Framework/CHIP/MTROnboardingPayloadParser.h @@ -28,7 +28,7 @@ typedef NS_ENUM(NSUInteger, MTROnboardingPayloadType) { MTROnboardingPayloadTypeNFC } MTR_DEPRECATED("MTROnboardingPayloadType is unused", ios(16.1, 17.0), macos(13.0, 14.0), watchos(9.1, 10.0), tvos(16.1, 17.0)); -MTR_DEPRECATED("Please use [MTRSetupPayload setupPayloadWithOnboardingPayload:error:]", ios(16.1, 17.0), macos(13.0, 14.0), +MTR_DEPRECATED("Please use [MTRSetupPayload initWithPayload:]", ios(16.1, 17.0), macos(13.0, 14.0), watchos(9.1, 10.0), tvos(16.1, 17.0)) @interface MTROnboardingPayloadParser : NSObject diff --git a/src/darwin/Framework/CHIP/MTROnboardingPayloadParser.mm b/src/darwin/Framework/CHIP/MTROnboardingPayloadParser.mm index fb73932d5a3a42..be9a5fc5e1d0fe 100644 --- a/src/darwin/Framework/CHIP/MTROnboardingPayloadParser.mm +++ b/src/darwin/Framework/CHIP/MTROnboardingPayloadParser.mm @@ -1,6 +1,6 @@ /** * - * Copyright (c) 2020 Project CHIP Authors + * Copyright (c) 2020-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. @@ -16,50 +16,15 @@ */ #import "MTROnboardingPayloadParser.h" -#import "MTRManualSetupPayloadParser.h" -#import "MTRQRCodeSetupPayloadParser.h" + #import "MTRSetupPayload.h" @implementation MTROnboardingPayloadParser -+ (bool)isQRCode:(NSString *)codeString -{ - return [codeString hasPrefix:@"MT:"]; -} - + (MTRSetupPayload * _Nullable)setupPayloadForOnboardingPayload:(NSString *)onboardingPayload error:(NSError * __autoreleasing *)error { - MTRSetupPayload * payload; - // MTROnboardingPayloadTypeNFC is of type QR code and handled same as QR code - MTROnboardingPayloadType type = - [self isQRCode:onboardingPayload] ? MTROnboardingPayloadTypeQRCode : MTROnboardingPayloadTypeManualCode; - switch (type) { - case MTROnboardingPayloadTypeManualCode: - payload = [self setupPayloadForManualCodeOnboardingPayload:onboardingPayload error:error]; - break; - case MTROnboardingPayloadTypeQRCode: - payload = [self setupPayloadForQRCodeOnboardingPayload:onboardingPayload error:error]; - break; - default: - break; - } - return payload; + return [MTRSetupPayload setupPayloadWithOnboardingPayload:onboardingPayload error:error]; } -+ (MTRSetupPayload * _Nullable)setupPayloadForQRCodeOnboardingPayload:(NSString *)onboardingPayload - error:(NSError * __autoreleasing *)error -{ - MTRQRCodeSetupPayloadParser * qrCodeParser = - [[MTRQRCodeSetupPayloadParser alloc] initWithBase38Representation:onboardingPayload]; - return [qrCodeParser populatePayload:error]; -} - -+ (MTRSetupPayload * _Nullable)setupPayloadForManualCodeOnboardingPayload:(NSString *)onboardingPayload - error:(NSError * __autoreleasing *)error -{ - MTRManualSetupPayloadParser * manualParser = - [[MTRManualSetupPayloadParser alloc] initWithDecimalStringRepresentation:onboardingPayload]; - return [manualParser populatePayload:error]; -} @end diff --git a/src/darwin/Framework/CHIP/MTRQRCodeSetupPayloadParser.h b/src/darwin/Framework/CHIP/MTRQRCodeSetupPayloadParser.h index 5f71b2668c6a6c..771b650cedc7dc 100644 --- a/src/darwin/Framework/CHIP/MTRQRCodeSetupPayloadParser.h +++ b/src/darwin/Framework/CHIP/MTRQRCodeSetupPayloadParser.h @@ -21,7 +21,7 @@ NS_ASSUME_NONNULL_BEGIN -MTR_DEPRECATED("Please use [MTRSetupPayload setupPayloadWithOnboardingPayload:error:]", ios(16.1, 16.4), macos(13.0, 13.3), +MTR_DEPRECATED("Please use [MTRSetupPayload -initWithPayload:]", ios(16.1, 16.4), macos(13.0, 13.3), watchos(9.1, 9.4), tvos(16.1, 16.4)) @interface MTRQRCodeSetupPayloadParser : NSObject - (instancetype)initWithBase38Representation:(NSString *)base38Representation; diff --git a/src/darwin/Framework/CHIP/MTRQRCodeSetupPayloadParser.mm b/src/darwin/Framework/CHIP/MTRQRCodeSetupPayloadParser.mm index acfa613bc6626f..2c7701da8fa716 100644 --- a/src/darwin/Framework/CHIP/MTRQRCodeSetupPayloadParser.mm +++ b/src/darwin/Framework/CHIP/MTRQRCodeSetupPayloadParser.mm @@ -1,6 +1,6 @@ /** * - * Copyright (c) 2020 Project CHIP Authors + * Copyright (c) 2020-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. @@ -16,54 +16,28 @@ */ #import "MTRQRCodeSetupPayloadParser.h" + #import "MTRError_Internal.h" -#import "MTRFramework.h" -#import "MTRLogging_Internal.h" #import "MTRSetupPayload_Internal.h" -#import -#import - -#include - @implementation MTRQRCodeSetupPayloadParser { NSString * _base38Representation; - chip::QRCodeSetupPayloadParser * _chipQRCodeSetupPayloadParser; -} - -+ (void)initialize -{ - MTRFrameworkInit(); } - (id)initWithBase38Representation:(NSString *)base38Representation { - if (self = [super init]) { - _base38Representation = base38Representation; - _chipQRCodeSetupPayloadParser = new chip::QRCodeSetupPayloadParser(std::string([base38Representation UTF8String])); - } + self = [super init]; + _base38Representation = base38Representation; return self; } - (MTRSetupPayload *)populatePayload:(NSError * __autoreleasing *)error { - chip::SetupPayload cPlusPluspayload; - MTRSetupPayload * payload; - - CHIP_ERROR chipError = _chipQRCodeSetupPayloadParser->populatePayload(cPlusPluspayload); - if (chipError == CHIP_NO_ERROR) { - payload = [[MTRSetupPayload alloc] initWithSetupPayload:cPlusPluspayload]; - } else if (error) { - *error = [MTRError errorForCHIPErrorCode:chipError]; + MTRSetupPayload * payload = [[MTRSetupPayload alloc] initWithQRCode:_base38Representation]; + if (!payload && error) { + *error = [MTRError errorWithCode:MTRErrorCodeInvalidArgument]; } - return payload; } -- (void)dealloc -{ - delete _chipQRCodeSetupPayloadParser; - _chipQRCodeSetupPayloadParser = nullptr; -} - @end diff --git a/src/darwin/Framework/CHIP/MTRSetupPayload.h b/src/darwin/Framework/CHIP/MTRSetupPayload.h index 7f11fc582ba4b3..5c789f3b794969 100644 --- a/src/darwin/Framework/CHIP/MTRSetupPayload.h +++ b/src/darwin/Framework/CHIP/MTRSetupPayload.h @@ -1,6 +1,6 @@ /** * - * Copyright (c) 2020-2023 Project CHIP Authors + * Copyright (c) 2020-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. @@ -20,7 +20,7 @@ NS_ASSUME_NONNULL_BEGIN typedef NS_OPTIONS(NSUInteger, MTRDiscoveryCapabilities) { - MTRDiscoveryCapabilitiesUnknown = 0, // Device capabilities are not known (e.g. all we have is a numeric code). + MTRDiscoveryCapabilitiesUnknown = 0, // Device capabilities are not known (e.g. we parsed a Manual Pairing Code). MTRDiscoveryCapabilitiesNone MTR_DEPRECATED_WITH_REPLACEMENT( "MTRDiscoveryCapabilitiesUnknown", ios(16.1, 16.4), macos(13.0, 13.3), watchos(9.1, 9.4), tvos(16.1, 16.4)) = 0, @@ -28,7 +28,7 @@ typedef NS_OPTIONS(NSUInteger, MTRDiscoveryCapabilities) { MTRDiscoveryCapabilitiesBLE = 1 << 1, // Device supports BLE MTRDiscoveryCapabilitiesOnNetwork = 1 << 2, // Device supports On Network setup - // If new values are added here, update the "description" method to include them. + // Note: New values added here need to be included in MTRDiscoveryCapabilitiesAsString() MTRDiscoveryCapabilitiesAllMask = MTRDiscoveryCapabilitiesSoftAP | MTRDiscoveryCapabilitiesBLE | MTRDiscoveryCapabilitiesOnNetwork, @@ -38,7 +38,7 @@ typedef NS_ENUM(NSUInteger, MTRCommissioningFlow) { MTRCommissioningFlowStandard = 0, // Device automatically enters commissioning mode upon power-up MTRCommissioningFlowUserActionRequired = 1, // Device requires a user interaction to enter commissioning mode MTRCommissioningFlowCustom = 2, // Commissioning steps should be retrieved from the distributed compliance ledger - MTRCommissioningFlowInvalid = 3, + MTRCommissioningFlowInvalid MTR_NEWLY_DEPRECATED("Not a valid MTRCommissioningFlow value") = 3, }; typedef NS_ENUM(NSUInteger, MTROptionalQRCodeInfoType) { @@ -49,36 +49,76 @@ typedef NS_ENUM(NSUInteger, MTROptionalQRCodeInfoType) { }; /** - * An optional information item present in the QR code the setup payload was - * initialized from. + * An optional information item present in the setup payload. + * + * Note that while the Matter specification allows elements containing + * arbitrary TLV data types, this implementation currently only supports + * String and Int32 values. + * + * Objects of this type are immutable; calling any deprecated property + * setters has no effect. */ MTR_AVAILABLE(ios(16.1), macos(13.0), watchos(9.1), tvos(16.1)) -@interface MTROptionalQRCodeInfo : NSObject +@interface MTROptionalQRCodeInfo : NSObject /* (see below) */ -@property (nonatomic, assign) MTROptionalQRCodeInfoType type MTR_AVAILABLE(ios(16.4), macos(13.3), watchos(9.4), tvos(16.4)); +/** + * Initializes the object with a tag and string value. + * The tag must be in the range 0x80 - 0xFF. + */ +- (instancetype)initWithTag:(NSNumber *)tag stringValue:(NSString *)value MTR_NEWLY_AVAILABLE; /** - * The numeric value of the TLV tag for this information item. + * Initializes the object with a tag and int32 value. + * The tag must be in the range 0x80 - 0xFF. */ -@property (nonatomic, copy) NSNumber * tag; +- (instancetype)initWithTag:(NSNumber *)tag int32Value:(int32_t)value MTR_NEWLY_AVAILABLE; + +@property (nonatomic, readonly, assign) MTROptionalQRCodeInfoType type MTR_AVAILABLE(ios(16.4), macos(13.3), watchos(9.4), tvos(16.4)); /** - * Exactly one of integerValue and stringValue will be non-nil, depending on the - * the value of "type". + * The vendor-specific TLV tag number for this information item. + * + * Vendor-specific elements have tags in the range 0x80 - 0xFF. */ -@property (nonatomic, copy, nullable) NSNumber * integerValue; -@property (nonatomic, copy, nullable) NSString * stringValue; +@property (nonatomic, readonly, copy) NSNumber * tag; + +/** + * The value held in this extension element, + * if `type` is an integer type, or nil otherwise. + */ +@property (nonatomic, readonly, copy, nullable) NSNumber * integerValue; + +/** + * The value held in this extension element, + * if `type` is `MTROptionalQRCodeInfoTypeString`, or nil otherwise. + */ +@property (nonatomic, readonly, copy, nullable) NSString * stringValue; @end +MTR_NEWLY_AVAILABLE +@interface MTROptionalQRCodeInfo () +@end + /** - * A setup payload that can be created from a numeric code or QR code and - * serialized to a numeric code or QR code, though serializing to QR code after - * creating from numeric code will not work, because some required information - * will be missing. + * A Matter Onboarding Payload. + * + * It can be represented as a numeric Manual Pairing Code or as QR Code. + * The QR Code format contains more information though, so creating a + * QR Code from a payload that was initialized from a Manual Pairing Code + * will not work, because some required information will be missing. + * + * This class can also be used to create an onboarding payload directly + * from the underlying values (passcode, discriminator, etc). */ MTR_AVAILABLE(ios(16.1), macos(13.0), watchos(9.1), tvos(16.1)) -@interface MTRSetupPayload : NSObject +@interface MTRSetupPayload : NSObject /* also (see below) */ + +/** + * Initializes the payload object from the provide QR Code or Manual Pairing Code string. + * Returns nil if the payload is not valid. + */ +- (nullable instancetype)initWithPayload:(NSString *)payload MTR_NEWLY_AVAILABLE; @property (nonatomic, copy) NSNumber * version; @property (nonatomic, copy) NSNumber * vendorID; @@ -92,19 +132,45 @@ MTR_AVAILABLE(ios(16.1), macos(13.0), watchos(9.1), tvos(16.1)) */ @property (nonatomic, assign) MTRDiscoveryCapabilities discoveryCapabilities MTR_AVAILABLE(ios(16.4), macos(13.3), watchos(9.4), tvos(16.4)); + @property (nonatomic, copy) NSNumber * discriminator; /** * If hasShortDiscriminator is true, the discriminator value contains just the * high 4 bits of the full discriminator. For example, if * hasShortDiscriminator is true and discriminator is 0xA, then the full - * discriminator can be anything in the range 0xA00 t0 0xAFF. + * discriminator can be anything in the range 0xA00 to 0xAFF. */ @property (nonatomic, assign) BOOL hasShortDiscriminator; + @property (nonatomic, copy) NSNumber * setupPasscode MTR_AVAILABLE(ios(16.4), macos(13.3), watchos(9.4), tvos(16.4)); +/** + * The value of the Serial Number extension element, if any. + */ @property (nonatomic, copy, nullable) NSString * serialNumber; -- (NSArray * _Nullable)getAllOptionalVendorData:(NSError * __autoreleasing *)error; + +/** + * The list of Manufacturer-specific extension elements contained in the setup code. May be empty. + */ +@property (nonatomic, readonly, copy) NSArray * vendorElements MTR_NEWLY_AVAILABLE; + +/** + * Returns the Manufacturer-specific extension element with the specified tag, if any. + * The tag must be in the range 0x80 - 0xFF. + */ +- (nullable MTROptionalQRCodeInfo *)vendorElementWithTag:(NSNumber *)tag MTR_NEWLY_AVAILABLE; + +/** + * Removes the extension element with the specified tag, if any. + * The tag must be in the range 0x80 - 0xFF. + */ +- (void)removeVendorElementWithTag:(NSNumber *)tag MTR_NEWLY_AVAILABLE; + +/** + * Adds or replaces a Manufacturer-specific extension element. + */ +- (void)addOrReplaceVendorElement:(MTROptionalQRCodeInfo *)element MTR_NEWLY_AVAILABLE; /** * Generate a random Matter-valid setup PIN. @@ -116,15 +182,6 @@ MTR_AVAILABLE(ios(16.1), macos(13.0), watchos(9.1), tvos(16.1)) */ + (NSNumber *)generateRandomSetupPasscode MTR_AVAILABLE(ios(16.2), macos(13.1), watchos(9.2), tvos(16.2)); -/** - * Create an MTRSetupPayload with the given onboarding payload. - * - * Will return nil on errors (e.g. if the onboarding payload cannot be parsed). - */ -+ (MTRSetupPayload * _Nullable)setupPayloadWithOnboardingPayload:(NSString *)onboardingPayload - error:(NSError * __autoreleasing *)error - MTR_AVAILABLE(ios(16.2), macos(13.1), watchos(9.2), tvos(16.2)); - /** * Initialize an MTRSetupPayload with the given passcode and discriminator. * This will pre-set version, product id, and vendor id to 0. @@ -132,39 +189,76 @@ MTR_AVAILABLE(ios(16.1), macos(13.0), watchos(9.1), tvos(16.1)) - (instancetype)initWithSetupPasscode:(NSNumber *)setupPasscode discriminator:(NSNumber *)discriminator MTR_AVAILABLE(ios(16.2), macos(13.1), watchos(9.2), tvos(16.2)); -/** Get 11 digit manual entry code from the setup payload. */ -- (NSString * _Nullable)manualEntryCode; +/** + * Creates a Manual Pairing Code from this setup payload. + * Returns nil if this payload cannot be represented as a valid Manual Pairing Code. + * + * The following properties must be populated for a valid Manual Pairing Code: + * - setupPasscode + * - discriminator (short or long) + * + * In most cases the pairing code will be 11 digits long. If the payload indicates + * a `commissioningFlow` other than `MTRCommissioningFlowStandard`, a 21 digit code + * will be produced that includes the vendorID and productID values. + */ +- (nullable NSString *)manualEntryCode; /** - * Get a QR code from the setup payload. + * Creates a QR Code payload from this setup payload. + * Returns nil if this payload cannot be represented as a valid QR Code. * - * Returns nil on failure (e.g. if the setup payload does not have all the - * information a QR code needs). + * The following properties must be populated for a valid QR Code: + * - setupPasscode + * - discriminator (must be long) + * - discoveryCapabilities (not MTRDiscoveryCapabilitiesUnknown) */ -- (NSString * _Nullable)qrCodeString:(NSError * __autoreleasing *)error - MTR_AVAILABLE(ios(16.2), macos(13.1), watchos(9.2), tvos(16.2)); +- (NSString * _Nullable)qrCodeString MTR_NEWLY_AVAILABLE; @end +MTR_NEWLY_AVAILABLE +@interface MTRSetupPayload () +@end + @interface MTROptionalQRCodeInfo (Deprecated) -@property (nonatomic, copy) - NSNumber * infoType MTR_DEPRECATED("Please use type", ios(16.1, 16.4), macos(13.0, 13.3), watchos(9.1, 9.4), tvos(16.1, 16.4)); +- (instancetype)init MTR_NEWLY_DEPRECATED("Please use -initWithTag:...value:"); + +@property (nonatomic, copy) NSNumber * infoType + MTR_DEPRECATED("Please use type", ios(16.1, 16.4), macos(13.0, 13.3), watchos(9.1, 9.4), tvos(16.1, 16.4)); +- (void)setType:(MTROptionalQRCodeInfoType)type MTR_NEWLY_DEPRECATED("MTROptionalQRCodeInfo is immutable"); +- (void)setTag:(NSNumber *)tag MTR_NEWLY_DEPRECATED("MTROptionalQRCodeInfo is immutable"); +- (void)setIntegerValue:(NSNumber *)integerValue MTR_NEWLY_DEPRECATED("MTROptionalQRCodeInfo is immutable"); +- (void)setStringValue:(NSString *)stringValue MTR_NEWLY_DEPRECATED("MTROptionalQRCodeInfo is immutable"); @end @interface MTRSetupPayload (Deprecated) @property (nonatomic, copy, nullable) NSNumber * rendezvousInformation MTR_DEPRECATED( "Please use discoveryCapabilities", ios(16.1, 16.4), macos(13.0, 13.3), watchos(9.1, 9.4), tvos(16.1, 16.4)); + @property (nonatomic, copy) NSNumber * setUpPINCode MTR_DEPRECATED( "Please use setupPasscode", ios(16.1, 16.4), macos(13.0, 13.3), watchos(9.1, 9.4), tvos(16.1, 16.4)); -- (instancetype)init MTR_DEPRECATED("Please use initWithSetupPasscode or setupPayloadWithOnboardingPayload", ios(16.1, 16.4), +- (instancetype)init MTR_DEPRECATED("Please use -initWithSetupPasscode:discriminator: or -initWithPayload:", ios(16.1, 16.4), macos(13.0, 13.3), watchos(9.1, 9.4), tvos(16.1, 16.4)); -+ (instancetype)new MTR_DEPRECATED("Please use initWithSetupPasscode or setupPayloadWithOnboardingPayload", ios(16.1, 16.4), + ++ (instancetype)new MTR_DEPRECATED("Please use -initWithSetupPasscode:discriminator: or -initWithPayload:", ios(16.1, 16.4), macos(13.0, 13.3), watchos(9.1, 9.4), tvos(16.1, 16.4)); ++ (MTRSetupPayload * _Nullable)setupPayloadWithOnboardingPayload:(NSString *)onboardingPayload + error:(NSError * __autoreleasing *)error + MTR_AVAILABLE(ios(16.2), macos(13.1), watchos(9.2), tvos(16.2)) + MTR_NEWLY_DEPRECATED("Please use -initWithPayload:"); + +- (NSString * _Nullable)qrCodeString:(NSError * __autoreleasing *)error + MTR_AVAILABLE(ios(16.2), macos(13.1), watchos(9.2), tvos(16.2)) + MTR_NEWLY_DEPRECATED("Please use -qrCodeString"); + +- (NSArray * _Nullable)getAllOptionalVendorData:(NSError * __autoreleasing *)error + MTR_NEWLY_DEPRECATED("Please use -vendorElements"); + @end NS_ASSUME_NONNULL_END diff --git a/src/darwin/Framework/CHIP/MTRSetupPayload.mm b/src/darwin/Framework/CHIP/MTRSetupPayload.mm index d5788c63dc6f6c..98d9f2b342c909 100644 --- a/src/darwin/Framework/CHIP/MTRSetupPayload.mm +++ b/src/darwin/Framework/CHIP/MTRSetupPayload.mm @@ -19,251 +19,520 @@ #import "MTRError_Internal.h" #import "MTRFramework.h" -#import "MTROnboardingPayloadParser.h" +#import "MTRLogging_Internal.h" +#import "MTRUtilities.h" +#include #include +#include #include +#include #include #include -@implementation MTROptionalQRCodeInfo -@end - MTR_DIRECT_MEMBERS -@implementation MTRSetupPayload { - chip::SetupPayload _chipSetupPayload; +@implementation MTROptionalQRCodeInfo { + chip::OptionalQRCodeInfo _info; } -+ (void)initialize +static uint8_t ValidateVendorTag(NSNumber * tag) { - // Need to make sure we set up Platform memory stuff before we start - // serializing payloads. - MTRFrameworkInit(); + auto integerValue = tag.integerValue; + auto tagValue = static_cast(integerValue); + MTRVerifyArgumentOrDie(tag != nil && chip::CanCastTo(integerValue) && chip::SetupPayload::IsVendorTag(tagValue), @"tag must be a vendor tag (0x80 - 0xFF)"); + return tagValue; } -- (MTRDiscoveryCapabilities)convertRendezvousFlags:(const chip::Optional &)value +- (instancetype)initWithTag:(NSNumber *)tag int32Value:(int32_t)value { - if (!value.HasValue()) { - return MTRDiscoveryCapabilitiesUnknown; - } + self = [super init]; + _info.type = chip::optionalQRCodeInfoTypeInt32; + _info.tag = ValidateVendorTag(tag); + _info.int32 = value; + return self; +} - NSUInteger flags = 0; - if (value.Value().Has(chip::RendezvousInformationFlag::kBLE)) { - flags |= MTRDiscoveryCapabilitiesBLE; - } - if (value.Value().Has(chip::RendezvousInformationFlag::kSoftAP)) { - flags |= MTRDiscoveryCapabilitiesSoftAP; - } - if (value.Value().Has(chip::RendezvousInformationFlag::kOnNetwork)) { - flags |= MTRDiscoveryCapabilitiesOnNetwork; - } - if (flags == MTRDiscoveryCapabilitiesUnknown) { - // OnNetwork has to be supported! - flags = MTRDiscoveryCapabilitiesOnNetwork; - } - return flags; +- (instancetype)initWithTag:(NSNumber *)tag stringValue:(NSString *)value +{ + self = [super init]; + _info.type = chip::optionalQRCodeInfoTypeString; + _info.tag = ValidateVendorTag(tag); + MTRVerifyArgumentOrDie(value != nil, @"value"); + _info.data = value.UTF8String; + return self; +} + +- (nullable instancetype)initWithQRCodeInfo:(chip::OptionalQRCodeInfo const &)info +{ + self = [super init]; + _info = info; + // Don't expose objects with an out-of-range tag or invalid type + VerifyOrReturnValue(chip::SetupPayload::IsVendorTag(_info.tag), nil); + VerifyOrReturnValue(self.type != MTROptionalQRCodeInfoTypeUnknown, nil); + return self; } -+ (chip::Optional)convertDiscoveryCapabilities:(MTRDiscoveryCapabilities)value +- (CHIP_ERROR)addAsVendorElementTo:(chip::SetupPayload &)payload { - if (value == MTRDiscoveryCapabilitiesUnknown) { - return chip::NullOptional; + switch (_info.type) { + case chip::optionalQRCodeInfoTypeString: + return payload.addOptionalVendorData(_info.tag, _info.data); + case chip::optionalQRCodeInfoTypeInt32: + return payload.addOptionalVendorData(_info.tag, _info.int32); + default: + return CHIP_ERROR_INCORRECT_STATE; } +} - chip::RendezvousInformationFlags flags; - if (value & MTRDiscoveryCapabilitiesBLE) { - flags.Set(chip::RendezvousInformationFlag::kBLE); +- (MTROptionalQRCodeInfoType)type +{ + // The way OptionalQRCodeInfo uses what are effectively C type identifiers (uint32 etc) + // rather than TLV types is not ideal. If we add support for additional integer types + // we should consider replacing MTROptionalQRCodeInfoTypeInt32 with + // MTROptionalQRCodeInfoTypeInteger and hiding the low-level C representation. + switch (_info.type) { + case chip::optionalQRCodeInfoTypeString: + return MTROptionalQRCodeInfoTypeString; + case chip::optionalQRCodeInfoTypeInt32: + return MTROptionalQRCodeInfoTypeInt32; + // No 'default:' so we get a warning if new types are added. + // Note: isEqual: also switches over these types. + // OptionalQRCodeInfo does not support these types + case chip::optionalQRCodeInfoTypeInt64: + case chip::optionalQRCodeInfoTypeUInt32: + case chip::optionalQRCodeInfoTypeUInt64: + // We should never see the unknown type + case chip::optionalQRCodeInfoTypeUnknown: + /* fall through */; + } + return MTROptionalQRCodeInfoTypeUnknown; +} + +- (NSNumber *)tag +{ + return @(_info.tag); +} + +- (NSNumber *)integerValue +{ + VerifyOrReturnValue(_info.type == chip::optionalQRCodeInfoTypeInt32, nil); + return @(_info.int32); +} + +- (NSString *)stringValue +{ + VerifyOrReturnValue(_info.type == chip::optionalQRCodeInfoTypeString, nil); + return [NSString stringWithUTF8String:_info.data.c_str()]; +} + +- (id)copyWithZone:(NSZone *)zone +{ + return self; // immutable +} + +- (NSUInteger)hash +{ + return _info.type << 8 | _info.tag; +} + +- (BOOL)isEqual:(id)object +{ + VerifyOrReturnValue([object class] == [self class], NO); + MTROptionalQRCodeInfo * other = object; + VerifyOrReturnValue(_info.tag == other->_info.tag, NO); + VerifyOrReturnValue(_info.type == other->_info.type, NO); + switch (_info.type) { + case chip::optionalQRCodeInfoTypeString: + return _info.data == other->_info.data; + case chip::optionalQRCodeInfoTypeInt32: + return _info.int32 == other->_info.int32; + default: + return NO; // unreachable, type is checked in init } +} + +- (NSString *)description +{ + return [NSString stringWithFormat:@"0x%02x=%@", self.tag.unsignedCharValue, self.integerValue ?: self.stringValue]; +} + +@end + +static constexpr uint8_t kShortDiscriminatorMask = (1 << chip::SetupDiscriminator::kShortBits) - 1; +static constexpr uint16_t kLongDiscriminatorMask = (1 << chip::SetupDiscriminator::kLongBits) - 1; + +static MTRDiscoveryCapabilities KnownDiscoveryCapabilities(NSUInteger value) +{ + // A known but 0 value cannot be represented by MTRDiscoveryCapabilities + // since MTRDiscoveryCapabilitiesUnknown represents an absent value. + // However such a 0 value is not logically valid, because OnNetwork must + // always be supported. So just return that if we get a value of 0. + return (value != MTRDiscoveryCapabilitiesUnknown ? value : MTRDiscoveryCapabilitiesOnNetwork); +} + +static NSString * MTRDiscoveryCapabilitiesAsString(MTRDiscoveryCapabilities value) +{ + VerifyOrReturnValue(value != MTRDiscoveryCapabilitiesUnknown, @"Unknown"); + NSMutableString * capabilities = [NSMutableString string]; if (value & MTRDiscoveryCapabilitiesSoftAP) { - flags.Set(chip::RendezvousInformationFlag::kSoftAP); + [capabilities appendString:@"|SoftAP"]; + value &= ~MTRDiscoveryCapabilitiesSoftAP; + } + if (value & MTRDiscoveryCapabilitiesBLE) { + [capabilities appendString:@"|BLE"]; + value &= ~MTRDiscoveryCapabilitiesBLE; } if (value & MTRDiscoveryCapabilitiesOnNetwork) { - flags.Set(chip::RendezvousInformationFlag::kOnNetwork); + [capabilities appendString:@"|OnNetwork"]; + value &= ~MTRDiscoveryCapabilitiesOnNetwork; + } + if (value != 0) { + [capabilities appendFormat:@"|0x%llx", (unsigned long long) value]; } - return chip::MakeOptional(flags); + return [capabilities substringFromIndex:1]; } -- (MTRCommissioningFlow)convertCommissioningFlow:(chip::CommissioningFlow)value +MTR_DIRECT_MEMBERS +@implementation MTRSetupPayload { + // Apart from the additional logic to handle discriminators detailed below, + // this class is simply a wrapper around this underlying SetupPayload. + chip::SetupPayload _payload; + + // SetupPayload deals with the discriminator value and its shortness as a composite + // SetupDiscriminator value, which is arguably a tidier API than letting clients + // set a value first and only later tell us whether it's long or short. But that is + // what our API does, so we need to continue to support it. To make this work, we + // keep track of the potentially-long value that was set by the client. + NSNumber * _Nullable _shadowDiscriminator; +} + ++ (void)initialize { - if (value == chip::CommissioningFlow::kStandard) { - return MTRCommissioningFlowStandard; - } - if (value == chip::CommissioningFlow::kUserActionRequired) { - return MTRCommissioningFlowUserActionRequired; - } - if (value == chip::CommissioningFlow::kCustom) { - return MTRCommissioningFlowCustom; - } - return MTRCommissioningFlowInvalid; + // Some aspects of working with chip::SetupPayload use Platform memory primitives. + MTRFrameworkInit(); } -+ (chip::CommissioningFlow)unconvertCommissioningFlow:(MTRCommissioningFlow)value +- (nullable instancetype)initWithPayload:(NSString *)payload { - if (value == MTRCommissioningFlowStandard) { - return chip::CommissioningFlow::kStandard; - } - if (value == MTRCommissioningFlowUserActionRequired) { - return chip::CommissioningFlow::kUserActionRequired; + return ([payload hasPrefix:@"MT:"]) ? [self initWithQRCode:payload] : [self initWithManualPairingCode:payload]; +} + +- (CHIP_ERROR)initializeFromQRCode:(NSString *)qrCode +{ + std::string string([(qrCode ?: @"") UTF8String]); // handle nil gracefully + chip::QRCodeSetupPayloadParser parser(string); + return parser.populatePayload(_payload); +} + +- (nullable instancetype)initWithQRCode:(NSString *)qrCodePayload +{ + self = [super init]; + CHIP_ERROR err = [self initializeFromQRCode:qrCodePayload]; + if (err != CHIP_NO_ERROR) { + MTR_LOG_ERROR("Failed to parse QR Code payload: %" CHIP_ERROR_FORMAT, err.Format()); + return nil; } - if (value == MTRCommissioningFlowCustom) { - return chip::CommissioningFlow::kCustom; + if (!_payload.isValidQRCodePayload(chip::PayloadContents::ValidationMode::kConsume)) { + MTR_LOG_ERROR("Invalid QR Code payload"); + return nil; } - // It's MTRCommissioningFlowInvalid ... now what? But in practice - // this is not called when we have MTRCommissioningFlowInvalid. - return chip::CommissioningFlow::kStandard; + + return self; } -- (instancetype)initWithSetupPayload:(chip::SetupPayload)setupPayload +- (nullable instancetype)initWithManualPairingCode:(NSString *)manualCode { - if (self = [super init]) { - _chipSetupPayload = setupPayload; - _version = [NSNumber numberWithUnsignedChar:setupPayload.version]; - _vendorID = [NSNumber numberWithUnsignedShort:setupPayload.vendorID]; - _productID = [NSNumber numberWithUnsignedShort:setupPayload.productID]; - _commissioningFlow = [self convertCommissioningFlow:setupPayload.commissioningFlow]; - _discoveryCapabilities = [self convertRendezvousFlags:setupPayload.rendezvousInformation]; - _hasShortDiscriminator = setupPayload.discriminator.IsShortDiscriminator(); - if (_hasShortDiscriminator) { - _discriminator = [NSNumber numberWithUnsignedShort:setupPayload.discriminator.GetShortValue()]; - } else { - _discriminator = [NSNumber numberWithUnsignedShort:setupPayload.discriminator.GetLongValue()]; - } - _setupPasscode = [NSNumber numberWithUnsignedInt:setupPayload.setUpPINCode]; + self = [super init]; - [self getSerialNumber:setupPayload]; + std::string string([(manualCode ?: @"") UTF8String]); // handle nil gracefully + chip::ManualSetupPayloadParser parser(string); + CHIP_ERROR err = parser.populatePayload(_payload); + if (err != CHIP_NO_ERROR) { + MTR_LOG_ERROR("Failed to parse Manual Pairing Code: %" CHIP_ERROR_FORMAT, err.Format()); + return nil; } + if (!_payload.isValidManualCode(chip::PayloadContents::ValidationMode::kConsume)) { + MTR_LOG_ERROR("Invalid Manual Pairing Code"); + return nil; + } + return self; } - (instancetype)initWithSetupPasscode:(NSNumber *)setupPasscode discriminator:(NSNumber *)discriminator { - if (self = [super init]) { - _version = @(0); // Only supported Matter version so far. - _vendorID = @(0); // Not available. - _productID = @(0); // Not available. - _commissioningFlow = MTRCommissioningFlowStandard; - // We are using a long discriminator, so have to have a known - // discoveryCapabilities to be a valid payload. Just default to "try - // all discovery methods". - _discoveryCapabilities = MTRDiscoveryCapabilitiesAllMask; - _hasShortDiscriminator = NO; - _discriminator = discriminator; - _setupPasscode = setupPasscode; - _serialNumber = nil; - } + self = [super init]; + + // A default-constructed SetupPayload already has most of the expected + // default values. Set discoveryCapabilities to "all known methods" so + // we're representing a valid QR Code payload. + self.setupPasscode = setupPasscode; + self.discriminator = discriminator; + self.discoveryCapabilities = MTRDiscoveryCapabilitiesAllMask; + return self; } -- (void)getSerialNumber:(chip::SetupPayload)setupPayload +- (instancetype)initWithSetupPayload:(chip::SetupPayload)setupPayload +{ + self = [super init]; + _payload = setupPayload; + return self; +} + +#pragma mark - Mutable properties + +- (NSNumber *)version +{ + return @(_payload.version); +} + +- (void)setVersion:(NSNumber *)version +{ + _payload.version = static_cast(version.unsignedIntegerValue); +} + +- (NSNumber *)vendorID +{ + return @(_payload.vendorID); +} + +- (void)setVendorID:(NSNumber *)vendorID +{ + _payload.vendorID = static_cast(vendorID.unsignedIntegerValue); +} + +- (NSNumber *)productID { - std::string serialNumberC; - CHIP_ERROR err = setupPayload.getSerialNumber(serialNumberC); - if (err == CHIP_NO_ERROR) { - _serialNumber = [NSString stringWithUTF8String:serialNumberC.c_str()]; + return @(_payload.productID); +} + +- (void)setProductID:(NSNumber *)productID +{ + _payload.productID = static_cast(productID.unsignedIntegerValue); +} + +- (MTRCommissioningFlow)commissioningFlow +{ + // To avoid ending up with special logic to handle MTRCommissioningFlowInvalid, + // we simply cast between MTRCommissioningFlow and chip::CommissioningFlow. + // Both types represent the same set of values defined in the Matter spec. + using chip::CommissioningFlow; + static_assert(static_cast(MTRCommissioningFlowStandard) == CommissioningFlow::kStandard); + static_assert(static_cast(MTRCommissioningFlowUserActionRequired) == CommissioningFlow::kUserActionRequired); + static_assert(static_cast(MTRCommissioningFlowCustom) == CommissioningFlow::kCustom); + return static_cast(_payload.commissioningFlow); +} + +- (void)setCommissioningFlow:(MTRCommissioningFlow)commissioningFlow +{ + _payload.commissioningFlow = static_cast(commissioningFlow); +} + +- (MTRDiscoveryCapabilities)discoveryCapabilities +{ + VerifyOrReturnValue(_payload.rendezvousInformation.HasValue(), MTRDiscoveryCapabilitiesUnknown); + + // MTRDiscoveryCapabilities and chip::RendezvousInformationFlag represent + // the same set of bit flags from the Matter spec, so we can simply cast. + using RendezvousFlag = chip::RendezvousInformationFlag; + static_assert(static_cast(MTRDiscoveryCapabilitiesSoftAP) == RendezvousFlag::kSoftAP); + static_assert(static_cast(MTRDiscoveryCapabilitiesBLE) == RendezvousFlag::kBLE); + static_assert(static_cast(MTRDiscoveryCapabilitiesOnNetwork) == RendezvousFlag::kOnNetwork); + auto value = static_cast(_payload.rendezvousInformation.Value().Raw()); + + // Ensure a known (HasValue()) but 0 value does not map to MTRDiscoveryCapabilitiesUnknown. + return KnownDiscoveryCapabilities(value); +} + +- (void)setDiscoveryCapabilities:(MTRDiscoveryCapabilities)discoveryCapabilities +{ + if (discoveryCapabilities == MTRDiscoveryCapabilitiesUnknown) { + _payload.rendezvousInformation.ClearValue(); + } else { + auto flags = static_cast(discoveryCapabilities); + _payload.rendezvousInformation.SetValue(chip::RendezvousInformationFlags(flags)); } } -- (NSArray *)getAllOptionalVendorData:(NSError * __autoreleasing *)error +- (NSNumber *)discriminator { - NSMutableArray * allOptionalData = [NSMutableArray new]; - std::vector chipOptionalData = _chipSetupPayload.getAllOptionalVendorData(); - for (chip::OptionalQRCodeInfo chipInfo : chipOptionalData) { - MTROptionalQRCodeInfo * info = [MTROptionalQRCodeInfo new]; - info.tag = [NSNumber numberWithUnsignedChar:chipInfo.tag]; - switch (chipInfo.type) { - case chip::optionalQRCodeInfoTypeString: - info.type = MTROptionalQRCodeInfoTypeString; - info.stringValue = [NSString stringWithUTF8String:chipInfo.data.c_str()]; - break; - case chip::optionalQRCodeInfoTypeInt32: - info.type = MTROptionalQRCodeInfoTypeInt32; - info.integerValue = [NSNumber numberWithInt:chipInfo.int32]; - break; - default: - if (error) { - *error = [NSError errorWithDomain:MTRErrorDomain code:MTRErrorCodeInvalidArgument userInfo:nil]; - } - return nil; - } - [allOptionalData addObject:info]; + VerifyOrReturnValue(_shadowDiscriminator == nil, _shadowDiscriminator); + auto & pd = _payload.discriminator; + return @(pd.IsShortDiscriminator() ? pd.GetShortValue() : pd.GetLongValue()); +} + +- (void)setDiscriminator:(uint16_t)discriminator isShort:(BOOL)isShort +{ + if (isShort) { + _payload.discriminator.SetShortValue(discriminator & kShortDiscriminatorMask); + _shadowDiscriminator = @(discriminator); // keep as a shadow value + } else { + _payload.discriminator.SetLongValue(discriminator); + _shadowDiscriminator = nil; // no need to keep a shadow copy } - return allOptionalData; } -+ (NSUInteger)generateRandomPIN +- (void)setDiscriminator:(NSNumber *)discriminator { - return [[MTRSetupPayload generateRandomSetupPasscode] unsignedIntValue]; + // Truncate down to the range of a long discriminator. Then update + // our SetupPayload with the value, but keep the current shortness. + uint16_t value = discriminator.unsignedIntegerValue & kLongDiscriminatorMask; + [self setDiscriminator:value isShort:self.hasShortDiscriminator]; } -+ (NSNumber *)generateRandomSetupPasscode +- (BOOL)hasShortDiscriminator { - do { - // Make sure the thing we generate is in the right range. - uint32_t setupPIN = arc4random_uniform(chip::kSetupPINCodeMaximumValue) + 1; - if (chip::SetupPayload::IsValidSetupPIN(setupPIN)) { - return @(setupPIN); - } + return _payload.discriminator.IsShortDiscriminator(); +} - // We got pretty unlikely with our random number generation. Just try - // again. The chance that this loop does not terminate in a reasonable - // amount of time is astronomically low, assuming arc4random_uniform is not - // broken. - } while (true); +- (void)setHasShortDiscriminator:(BOOL)hasShortDiscriminator +{ + VerifyOrReturn(hasShortDiscriminator != self.hasShortDiscriminator); + [self setDiscriminator:self.discriminator.unsignedShortValue isShort:hasShortDiscriminator]; +} - // Not reached. - return @(chip::kSetupPINCodeUndefinedValue); +- (NSNumber *)setupPasscode +{ + return @(_payload.setUpPINCode); } -+ (bool)isQRCode:(NSString *)onboardingPayload +- (void)setSetupPasscode:(NSNumber *)setupPasscode { - return [onboardingPayload hasPrefix:@"MT:"]; + _payload.setUpPINCode = static_cast(setupPasscode.unsignedIntegerValue); } -+ (MTRSetupPayload * _Nullable)setupPayloadWithOnboardingPayload:(NSString *)onboardingPayload - error:(NSError * __autoreleasing *)error +- (nullable NSString *)serialNumber { - // TODO: Do we actually need the MTROnboardingPayloadParser abstraction? - MTRSetupPayload * payload = [MTROnboardingPayloadParser setupPayloadForOnboardingPayload:onboardingPayload error:error]; - if (payload == nil) { - return nil; - } + std::string value; + VerifyOrReturnValue(_payload.getSerialNumber(value) == CHIP_NO_ERROR, nil); + return [NSString stringWithUTF8String:value.c_str()]; +} - bool isQRCode = [MTRSetupPayload isQRCode:onboardingPayload]; - bool validPayload; - if (isQRCode) { - validPayload = payload->_chipSetupPayload.isValidQRCodePayload(); +- (void)setSerialNumber:(nullable NSString *)serialNumber +{ + if (serialNumber) { + NSString * existing = self.serialNumber; + if (existing) { + // The underlying TLV tag can be encoded as either a string or an integer, + // avoid changing it if the represented serial number is not changing. + VerifyOrReturn(![existing isEqualToString:serialNumber]); + _payload.removeSerialNumber(); + } + CHIP_ERROR err = _payload.addSerialNumber(serialNumber.UTF8String); + if (err != CHIP_NO_ERROR) { + MTR_LOG_ERROR("Ignoring unexpected error in SetupPayload::addSerialNumber: %" CHIP_ERROR_FORMAT, err.Format()); + } } else { - validPayload = payload->_chipSetupPayload.isValidManualCode(); + _payload.removeSerialNumber(); } +} - if (!validPayload) { - if (error) { - *error = [MTRError errorForCHIPErrorCode:CHIP_ERROR_INVALID_ARGUMENT logContext:onboardingPayload]; +- (NSArray *)vendorElements +{ + std::vector elements = _payload.getAllOptionalVendorData(); + VerifyOrReturnValue(!elements.empty(), @[]); + NSMutableArray * infos = [[NSMutableArray alloc] initWithCapacity:elements.size()]; + for (auto const & element : elements) { + MTROptionalQRCodeInfo * info = [[MTROptionalQRCodeInfo alloc] initWithQRCodeInfo:element]; + if (info != nil) { // ignore invalid elements (there shouldn't be any) + [infos addObject:info]; } - return nil; } + return infos; +} - return payload; +- (MTROptionalQRCodeInfo *)vendorElementWithTag:(NSNumber *)tag +{ + chip::OptionalQRCodeInfo element; + VerifyOrReturnValue(_payload.getOptionalVendorData(ValidateVendorTag(tag), element) == CHIP_NO_ERROR, nil); + return [[MTROptionalQRCodeInfo alloc] initWithQRCodeInfo:element]; } -- (NSString *)description +- (void)removeVendorElementWithTag:(NSNumber *)tag { - NSMutableArray * capabilities = [NSMutableArray array]; - if (self.discoveryCapabilities & MTRDiscoveryCapabilitiesSoftAP) { - [capabilities addObject:@"SoftAP"]; - } - if (self.discoveryCapabilities & MTRDiscoveryCapabilitiesBLE) { - [capabilities addObject:@"BLE"]; + _payload.removeOptionalVendorData(ValidateVendorTag(tag)); +} + +- (void)addOrReplaceVendorElement:(MTROptionalQRCodeInfo *)element +{ + MTRVerifyArgumentOrDie(element != nil, @"element"); + CHIP_ERROR err = [element addAsVendorElementTo:_payload]; + VerifyOrDieWithMsg(err == CHIP_NO_ERROR, NotSpecified, "Internal error: %" CHIP_ERROR_FORMAT, err.Format()); +} + +#pragma mark - Export methods + +- (nullable NSString *)manualEntryCode +{ + chip::ManualSetupPayloadGenerator generator(_payload); + std::string result; + VerifyOrReturnValue(generator.payloadDecimalStringRepresentation(result) == CHIP_NO_ERROR, nil); + return [NSString stringWithUTF8String:result.c_str()]; +} + +- (nullable NSString *)qrCodeString +{ + return [self qrCodeStringSkippingValidation:NO]; +} + +- (nullable NSString *)qrCodeStringSkippingValidation:(BOOL)allowInvalid +{ + chip::QRCodeSetupPayloadGenerator generator(_payload); + generator.SetAllowInvalidPayload(allowInvalid); + std::string result; + CHIP_ERROR err = generator.payloadBase38RepresentationWithAutoTLVBuffer(result); + if (allowInvalid) { + // Encoding should always work if invalid payloads are allowed + VerifyOrDieWithMsg(err == CHIP_NO_ERROR, NotSpecified, "Internal error: %" CHIP_ERROR_FORMAT, err.Format()); + } else { + VerifyOrReturnValue(err == CHIP_NO_ERROR, nil); } - if (self.discoveryCapabilities & MTRDiscoveryCapabilitiesOnNetwork) { - [capabilities addObject:@"OnNetwork"]; + return [NSString stringWithUTF8String:result.c_str()]; +} + +#pragma mark - Miscellaneous + +- (id)copyWithZone:(NSZone *)zone +{ + MTRSetupPayload * copy = [[MTRSetupPayload alloc] initWithSetupPayload:_payload]; + copy->_shadowDiscriminator = _shadowDiscriminator; + return copy; +} + +- (NSUInteger)hash +{ + return self.discriminator.unsignedIntegerValue; +} + +- (BOOL)isEqual:(id)object +{ + VerifyOrReturnValue([object class] == [self class], NO); + MTRSetupPayload * other = object; + VerifyOrReturnValue(_payload == other->_payload, NO); + VerifyOrReturnValue(MTREqualObjects(_shadowDiscriminator, other->_shadowDiscriminator), NO); + return YES; +} + +- (NSString *)description +{ + // Note: The description does not include the passcode for security reasons! + + NSMutableString * result = [NSMutableString stringWithFormat:@"", - self.discriminator.unsignedIntValue, self.hasShortDiscriminator ? @"YES" : @"NO", [capabilities componentsJoinedByString:@"|"]]; + [result appendString:@">"]; + return result; } #pragma mark - NSSecureCoding @@ -272,11 +541,12 @@ - (NSString *)description static NSString * const MTRSetupPayloadCodingKeyVendorID = @"MTRSP.ck.vendorID"; static NSString * const MTRSetupPayloadCodingKeyProductID = @"MTRSP.ck.productID"; static NSString * const MTRSetupPayloadCodingKeyCommissioningFlow = @"MTRSP.ck.commissioningFlow"; -static NSString * const MTRSetupPayloadCodingKeyDiscoveryCapabilities = @"MTRSP.ck.rendezvousFlags"; +static NSString * const MTRSetupPayloadCodingKeyRendevouzInformation = @"MTRSP.ck.rendezvousFlags"; static NSString * const MTRSetupPayloadCodingKeyHasShortDiscriminator = @"MTRSP.ck.hasShortDiscriminator"; static NSString * const MTRSetupPayloadCodingKeyDiscriminator = @"MTRSP.ck.discriminator"; static NSString * const MTRSetupPayloadCodingKeySetupPasscode = @"MTRSP.ck.setupPINCode"; static NSString * const MTRSetupPayloadCodingKeySerialNumber = @"MTRSP.ck.serialNumber"; +static NSString * const MTRSetupPayloadCodingKeyQRCode = @"qr"; + (BOOL)supportsSecureCoding { @@ -285,186 +555,148 @@ + (BOOL)supportsSecureCoding - (void)encodeWithCoder:(NSCoder *)coder { + [coder encodeObject:[self qrCodeStringSkippingValidation:YES] forKey:MTRSetupPayloadCodingKeyQRCode]; [coder encodeObject:self.version forKey:MTRSetupPayloadCodingKeyVersion]; [coder encodeObject:self.vendorID forKey:MTRSetupPayloadCodingKeyVendorID]; [coder encodeObject:self.productID forKey:MTRSetupPayloadCodingKeyProductID]; - // Casts are safe because commissioning flow, discoveryCapabilities, and - // hasShortDiscriminator values are all pretty small and non-negative. - [coder encodeInteger:static_cast(self.commissioningFlow) forKey:MTRSetupPayloadCodingKeyCommissioningFlow]; - // We used to encode the discovery capabilities as an NSNumber object, with - // nil representing "unknown". Keep doing that, for backwards compat. - [coder encodeObject:[MTRSetupPayload _boxDiscoveryCapabilities:self.discoveryCapabilities] - forKey:MTRSetupPayloadCodingKeyDiscoveryCapabilities]; - [coder encodeInteger:static_cast(self.hasShortDiscriminator) forKey:MTRSetupPayloadCodingKeyHasShortDiscriminator]; + [coder encodeInteger:self.commissioningFlow forKey:MTRSetupPayloadCodingKeyCommissioningFlow]; + // For compatibility reasons, keep encoding rendevouzInformation instead of discoveryCapabilities + [coder encodeObject:self.rendezvousInformation forKey:MTRSetupPayloadCodingKeyRendevouzInformation]; + [coder encodeInteger:(self.hasShortDiscriminator ? 1 : 0) forKey:MTRSetupPayloadCodingKeyHasShortDiscriminator]; [coder encodeObject:self.discriminator forKey:MTRSetupPayloadCodingKeyDiscriminator]; [coder encodeObject:self.setupPasscode forKey:MTRSetupPayloadCodingKeySetupPasscode]; [coder encodeObject:self.serialNumber forKey:MTRSetupPayloadCodingKeySerialNumber]; } -- (instancetype _Nullable)initWithCoder:(NSCoder *)decoder -{ - NSNumber * version = [decoder decodeObjectOfClass:[NSNumber class] forKey:MTRSetupPayloadCodingKeyVersion]; - NSNumber * vendorID = [decoder decodeObjectOfClass:[NSNumber class] forKey:MTRSetupPayloadCodingKeyVendorID]; - NSNumber * productID = [decoder decodeObjectOfClass:[NSNumber class] forKey:MTRSetupPayloadCodingKeyProductID]; - NSInteger commissioningFlow = [decoder decodeIntegerForKey:MTRSetupPayloadCodingKeyCommissioningFlow]; - MTRDiscoveryCapabilities discoveryCapabilities = - [MTRSetupPayload _unboxDiscoveryCapabilities:[decoder decodeObjectOfClass:[NSNumber class] - forKey:MTRSetupPayloadCodingKeyDiscoveryCapabilities]]; - NSInteger hasShortDiscriminator = [decoder decodeIntegerForKey:MTRSetupPayloadCodingKeyHasShortDiscriminator]; - NSNumber * discriminator = [decoder decodeObjectOfClass:[NSNumber class] forKey:MTRSetupPayloadCodingKeyDiscriminator]; - NSNumber * setupPasscode = [decoder decodeObjectOfClass:[NSNumber class] forKey:MTRSetupPayloadCodingKeySetupPasscode]; - NSString * serialNumber = [decoder decodeObjectOfClass:[NSString class] forKey:MTRSetupPayloadCodingKeySerialNumber]; - - MTRSetupPayload * payload = [[MTRSetupPayload alloc] init]; - payload.version = version; - payload.vendorID = vendorID; - payload.productID = productID; - payload.commissioningFlow = static_cast(commissioningFlow); - payload.discoveryCapabilities = discoveryCapabilities; - payload.hasShortDiscriminator = static_cast(hasShortDiscriminator); - payload.discriminator = discriminator; - payload.setupPasscode = setupPasscode; - payload.serialNumber = serialNumber; - - return payload; -} - -- (NSString * _Nullable)manualEntryCode +- (instancetype)initWithCoder:(NSCoder *)coder { - CHIP_ERROR err = CHIP_NO_ERROR; - std::string outDecimalString; - chip::SetupPayload payload; + self = [super init]; - /// The 11 digit manual pairing code only requires the version, VID_PID present flag, - /// discriminator, and the setup pincode. - payload.version = [self.version unsignedCharValue]; - if (self.hasShortDiscriminator) { - payload.discriminator.SetShortValue([self.discriminator unsignedCharValue]); + // We can't rely on the QR code to be present because older versions of this class + // did not encode it. When present, it carries almost the entire state of the object. + NSString * qrCode = [coder decodeObjectOfClass:NSString.class forKey:MTRSetupPayloadCodingKeyQRCode]; + if (qrCode != nil) { + [self initializeFromQRCode:qrCode]; } else { - payload.discriminator.SetLongValue([self.discriminator unsignedShortValue]); + self.version = [coder decodeObjectOfClass:NSNumber.class forKey:MTRSetupPayloadCodingKeyVersion]; + self.vendorID = [coder decodeObjectOfClass:NSNumber.class forKey:MTRSetupPayloadCodingKeyVendorID]; + self.productID = [coder decodeObjectOfClass:NSNumber.class forKey:MTRSetupPayloadCodingKeyProductID]; + self.commissioningFlow = static_cast([coder decodeIntegerForKey:MTRSetupPayloadCodingKeyCommissioningFlow]); + self.setupPasscode = [coder decodeObjectOfClass:NSNumber.class forKey:MTRSetupPayloadCodingKeySetupPasscode]; + self.serialNumber = [coder decodeObjectOfClass:NSString.class forKey:MTRSetupPayloadCodingKeySerialNumber]; } - payload.setUpPINCode = [self.setupPasscode unsignedIntValue]; - err = chip::ManualSetupPayloadGenerator(payload).payloadDecimalStringRepresentation(outDecimalString); + // The QR code cannot represent short discriminators or the absence of rendevouz + // information, so always decode the state of those properties separately. + self.hasShortDiscriminator = ([coder decodeIntegerForKey:MTRSetupPayloadCodingKeyHasShortDiscriminator] != 0); + self.discriminator = [coder decodeObjectOfClass:NSNumber.class forKey:MTRSetupPayloadCodingKeyDiscriminator]; + // For compatibility reasons, keep decoding rendevouzInformation instead of discoveryCapabilities + self.rendezvousInformation = [coder decodeObjectOfClass:NSNumber.class forKey:MTRSetupPayloadCodingKeyRendevouzInformation]; - if (err != CHIP_NO_ERROR) { - return nil; - } - - return [NSString stringWithUTF8String:outDecimalString.c_str()]; + return self; } -- (NSString * _Nullable)qrCodeString:(NSError * __autoreleasing *)error +#pragma mark - Utility class methods + ++ (NSUInteger)generateRandomPIN { - if (self.commissioningFlow == MTRCommissioningFlowInvalid) { - // No idea how to map this to the standard codes. - if (error != nil) { - *error = [MTRError errorForCHIPErrorCode:CHIP_ERROR_INCORRECT_STATE logContext:@"invalid flow"]; + do { + // Make sure the thing we generate is in the right range. + uint32_t setupPIN = arc4random_uniform(chip::kSetupPINCodeMaximumValue) + 1; + if (chip::SetupPayload::IsValidSetupPIN(setupPIN)) { + return setupPIN; } - return nil; - } - if (self.hasShortDiscriminator) { - // Can't create a QR code with a short discriminator. - if (error != nil) { - *error = [MTRError errorForCHIPErrorCode:CHIP_ERROR_INCORRECT_STATE logContext:@"cannot create a QR code with a short descrimintor"]; - } - return nil; - } + // We got pretty unlikely with our random number generation. Just try + // again. The chance that this loop does not terminate in a reasonable + // amount of time is astronomically low, assuming arc4random_uniform is not + // broken. + } while (true); +} - if (self.discoveryCapabilities == MTRDiscoveryCapabilitiesUnknown) { - // Can't create a QR code if we don't know the discovery capabilities. - if (error != nil) { - *error = [MTRError errorForCHIPErrorCode:CHIP_ERROR_INCORRECT_STATE logContext:@"cannot create a QR code with unknown discovery capabilities"]; - } - return nil; - } ++ (NSNumber *)generateRandomSetupPasscode +{ + return @([self generateRandomPIN]); +} - chip::SetupPayload payload; - - payload.version = [self.version unsignedCharValue]; - payload.vendorID = [self.vendorID unsignedShortValue]; - payload.productID = [self.productID unsignedShortValue]; - payload.commissioningFlow = [MTRSetupPayload unconvertCommissioningFlow:self.commissioningFlow]; - payload.rendezvousInformation = [MTRSetupPayload convertDiscoveryCapabilities:self.discoveryCapabilities]; - payload.discriminator.SetLongValue([self.discriminator unsignedShortValue]); - payload.setUpPINCode = [self.setupPasscode unsignedIntValue]; - if (self.serialNumber != nil) { - CHIP_ERROR err = payload.addSerialNumber(self.serialNumber.UTF8String); - if (err != CHIP_NO_ERROR) { - if (error != nil) { - *error = [MTRError errorForCHIPErrorCode:err]; - } - return nil; - } - } +@end - std::string outQRCodeString; - CHIP_ERROR err = chip::QRCodeSetupPayloadGenerator(payload).payloadBase38RepresentationWithAutoTLVBuffer(outQRCodeString); +MTR_DIRECT_MEMBERS +@implementation MTROptionalQRCodeInfo (Deprecated) - if (err != CHIP_NO_ERROR) { - if (error != nil) { - *error = [MTRError errorForCHIPErrorCode:err]; - } - return nil; - } +- (instancetype)init +{ + return [self initWithTag:@0xff stringValue:@""]; +} - return [NSString stringWithUTF8String:outQRCodeString.c_str()]; +- (void)setType:(MTROptionalQRCodeInfoType)type +{ + /* ignored */ } -+ (nullable NSNumber *)_boxDiscoveryCapabilities:(MTRDiscoveryCapabilities)discoveryCapabilities +- (void)setTag:(NSNumber *)tag { - if (discoveryCapabilities == MTRDiscoveryCapabilitiesUnknown) { - return nil; - } + /* ignored */ +} - return @(discoveryCapabilities); +- (NSNumber *)infoType +{ + return @(self.type); } -+ (MTRDiscoveryCapabilities)_unboxDiscoveryCapabilities:(nullable NSNumber *)boxedDiscoveryCapabilities +- (void)setInfoType:(NSNumber *)infoType { - if (boxedDiscoveryCapabilities == nil) { - return MTRDiscoveryCapabilitiesUnknown; - } + /* ignored */ +} - NSUInteger value = [boxedDiscoveryCapabilities unsignedIntegerValue]; - if (value == MTRDiscoveryCapabilitiesUnknown) { - // The discovery capabilities were actually known - // (rendezvousInformation is not nil), and must support on-network. - return MTRDiscoveryCapabilitiesOnNetwork; - } +- (void)setIntegerValue:(NSNumber *)integerValue +{ + /* ignored */ +} - return static_cast(value); +- (void)setStringValue:(NSString *)stringValue +{ + /* ignored */ } @end MTR_DIRECT_MEMBERS -@implementation MTROptionalQRCodeInfo (Deprecated) +@implementation MTRSetupPayload (Deprecated) -- (NSNumber *)infoType +- (instancetype)init { - return @(self.type); + return [super init]; // a default-constructed SetupPayload is fine here } -- (void)setInfoType:(NSNumber *)infoType ++ (instancetype)new { - self.type = static_cast([infoType unsignedIntegerValue]); + return [super new]; } -@end - -MTR_DIRECT_MEMBERS -@implementation MTRSetupPayload (Deprecated) ++ (MTRSetupPayload * _Nullable)setupPayloadWithOnboardingPayload:(NSString *)onboardingPayload + error:(NSError * __autoreleasing *)error +{ + MTRSetupPayload * payload = [[MTRSetupPayload alloc] initWithPayload:onboardingPayload]; + if (!payload && error) { + *error = [MTRError errorWithCode:MTRErrorCodeInvalidArgument]; + } + return payload; +} - (nullable NSNumber *)rendezvousInformation { - return [MTRSetupPayload _boxDiscoveryCapabilities:self.discoveryCapabilities]; + auto value = self.discoveryCapabilities; + return (value != MTRDiscoveryCapabilitiesUnknown) ? @(value) : nil; } - (void)setRendezvousInformation:(nullable NSNumber *)rendezvousInformation { - self.discoveryCapabilities = [MTRSetupPayload _unboxDiscoveryCapabilities:rendezvousInformation]; + if (rendezvousInformation != nil) { + self.discoveryCapabilities = KnownDiscoveryCapabilities(rendezvousInformation.unsignedIntegerValue); + } else { + self.discoveryCapabilities = MTRDiscoveryCapabilitiesUnknown; + } } - (NSNumber *)setUpPINCode @@ -477,26 +709,18 @@ - (void)setSetUpPINCode:(NSNumber *)setUpPINCode self.setupPasscode = setUpPINCode; } -- (instancetype)init +- (nullable NSString *)qrCodeString:(NSError * __autoreleasing _Nullable *)error { - if (self = [super init]) { - _version = @(0); // Only supported Matter version so far. - _vendorID = @(0); // Not available. - _productID = @(0); // Not available. - _commissioningFlow = MTRCommissioningFlowStandard; - _discoveryCapabilities = MTRDiscoveryCapabilitiesUnknown; - _hasShortDiscriminator = NO; - _discriminator = @(0); - _setupPasscode = @(11111111); // Invalid passcode - _serialNumber = nil; + NSString * result = [self qrCodeString]; + if (!result && error) { + *error = [MTRError errorForCHIPErrorCode:CHIP_ERROR_INCORRECT_STATE]; } - - return self; + return result; } -+ (instancetype)new +- (NSArray *)getAllOptionalVendorData:(NSError * __autoreleasing *)error { - return [[self alloc] init]; + return self.vendorElements; // never actually fails } @end diff --git a/src/darwin/Framework/CHIP/MTRSetupPayload_Internal.h b/src/darwin/Framework/CHIP/MTRSetupPayload_Internal.h index a119a4b6e2059f..5e14ce025c8f73 100644 --- a/src/darwin/Framework/CHIP/MTRSetupPayload_Internal.h +++ b/src/darwin/Framework/CHIP/MTRSetupPayload_Internal.h @@ -19,18 +19,17 @@ #import "MTRDefines_Internal.h" -#ifdef __cplusplus -#import #import -#endif + +NS_ASSUME_NONNULL_BEGIN MTR_DIRECT_MEMBERS @interface MTRSetupPayload () -#ifdef __cplusplus - (instancetype)initWithSetupPayload:(chip::SetupPayload)setupPayload; -- (MTRDiscoveryCapabilities)convertRendezvousFlags:(const chip::Optional &)value; -- (MTRCommissioningFlow)convertCommissioningFlow:(chip::CommissioningFlow)value; -#endif +- (nullable instancetype)initWithQRCode:(NSString *)qrCodePayload; +- (nullable instancetype)initWithManualPairingCode:(NSString *)manualCode; @end + +NS_ASSUME_NONNULL_END diff --git a/src/darwin/Framework/CHIP/MTRUtilities.h b/src/darwin/Framework/CHIP/MTRUtilities.h new file mode 100644 index 00000000000000..754f5da7ecadd6 --- /dev/null +++ b/src/darwin/Framework/CHIP/MTRUtilities.h @@ -0,0 +1,27 @@ +/** + * + * 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 + +/** + * Calls `-[NSObject isEqual:]` but returns YES when comparing nil to nil. + */ +MTR_EXTERN BOOL MTREqualObjects(id _Nullable a, id _Nullable b); + +NS_ASSUME_NONNULL_END diff --git a/src/darwin/Framework/CHIP/MTRUtilities.mm b/src/darwin/Framework/CHIP/MTRUtilities.mm new file mode 100644 index 00000000000000..de85b498626495 --- /dev/null +++ b/src/darwin/Framework/CHIP/MTRUtilities.mm @@ -0,0 +1,23 @@ +/** + * + * 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 "MTRUtilities.h" + +BOOL MTREqualObjects(id _Nullable a, id _Nullable b) +{ + return (a == nil) ? (b == nil) : [a isEqual:b]; +} diff --git a/src/darwin/Framework/CHIPTests/MTRSetupPayloadTests.m b/src/darwin/Framework/CHIPTests/MTRSetupPayloadTests.m index 7060c3ba11599c..6f53305f943a69 100644 --- a/src/darwin/Framework/CHIPTests/MTRSetupPayloadTests.m +++ b/src/darwin/Framework/CHIPTests/MTRSetupPayloadTests.m @@ -1,6 +1,6 @@ /** * - * Copyright (c) 2020 Project CHIP Authors + * Copyright (c) 2020-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. @@ -40,6 +40,8 @@ - (void)testOnboardingPayloadParser_Manual_NoError XCTAssertEqual(payload.commissioningFlow, MTRCommissioningFlowCustom); XCTAssertEqual(payload.version.unsignedIntegerValue, 0); XCTAssertEqual(payload.discoveryCapabilities, MTRDiscoveryCapabilitiesUnknown); + + XCTAssertEqualObjects(payload.manualEntryCode, @"641286075300001000016"); } - (void)testOnboardingPayloadParser_QRCode_NoError @@ -90,31 +92,13 @@ - (void)testOnboardingPayloadParser_NFC_NoError XCTAssertEqual(payload.discoveryCapabilities, MTRDiscoveryCapabilitiesSoftAP); } -- (void)testManualParser -{ - NSError * error; - MTRSetupPayload * payload = [MTRSetupPayload setupPayloadWithOnboardingPayload:@"641286075300001000016" error:&error]; - - XCTAssertNotNil(payload); - XCTAssertNil(error); - - XCTAssertTrue(payload.hasShortDiscriminator); - XCTAssertEqual(payload.discriminator.unsignedIntegerValue, 10); - XCTAssertEqual(payload.setupPasscode.unsignedIntegerValue, 12345670); - XCTAssertEqual(payload.vendorID.unsignedIntegerValue, 1); - XCTAssertEqual(payload.productID.unsignedIntegerValue, 1); - XCTAssertEqual(payload.commissioningFlow, MTRCommissioningFlowCustom); - XCTAssertEqual(payload.version.unsignedIntegerValue, 0); - XCTAssertEqual(payload.discoveryCapabilities, MTRDiscoveryCapabilitiesUnknown); -} - - (void)testManualParser_Error { NSError * error; MTRSetupPayload * payload = [MTRSetupPayload setupPayloadWithOnboardingPayload:@"" error:&error]; XCTAssertNil(payload); - XCTAssertEqual(error.code, MTRErrorCodeInvalidStringLength); + XCTAssertNotNil(error); } - (void)testQRCodeParser_Error @@ -123,7 +107,7 @@ - (void)testQRCodeParser_Error MTRSetupPayload * payload = [MTRSetupPayload setupPayloadWithOnboardingPayload:@"MT:M5L90MP500K64J0000." error:&error]; XCTAssertNil(payload); - XCTAssertEqual(error.code, MTRErrorCodeInvalidArgument); + XCTAssertNotNil(error); } - (void)testQRCodeParser @@ -178,6 +162,62 @@ - (void)testQRCodeParserWithOptionalData XCTAssertEqual(info.integerValue.intValue, 12); } } + + // Test access by tag + XCTAssertEqualObjects([payload vendorElementWithTag:@130].stringValue, @"myData"); + XCTAssertEqualObjects([payload vendorElementWithTag:@131].integerValue, @12); +} + +- (void)testAddVendorElement +{ + MTRSetupPayload * payload = [[MTRSetupPayload alloc] initWithSetupPasscode:@314159 discriminator:@555]; + XCTAssertEqual(payload.vendorElements.count, 0); + [payload addOrReplaceVendorElement:[[MTROptionalQRCodeInfo alloc] initWithTag:@0xff int32Value:42]]; + XCTAssertEqual(payload.vendorElements.count, 1); + XCTAssertEqualObjects(payload.vendorElements.firstObject.integerValue, @42); + XCTAssertEqualObjects([payload vendorElementWithTag:@0xff].integerValue, @42); +} + +- (void)testVendorElementsEncodedToQRCode +{ + MTRSetupPayload * payload = [[MTRSetupPayload alloc] initWithSetupPasscode:@314159 discriminator:@555]; + [payload addOrReplaceVendorElement:[[MTROptionalQRCodeInfo alloc] initWithTag:@0x80 stringValue:@"Hello"]]; + MTRSetupPayload * decoded = [[MTRSetupPayload alloc] initWithPayload:payload.qrCodeString]; + XCTAssertNotNil(decoded); + XCTAssertEqualObjects([decoded vendorElementWithTag:@0x80].stringValue, @"Hello"); +} + +- (void)testRemoveVendorElements +{ + MTRSetupPayload * payload = [[MTRSetupPayload alloc] initWithPayload:@"MT:M5L90MP500K64J0A33P0SET70.QT52B.E23-WZE0WISA0DK5N1K8SQ1RYCU1O0"]; + XCTAssertNotNil(payload); + XCTAssertEqual(payload.vendorElements.count, 2); + [payload removeVendorElementWithTag:@128]; // no change, no vendor element present with this tag + XCTAssertEqual(payload.vendorElements.count, 2); + [payload removeVendorElementWithTag:@130]; + XCTAssertEqual(payload.vendorElements.count, 1); + [payload removeVendorElementWithTag:@131]; + XCTAssertEqual(payload.vendorElements.count, 0); +} + +- (void)testQrCodeInfoCopyAndEquality +{ + MTROptionalQRCodeInfo * a = [[MTROptionalQRCodeInfo alloc] initWithTag:@0x88 stringValue:@"hello"]; + MTROptionalQRCodeInfo * b = [[MTROptionalQRCodeInfo alloc] initWithTag:@0x88 stringValue:@"hello"]; + MTROptionalQRCodeInfo * c = [[MTROptionalQRCodeInfo alloc] initWithTag:@0xff stringValue:@"hello"]; + MTROptionalQRCodeInfo * d = [[MTROptionalQRCodeInfo alloc] initWithTag:@0x88 int32Value:42]; + MTROptionalQRCodeInfo * e = [[MTROptionalQRCodeInfo alloc] initWithTag:@0x88 int32Value:0xbad]; + XCTAssertTrue([a isEqual:a]); + XCTAssertTrue([a isEqual:[a copy]]); + XCTAssertTrue([a isEqual:b]); + XCTAssertTrue([a isEqual:[b copy]]); + XCTAssertEqual(a.hash, b.hash); + XCTAssertEqual(a.hash, [[a copy] hash]); + XCTAssertFalse([a isEqual:nil]); + XCTAssertFalse([a isEqual:c]); + XCTAssertFalse([a isEqual:d]); + XCTAssertFalse([a isEqual:e]); + XCTAssertFalse([d isEqual:e]); } - (void)testQRCodeWithNoCapabilities @@ -198,7 +238,7 @@ - (void)testQRCodeWithNoCapabilities XCTAssertEqual(payload.discoveryCapabilities, MTRDiscoveryCapabilitiesOnNetwork); } -- (void)testQRCodePropertyAliases +- (void)testDeprecatedPropertyAliases { NSError * error; MTRSetupPayload * payload = [MTRSetupPayload setupPayloadWithOnboardingPayload:@"MT:M5L9000000K64J00000" error:&error]; @@ -294,6 +334,9 @@ - (void)testSecureCodingRoundtrip ]) { MTRSetupPayload * payload = [MTRSetupPayload setupPayloadWithOnboardingPayload:string error:&error]; XCTAssertNotNil(payload, @"Error: %@", error); + // Ensure our test data has discriminator values that make for the most meaningful round-trip tests. + XCTAssert(payload.discriminator.integerValue != 0); + XCTAssert(payload.hasShortDiscriminator || payload.discriminator.integerValue > 0xf); [payloads addObject:payload]; } @@ -301,7 +344,14 @@ - (void)testSecureCodingRoundtrip [payloads addObject:[[MTRSetupPayload alloc] init]]; [payloads addObject:[[MTRSetupPayload alloc] initWithSetupPasscode:@22222222 discriminator:@42]]; + MTRSetupPayload * futureValues = [[MTRSetupPayload alloc] initWithSetupPasscode:@314159 discriminator:@555]; + futureValues.commissioningFlow = 3; // reserved in the spec + futureValues.discoveryCapabilities = 0x84; // bits 3-7 reserved in the spec + [payloads addObject:futureValues]; + for (MTRSetupPayload * payload in payloads) { + NSLog(@"Payload: %@", payload); + NSData * data = [NSKeyedArchiver archivedDataWithRootObject:payload requiringSecureCoding:YES error:&error]; XCTAssertNotNil(data, @"Error: %@", error); MTRSetupPayload * decoded = [NSKeyedUnarchiver unarchivedObjectOfClass:MTRSetupPayload.class fromData:data error:&error]; @@ -322,18 +372,16 @@ - (void)testSecureCodingRoundtrip NSArray * decodedVDs = [decoded getAllOptionalVendorData:&error]; XCTAssertNotNil(decodedVDs, @"Error: %@", error); -#if 0 // TODO: Encode / decode optional vendor data - // MTROptionalQRCodeInfo does not implement isEqual (yet) XCTAssertEqual(decodedVDs.count, payloadVDs.count); - for (int i = 0; i < decodedVDs.count; i++){ + for (int i = 0; i < decodedVDs.count; i++) { MTROptionalQRCodeInfo * decodedVD = decodedVDs[i]; MTROptionalQRCodeInfo * payloadVD = payloadVDs[i]; XCTAssertEqual(decodedVD.type, payloadVD.type); XCTAssertEqualObjects(decodedVD.tag, payloadVD.tag); XCTAssertEqualObjects(decodedVD.integerValue, payloadVD.integerValue); XCTAssertEqualObjects(decodedVD.stringValue, payloadVD.stringValue); + XCTAssertEqualObjects(decodedVD, payloadVD); // also check with isEqual: } -#endif // Note that we can't necessarily expect the manualEntryCode and qrCode strings // we generate here to match the original string, but we should get the same @@ -343,4 +391,52 @@ - (void)testSecureCodingRoundtrip } } +- (void)testCopyingAndEquality +{ + MTRSetupPayload * payload = [[MTRSetupPayload alloc] initWithPayload:@"MT:M5L9000000K64J00000"]; + XCTAssertFalse(payload.hasShortDiscriminator); // came from a QR code + XCTAssert(payload.discriminator.integerValue > 0xf); // can't "accidentally" round-trip through a short discriminator + + MTRSetupPayload * copy = [payload copy]; + XCTAssertNotIdentical(payload, copy); // MTRSetupPayload is mutable, must be a new object + + XCTAssertTrue([payload isEqual:copy]); + XCTAssertTrue([copy isEqual:payload]); + XCTAssertEqual(payload.hash, copy.hash); + + copy.hasShortDiscriminator = YES; + XCTAssertFalse([copy isEqual:payload]); + copy.hasShortDiscriminator = NO; + XCTAssertTrue([copy isEqual:payload]); + XCTAssertEqual(payload.hash, copy.hash); + + MTROptionalQRCodeInfo * element = [[MTROptionalQRCodeInfo alloc] initWithTag:@0x80 stringValue:@"To infinity and beyond!"]; + [copy addOrReplaceVendorElement:element]; + XCTAssertFalse([copy isEqual:payload]); + [payload addOrReplaceVendorElement:element]; + XCTAssertTrue([copy isEqual:payload]); + XCTAssertEqual(payload.hash, copy.hash); + + copy.serialNumber = @"555-123"; + XCTAssertFalse([copy isEqual:payload]); + payload.serialNumber = @"555-123"; + XCTAssertTrue([copy isEqual:payload]); + XCTAssertEqual(payload.hash, copy.hash); +} + +- (void)testCanParseFutureDiscoveryMethod +{ + // We must be able to process QR codes that include discovery methods we don't understand yet + XCTAssertEqual([[MTRSetupPayload alloc] initWithPayload:@"MT:000002VDK3VHMR49000"].discoveryCapabilities, 0x84); + XCTAssertEqual([[MTRSetupPayload alloc] initWithPayload:@"MT:-24J0Q.C.0KA0648G00"].discoveryCapabilities, 0xfa); +} + +- (void)testDescriptionShowsUnknownDiscoveryMethods +{ + MTRSetupPayload * a = [[MTRSetupPayload alloc] initWithSetupPasscode:@888 discriminator:@555]; + MTRSetupPayload * b = [a copy]; + b.discoveryCapabilities |= 0x80; + XCTAssertNotEqualObjects(a.description, b.description); +} + @end diff --git a/src/darwin/Framework/Matter.xcodeproj/project.pbxproj b/src/darwin/Framework/Matter.xcodeproj/project.pbxproj index be1712972895fb..4cb4edf4a8edd3 100644 --- a/src/darwin/Framework/Matter.xcodeproj/project.pbxproj +++ b/src/darwin/Framework/Matter.xcodeproj/project.pbxproj @@ -109,6 +109,8 @@ 3D0C484B29DA4FA0006D811F /* MTRErrorTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 3D0C484A29DA4FA0006D811F /* MTRErrorTests.m */; }; 3D3928D72BBCEA3D00CDEBB2 /* MTRAvailabilityTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 3D3928D62BBCEA3D00CDEBB2 /* MTRAvailabilityTests.m */; settings = {COMPILER_FLAGS = "-UMTR_NO_AVAILABILITY -Wno-unguarded-availability-new"; }; }; 3D4733AF2BDF1B80003DC19B /* MTRSetupPayloadTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 3D4733AE2BDF1B80003DC19B /* MTRSetupPayloadTests.m */; }; + 3D4733B32BE2D1DA003DC19B /* MTRUtilities.h in Headers */ = {isa = PBXBuildFile; fileRef = 3D4733B22BE2D1CF003DC19B /* MTRUtilities.h */; }; + 3D4733B52BE2D3D7003DC19B /* MTRUtilities.mm in Sources */ = {isa = PBXBuildFile; fileRef = 3D4733B42BE2D3D1003DC19B /* MTRUtilities.mm */; }; 3D69868529383096007314E7 /* com.csa.matter.plist in Copy Logging Preferences */ = {isa = PBXBuildFile; fileRef = 3D69868029382EF4007314E7 /* com.csa.matter.plist */; }; 3D843711294977000070D20A /* NSStringSpanConversion.h in Headers */ = {isa = PBXBuildFile; fileRef = 3D84370E294977000070D20A /* NSStringSpanConversion.h */; }; 3D843712294977000070D20A /* MTRCallbackBridgeBase.h in Headers */ = {isa = PBXBuildFile; fileRef = 3D84370F294977000070D20A /* MTRCallbackBridgeBase.h */; }; @@ -500,6 +502,8 @@ 3D0C484A29DA4FA0006D811F /* MTRErrorTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MTRErrorTests.m; sourceTree = ""; }; 3D3928D62BBCEA3D00CDEBB2 /* MTRAvailabilityTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MTRAvailabilityTests.m; sourceTree = ""; }; 3D4733AE2BDF1B80003DC19B /* MTRSetupPayloadTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MTRSetupPayloadTests.m; sourceTree = ""; }; + 3D4733B22BE2D1CF003DC19B /* MTRUtilities.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MTRUtilities.h; sourceTree = ""; }; + 3D4733B42BE2D3D1003DC19B /* MTRUtilities.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = MTRUtilities.mm; sourceTree = ""; }; 3D69868029382EF4007314E7 /* com.csa.matter.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = com.csa.matter.plist; sourceTree = ""; }; 3D84370E294977000070D20A /* NSStringSpanConversion.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = NSStringSpanConversion.h; sourceTree = ""; }; 3D84370F294977000070D20A /* MTRCallbackBridgeBase.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MTRCallbackBridgeBase.h; sourceTree = ""; }; @@ -1355,6 +1359,8 @@ 997DED152695343400975E97 /* MTRThreadOperationalDataset.mm */, 51C659D72BA3787500C54922 /* MTRTimeUtils.h */, 51C659D82BA3787500C54922 /* MTRTimeUtils.mm */, + 3D4733B22BE2D1CF003DC19B /* MTRUtilities.h */, + 3D4733B42BE2D3D1003DC19B /* MTRUtilities.mm */, 3D843710294977000070D20A /* NSDataSpanConversion.h */, 3D84370E294977000070D20A /* NSStringSpanConversion.h */, 5117DD3729A931AE00FFA1AA /* MTROperationalBrowser.h */, @@ -1643,6 +1649,7 @@ 514C7A022B64223400DD6D7B /* MTRServerEndpoint_Internal.h in Headers */, 511913FC28C100EF009235E9 /* MTRBaseSubscriptionCallback.h in Headers */, 51565CB12A7AD77600469F18 /* MTRDeviceControllerDataStore.h in Headers */, + 3D4733B32BE2D1DA003DC19B /* MTRUtilities.h in Headers */, 3D843713294977000070D20A /* NSDataSpanConversion.h in Headers */, 991DC08B247704DC00C13860 /* MTRLogging_Internal.h in Headers */, 51FE723F2ACDEF3E00437032 /* MTRCommandPayloadExtensions_Internal.h in Headers */, @@ -1893,6 +1900,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 3D4733B52BE2D3D7003DC19B /* MTRUtilities.mm in Sources */, 2C8C8FC2253E0C2100797F05 /* MTRPersistentStorageDelegateBridge.mm in Sources */, 99AECC802798A57F00B6355B /* MTRCommissioningParameters.mm in Sources */, 2CB7163C252E8A7C0026E2BB /* MTRDeviceControllerDelegateBridge.mm in Sources */,