From 78e3a09539f94fdab03a06aae1a726e8081d5117 Mon Sep 17 00:00:00 2001 From: Brandon Sneed Date: Thu, 26 Jan 2023 13:54:36 -0800 Subject: [PATCH] Fix null settings entry crash on tvOS (#1052) --- Segment.xcodeproj/project.pbxproj | 6 +++ Segment/Internal/SEGUserDefaultsStorage.m | 8 ++-- Segment/Internal/SEGUtils.h | 9 +++++ Segment/Internal/SEGUtils.m | 48 ++++++++++++++++++++++ SegmentTests/AnalyticsTests.swift | 26 ++++++++++++ SegmentTests/Utils/tvOSSettingsBad.json | 49 +++++++++++++++++++++++ 6 files changed, 142 insertions(+), 4 deletions(-) create mode 100644 SegmentTests/Utils/tvOSSettingsBad.json diff --git a/Segment.xcodeproj/project.pbxproj b/Segment.xcodeproj/project.pbxproj index a007fa6c2..3da87f46f 100644 --- a/Segment.xcodeproj/project.pbxproj +++ b/Segment.xcodeproj/project.pbxproj @@ -7,6 +7,8 @@ objects = { /* Begin PBXBuildFile section */ + 460367F52981A8E30081CCA3 /* tvOSSettingsBad.json in Resources */ = {isa = PBXBuildFile; fileRef = 460367F42981A8E30081CCA3 /* tvOSSettingsBad.json */; }; + 460367F62981AAA50081CCA3 /* tvOSSettingsBad.json in Resources */ = {isa = PBXBuildFile; fileRef = 460367F42981A8E30081CCA3 /* tvOSSettingsBad.json */; }; 5AF0E8AE77F57B356DACCFE7 /* AutoScreenReportingTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5AF0E8457CCFC077382BF449 /* AutoScreenReportingTest.swift */; }; 630FC8EA2107F2A500A759C5 /* SEGScreenReporting.h in Headers */ = {isa = PBXBuildFile; fileRef = 5AF0EDFEDC0EE79A0A0EB713 /* SEGScreenReporting.h */; settings = {ATTRIBUTES = (Public, ); }; }; 6E265C791FB1178C0030E08E /* IntegrationsManagerTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E265C781FB1178C0030E08E /* IntegrationsManagerTest.swift */; }; @@ -98,6 +100,7 @@ /* End PBXContainerItemProxy section */ /* Begin PBXFileReference section */ + 460367F42981A8E30081CCA3 /* tvOSSettingsBad.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = tvOSSettingsBad.json; sourceTree = ""; }; 5AF0E8457CCFC077382BF449 /* AutoScreenReportingTest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AutoScreenReportingTest.swift; sourceTree = ""; }; 5AF0EDFEDC0EE79A0A0EB713 /* SEGScreenReporting.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SEGScreenReporting.h; sourceTree = ""; }; 63E090D722DD49C300DEC7EC /* UIViewController+SegScreenTest.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "UIViewController+SegScreenTest.h"; sourceTree = ""; }; @@ -359,6 +362,7 @@ EADEB8E41DECD335005322DA /* TestUtils.swift */, A3BECDCE24BCE1E3009E2BD3 /* ObjcUtils.h */, A3BECDCF24BCE1E3009E2BD3 /* ObjcUtils.m */, + 460367F42981A8E30081CCA3 /* tvOSSettingsBad.json */, ); path = Utils; sourceTree = ""; @@ -501,6 +505,7 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( + 460367F62981AAA50081CCA3 /* tvOSSettingsBad.json in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -508,6 +513,7 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( + 460367F52981A8E30081CCA3 /* tvOSSettingsBad.json in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/Segment/Internal/SEGUserDefaultsStorage.m b/Segment/Internal/SEGUserDefaultsStorage.m index 444b4faa0..8fe58683e 100644 --- a/Segment/Internal/SEGUserDefaultsStorage.m +++ b/Segment/Internal/SEGUserDefaultsStorage.m @@ -94,10 +94,10 @@ - (void)setDictionary:(NSDictionary *)dictionary forKey:(NSString *)key { if (!self.crypto) { key = [self namespacedKey:key]; - [self setObject:dictionary forKey:key]; + [self setObject:[dictionary plistCompatible] forKey:key]; return; } - [self setPlist:dictionary forKey:key]; + [self setPlist:[dictionary plistCompatible] forKey:key]; } - (NSArray *)arrayForKey:(NSString *)key @@ -113,10 +113,10 @@ - (void)setArray:(NSArray *)array forKey:(NSString *)key { if (!self.crypto) { key = [self namespacedKey:key]; - [self setObject:array forKey:key]; + [self setObject:[array plistCompatible] forKey:key]; return; } - [self setPlist:array forKey:key]; + [self setPlist:[array plistCompatible] forKey:key]; } - (NSString *)stringForKey:(NSString *)key diff --git a/Segment/Internal/SEGUtils.h b/Segment/Internal/SEGUtils.h index 4c6c1f62a..ac8a14609 100644 --- a/Segment/Internal/SEGUtils.h +++ b/Segment/Internal/SEGUtils.h @@ -82,5 +82,14 @@ NSString *SEGEventNameForScreenTitle(NSString *title); @interface NSArray(SerializableDeepCopy) @end +@interface NSDictionary(PListJSON) +- (NSDictionary *)plistCompatible; +@end + +@interface NSArray(PListJSON) +- (NSDictionary *)plistCompatible; +@end + + NS_ASSUME_NONNULL_END diff --git a/Segment/Internal/SEGUtils.m b/Segment/Internal/SEGUtils.m index d0ea32d08..8781a1f85 100644 --- a/Segment/Internal/SEGUtils.m +++ b/Segment/Internal/SEGUtils.m @@ -653,3 +653,51 @@ - (NSMutableArray *)serializableMutableDeepCopy { } @end + + +@implementation NSDictionary(PListJSON) + +- (NSDictionary *)plistCompatible { + const NSMutableDictionary *replaced = [NSMutableDictionary new]; + const id null = [NSNull null]; + + for(NSString *key in self) { + const id object = [self objectForKey:key]; + if(object == null) { + continue; + } else if ([object isKindOfClass:[NSDictionary class]]) { + [replaced setObject:[object plistCompatible] forKey:key]; + } else if ([object isKindOfClass:[NSArray class]]) { + [replaced setObject:[object plistCompatible] forKey:key]; + } else { + [replaced setObject:object forKey:key]; + } + } + return [NSDictionary dictionaryWithDictionary:(NSDictionary*)replaced]; +} + +@end + +@implementation NSArray(PListJSON) + +- (NSArray *)plistCompatible { + const NSMutableArray *replaced = [NSMutableArray new]; + const id null = [NSNull null]; + + for (int i=0; i<[self count]; i++) { + const id object = [self objectAtIndex:i]; + + if ([object isKindOfClass:[NSDictionary class]]) { + [replaced setObject:[object plistCompatible] atIndexedSubscript:i]; + } else if ([object isKindOfClass:[NSArray class]]) { + [replaced setObject:[object plistCompatible] atIndexedSubscript:i]; + } else if (object == null) { + continue; + } else { + [replaced setObject:object atIndexedSubscript:i]; + } + } + return [NSArray arrayWithArray:(NSArray*)replaced]; +} + +@end diff --git a/SegmentTests/AnalyticsTests.swift b/SegmentTests/AnalyticsTests.swift index af5dd2f8d..35217b945 100644 --- a/SegmentTests/AnalyticsTests.swift +++ b/SegmentTests/AnalyticsTests.swift @@ -372,4 +372,30 @@ class AnalyticsTests: XCTestCase { XCTAssertNotNil(data) } } + + #if os(tvOS) + func testTVOSSettingsBad() { + // this test is expected to crash if it fails. + var initialized = false + NotificationCenter.default.addObserver(forName: NSNotification.Name(rawValue: SEGAnalyticsIntegrationDidStart), object: nil, queue: nil) { (notification) in + let key = notification.object as? String + if (key == "Segment.io") { + initialized = true + } + } + + var config = AnalyticsConfiguration(writeKey: "1234") + let url = Bundle(for: AnalyticsTests.self).url(forResource: "tvOSSettingsBad.json", withExtension: nil) + let data = try! Data(contentsOf: url!) + let settings = try! JSONSerialization.jsonObject(with: data) as! NSDictionary + + let analytics = Analytics(configuration: config) + + analytics.test_integrationsManager()?.test_setCachedSettings(settings: settings as NSDictionary) + + while (!initialized) { // wait for integrations to get setup + RunLoop.main.run(until: Date.distantPast) + } + } + #endif } diff --git a/SegmentTests/Utils/tvOSSettingsBad.json b/SegmentTests/Utils/tvOSSettingsBad.json new file mode 100644 index 000000000..05914171a --- /dev/null +++ b/SegmentTests/Utils/tvOSSettingsBad.json @@ -0,0 +1,49 @@ +{ + "integrations": { + "Segment.io": { + "apiKey": "1234", + "unbundledIntegrations": [], + "addBundledMetadata": true + } + }, + "edgeFunction": {}, + "analyticsNextEnabled": false, + "middlewareSettings": { + "routingRules": [ + { + "transformers": [ + [ + { + "type": "drop" + } + ], + [ + { + "type": "allow_properties", + "config": { + "allow": { + "traits": null, + "context": null, + "_metadata": null, + "integrations": null, + "properties.content": [ + "asset_id", + "full_episode", + "load_type", + "program", + "title", + "video_tms_id" + ] + } + } + } + ] + ] + } + ] + }, + "enabledMiddleware": {}, + "metrics": { + "sampleRate": 0.1 + }, + }