From b7d7d7644f297131cc085137216cb19b37df7395 Mon Sep 17 00:00:00 2001 From: Nickolas Dimitrakas Date: Thu, 12 Mar 2026 16:59:20 -0400 Subject: [PATCH 1/2] fix: correct NSKeyedUnarchiver class mappings for MPUploadSettings backward compat PR #469 used NSKeyedArchiver setClassName:forClass: (the encode direction) which has no effect on decoding. This replaces it with NSKeyedUnarchiver setClass:forClassName: (the decode direction) so persisted upload settings encoded under the old Swift module name (mParticle_Apple_SDK_NoLocation.MPUploadSettings) can be decoded after upgrading to the Obj-C module (mParticle_Apple_SDK.MPUploadSettings). Also extends coverage to MPPersistenceController.m fetchUploads, which reads upload settings blobs from the SQLite uploads table but was not covered by the original fix, causing those rows to be silently skipped and never uploaded. Without this fix, users upgrading from an SDK version where MPUploadSettings was a Swift class will lose queued upload data on first launch post-upgrade. Made-with: Cursor --- .../Persistence/MPPersistenceController.m | 2 ++ mParticle-Apple-SDK/Utils/UploadSettingsUtils.h | 1 + mParticle-Apple-SDK/Utils/UploadSettingsUtils.m | 10 ++++++++-- 3 files changed, 11 insertions(+), 2 deletions(-) diff --git a/mParticle-Apple-SDK/Persistence/MPPersistenceController.m b/mParticle-Apple-SDK/Persistence/MPPersistenceController.m index beb307496..22fee1c41 100644 --- a/mParticle-Apple-SDK/Persistence/MPPersistenceController.m +++ b/mParticle-Apple-SDK/Persistence/MPPersistenceController.m @@ -19,6 +19,7 @@ #import "MPApplication.h" #import "MParticleUserNotification.h" #import "MPUserDefaultsConnector.h" +#import "UploadSettingsUtils.h" @import mParticle_Apple_SDK_Swift; // Prototype declaration of the C functions @@ -1245,6 +1246,7 @@ - (MPMessage *)fetchSessionEndMessageInSession:(MPSession *)session { NSData *uploadSettingsData = dataValue(preparedStatement, 8); if (uploadSettingsData) { NSError *error = nil; + [UploadSettingsUtils registerUploadSettingsClassMappings]; MPUploadSettings *uploadSettings = [NSKeyedUnarchiver unarchivedObjectOfClass:[MPUploadSettings class] fromData:uploadSettingsData error:&error]; if (uploadSettings && !error) { diff --git a/mParticle-Apple-SDK/Utils/UploadSettingsUtils.h b/mParticle-Apple-SDK/Utils/UploadSettingsUtils.h index 407a6a119..0addd6b0e 100644 --- a/mParticle-Apple-SDK/Utils/UploadSettingsUtils.h +++ b/mParticle-Apple-SDK/Utils/UploadSettingsUtils.h @@ -8,6 +8,7 @@ NS_ASSUME_NONNULL_BEGIN + (void)setLastUploadSettings:(nullable MPUploadSettings *)lastUploadSettings userDefaults:(MPUserDefaults*)userDefaults; + (nullable MPUploadSettings *)lastUploadSettingsWithUserDefaults:(MPUserDefaults*)userDefaults; ++ (void)registerUploadSettingsClassMappings; @end diff --git a/mParticle-Apple-SDK/Utils/UploadSettingsUtils.m b/mParticle-Apple-SDK/Utils/UploadSettingsUtils.m index 166636ffc..ce3baa6c3 100644 --- a/mParticle-Apple-SDK/Utils/UploadSettingsUtils.m +++ b/mParticle-Apple-SDK/Utils/UploadSettingsUtils.m @@ -3,6 +3,13 @@ @implementation UploadSettingsUtils ++ (void)registerUploadSettingsClassMappings { + [NSKeyedUnarchiver setClass:[MPUploadSettings class] + forClassName:@"mParticle_Apple_SDK.MPUploadSettings"]; + [NSKeyedUnarchiver setClass:[MPUploadSettings class] + forClassName:@"mParticle_Apple_SDK_NoLocation.MPUploadSettings"]; +} + + (void)setLastUploadSettings:(nullable MPUploadSettings *)lastUploadSettings userDefaults:(MPUserDefaults*)userDefaults { if (lastUploadSettings) { @@ -31,8 +38,7 @@ + (nullable MPUploadSettings*)lastUploadSettingsWithUserDefaults:(MPUserDefaults } NSError *error = nil; - [NSKeyedArchiver setClassName:@"mParticle_Apple_SDK.MPUploadSettings" forClass:MPUploadSettings.self]; - [NSKeyedArchiver setClassName:@"mParticle_Apple_SDK_NoLocation.MPUploadSettings" forClass:MPUploadSettings.self]; + [self registerUploadSettingsClassMappings]; MPUploadSettings *settings = [NSKeyedUnarchiver unarchivedObjectOfClass:[MPUploadSettings class] fromData:(NSData *)obj From 89a398f9f597d42684bd153a067bdd09744147b9 Mon Sep 17 00:00:00 2001 From: Nickolas Dimitrakas Date: Fri, 13 Mar 2026 12:59:01 -0400 Subject: [PATCH 2/2] refactor: move legacy class mapping registration to MPUploadSettings +initialize Move NSKeyedUnarchiver class name mappings from UploadSettingsUtils into MPUploadSettings +initialize so they are registered automatically by the Obj-C runtime the first time any decode path evaluates [MPUploadSettings class], with no manual wiring required anywhere. Also adds two unit tests: - testFetchUploads_decodesLegacySwiftModuleClassName: verifies fetchUploads correctly decodes SQLite rows whose upload_settings blob was encoded under the legacy Swift module class name. - testRegisterUploadSettingsClassMappings_decodesAllModuleNames: verifies both legacy module names (mParticle_Apple_SDK and mParticle_Apple_SDK_NoLocation) can be decoded into MPUploadSettings after the mapping is registered. Made-with: Cursor --- .../ObjCTests/MPPersistenceControllerTests.m | 84 +++++++++++++++++++ .../Persistence/MPPersistenceController.m | 2 - mParticle-Apple-SDK/Utils/MPUploadSettings.m | 9 ++ .../Utils/UploadSettingsUtils.h | 1 - .../Utils/UploadSettingsUtils.m | 9 -- 5 files changed, 93 insertions(+), 12 deletions(-) diff --git a/UnitTests/ObjCTests/MPPersistenceControllerTests.m b/UnitTests/ObjCTests/MPPersistenceControllerTests.m index 2cdb0687b..21709dea0 100644 --- a/UnitTests/ObjCTests/MPPersistenceControllerTests.m +++ b/UnitTests/ObjCTests/MPPersistenceControllerTests.m @@ -17,6 +17,8 @@ #import "MPBaseTestCase.h" #import "MPStateMachine.h" #import "MPKitFilter.h" +#import "UploadSettingsUtils.h" +#import "MPUploadSettings.h" @interface MParticle () @@ -1458,4 +1460,86 @@ - (void)testMaxBytesPerBatch { XCTAssertGreaterThan(maxBytesCrash, maxBytesNormal, @"Crash batch should allow more bytes than normal batch"); } +// Simulates a row written by the old Swift module (mParticle_Apple_SDK_NoLocation) +// by manually encoding MPUploadSettings under the legacy class name and verifying +// fetchUploads can still decode and return it after the ObjC migration. +- (void)testFetchUploads_decodesLegacySwiftModuleClassName { + MPSession *session = [[MPSession alloc] initWithStartTime:[[NSDate date] timeIntervalSince1970] + userId:[MPPersistenceController_PRIVATE mpId]]; + NSDictionary *uploadDictionary = @{kMPOptOutKey: @NO, + kMPSessionTimeoutKey: @120, + kMPUploadIntervalKey: @10, + kMPLifeTimeValueKey: @0, + kMPMessagesKey: @[], + kMPMessageIdKey: [[NSUUID UUID] UUIDString]}; + + MPUploadSettings *settings = [MPUploadSettings currentUploadSettingsWithStateMachine:[MParticle sharedInstance].stateMachine + networkOptions:[MParticle sharedInstance].networkOptions]; + + // Encode using the legacy Swift module class name to simulate pre-migration persisted data + [NSKeyedArchiver setClassName:@"mParticle_Apple_SDK_NoLocation.MPUploadSettings" + forClass:[MPUploadSettings class]]; + NSData *legacyBlob = [NSKeyedArchiver archivedDataWithRootObject:settings + requiringSecureCoding:YES + error:nil]; + // Reset so other tests are not affected + [NSKeyedArchiver setClassName:nil forClass:[MPUploadSettings class]]; + + MPUpload *upload = [[MPUpload alloc] initWithSessionId:@(session.sessionId) + uploadDictionary:uploadDictionary + dataPlanId:nil + dataPlanVersion:nil + uploadSettings:settings]; + + MPPersistenceController_PRIVATE *persistence = [MParticle sharedInstance].persistenceController; + [persistence saveUpload:upload]; + + // Patch the saved row's upload_settings blob with the legacy-encoded version + // to simulate the state of a DB row written before the Swift→ObjC migration + NSString *dbPath = [persistence valueForKey:@"databasePath"]; + sqlite3 *db; + sqlite3_open([dbPath UTF8String], &db); + sqlite3_stmt *stmt; + sqlite3_prepare_v2(db, "UPDATE uploads SET upload_settings = ? WHERE _id = ?", -1, &stmt, NULL); + sqlite3_bind_blob(stmt, 1, legacyBlob.bytes, (int)legacyBlob.length, SQLITE_TRANSIENT); + sqlite3_bind_int64(stmt, 2, upload.uploadId); + sqlite3_step(stmt); + sqlite3_finalize(stmt); + sqlite3_close(db); + + NSArray *fetched = [persistence fetchUploads]; + XCTAssertEqual(fetched.count, 1, @"Upload with legacy class name should be decoded and returned by fetchUploads"); + XCTAssertEqualObjects(fetched.firstObject.uploadSettings.apiKey, settings.apiKey, + @"Decoded upload settings apiKey should match original"); +} + +// Verifies registerUploadSettingsClassMappings enables decoding blobs encoded +// under either the old Swift module name or the current ObjC module name. +- (void)testRegisterUploadSettingsClassMappings_decodesAllModuleNames { + MPUploadSettings *original = [MPUploadSettings currentUploadSettingsWithStateMachine:[MParticle sharedInstance].stateMachine + networkOptions:[MParticle sharedInstance].networkOptions]; + + NSArray *legacyNames = @[ + @"mParticle_Apple_SDK_NoLocation.MPUploadSettings", + @"mParticle_Apple_SDK.MPUploadSettings" + ]; + + for (NSString *legacyName in legacyNames) { + [NSKeyedArchiver setClassName:legacyName forClass:[MPUploadSettings class]]; + NSData *blob = [NSKeyedArchiver archivedDataWithRootObject:original + requiringSecureCoding:YES + error:nil]; + [NSKeyedArchiver setClassName:nil forClass:[MPUploadSettings class]]; + + NSError *error = nil; + MPUploadSettings *decoded = [NSKeyedUnarchiver unarchivedObjectOfClass:[MPUploadSettings class] + fromData:blob + error:&error]; + XCTAssertNil(error, @"Should decode without error for class name: %@", legacyName); + XCTAssertNotNil(decoded, @"Decoded settings should not be nil for class name: %@", legacyName); + XCTAssertEqualObjects(decoded.apiKey, original.apiKey, + @"apiKey should survive round-trip for class name: %@", legacyName); + } +} + @end diff --git a/mParticle-Apple-SDK/Persistence/MPPersistenceController.m b/mParticle-Apple-SDK/Persistence/MPPersistenceController.m index 22fee1c41..beb307496 100644 --- a/mParticle-Apple-SDK/Persistence/MPPersistenceController.m +++ b/mParticle-Apple-SDK/Persistence/MPPersistenceController.m @@ -19,7 +19,6 @@ #import "MPApplication.h" #import "MParticleUserNotification.h" #import "MPUserDefaultsConnector.h" -#import "UploadSettingsUtils.h" @import mParticle_Apple_SDK_Swift; // Prototype declaration of the C functions @@ -1246,7 +1245,6 @@ - (MPMessage *)fetchSessionEndMessageInSession:(MPSession *)session { NSData *uploadSettingsData = dataValue(preparedStatement, 8); if (uploadSettingsData) { NSError *error = nil; - [UploadSettingsUtils registerUploadSettingsClassMappings]; MPUploadSettings *uploadSettings = [NSKeyedUnarchiver unarchivedObjectOfClass:[MPUploadSettings class] fromData:uploadSettingsData error:&error]; if (uploadSettings && !error) { diff --git a/mParticle-Apple-SDK/Utils/MPUploadSettings.m b/mParticle-Apple-SDK/Utils/MPUploadSettings.m index bd1188df6..50abc5374 100644 --- a/mParticle-Apple-SDK/Utils/MPUploadSettings.m +++ b/mParticle-Apple-SDK/Utils/MPUploadSettings.m @@ -14,6 +14,15 @@ @implementation MPUploadSettings ++ (void)initialize { + if (self == [MPUploadSettings class]) { + [NSKeyedUnarchiver setClass:[MPUploadSettings class] + forClassName:@"mParticle_Apple_SDK.MPUploadSettings"]; + [NSKeyedUnarchiver setClass:[MPUploadSettings class] + forClassName:@"mParticle_Apple_SDK_NoLocation.MPUploadSettings"]; + } +} + + (BOOL)supportsSecureCoding { return YES; } diff --git a/mParticle-Apple-SDK/Utils/UploadSettingsUtils.h b/mParticle-Apple-SDK/Utils/UploadSettingsUtils.h index 0addd6b0e..407a6a119 100644 --- a/mParticle-Apple-SDK/Utils/UploadSettingsUtils.h +++ b/mParticle-Apple-SDK/Utils/UploadSettingsUtils.h @@ -8,7 +8,6 @@ NS_ASSUME_NONNULL_BEGIN + (void)setLastUploadSettings:(nullable MPUploadSettings *)lastUploadSettings userDefaults:(MPUserDefaults*)userDefaults; + (nullable MPUploadSettings *)lastUploadSettingsWithUserDefaults:(MPUserDefaults*)userDefaults; -+ (void)registerUploadSettingsClassMappings; @end diff --git a/mParticle-Apple-SDK/Utils/UploadSettingsUtils.m b/mParticle-Apple-SDK/Utils/UploadSettingsUtils.m index ce3baa6c3..422e6ea80 100644 --- a/mParticle-Apple-SDK/Utils/UploadSettingsUtils.m +++ b/mParticle-Apple-SDK/Utils/UploadSettingsUtils.m @@ -3,14 +3,6 @@ @implementation UploadSettingsUtils -+ (void)registerUploadSettingsClassMappings { - [NSKeyedUnarchiver setClass:[MPUploadSettings class] - forClassName:@"mParticle_Apple_SDK.MPUploadSettings"]; - [NSKeyedUnarchiver setClass:[MPUploadSettings class] - forClassName:@"mParticle_Apple_SDK_NoLocation.MPUploadSettings"]; -} - - + (void)setLastUploadSettings:(nullable MPUploadSettings *)lastUploadSettings userDefaults:(MPUserDefaults*)userDefaults { if (lastUploadSettings) { NSError *error = nil; @@ -38,7 +30,6 @@ + (nullable MPUploadSettings*)lastUploadSettingsWithUserDefaults:(MPUserDefaults } NSError *error = nil; - [self registerUploadSettingsClassMappings]; MPUploadSettings *settings = [NSKeyedUnarchiver unarchivedObjectOfClass:[MPUploadSettings class] fromData:(NSData *)obj