From 8337ad32728e14abddd15d350f6e4cf8346403a6 Mon Sep 17 00:00:00 2001 From: Thilo Molitor Date: Wed, 28 Aug 2024 03:47:12 +0200 Subject: [PATCH] Swizzle object access methods of NSUserDefaults for auto (de)serialisation This category will automatically (de)serialize all NSObjects conforming to NSSecureCoding protocol when accessing any NSUserDefaults db. This works automagically and has only one small downside: NSData objects won't be saved as NSData objects but serialized into an NSData object. Since we control both, setters and getters, this doesn't matter (I will only matter if setting NSData values in the underlying property list using an external tool). --- Monal/Classes/HelperTools.m | 62 +++++++++++++++++++++++++++++++++++++ 1 file changed, 62 insertions(+) diff --git a/Monal/Classes/HelperTools.m b/Monal/Classes/HelperTools.m index ef0944761..dd26c4c85 100644 --- a/Monal/Classes/HelperTools.m +++ b/Monal/Classes/HelperTools.m @@ -75,6 +75,11 @@ @interface MLDelayableTimer() -(void) invalidate; @end +@interface NSUserDefaults (SerializeNSObject) +-(id) swizzled_objectForKey:(NSString*) defaultName; +-(void) swizzled_setObject:(id) value forKey:(NSString*) defaultName; +@end + static char* _crashBundleName = "UnifiedReport"; static NSString* _processID; static DDFileLogger* _fileLogger = nil; @@ -291,6 +296,62 @@ -(id) initWithObj:(id) obj } @end +@implementation NSUserDefaults (SerializeNSObject) +-(id) swizzled_objectForKey:(NSString*) defaultName +{ + //this will call the original not this one, because of swizzling! + id data = [self swizzled_objectForKey:defaultName]; + //always unserialize this: every real NSData should be serialized to NSData (e.g. an NSData containing a serialized NSData) + //and therefore any exception thrown by unserialize of not serialized data should never happen as it is an implementation error in Monal + if([data isKindOfClass:[NSData class]]) + { + @try { + return [HelperTools unserializeData:data]; + } @catch (NSException* exception) { + NSMutableDictionary* userInfo = [NSMutableDictionary dictionaryWithDictionary:nilDefault(exception.userInfo, @{})]; + [userInfo addEntriesFromDictionary:@{@"userDefaultsName":defaultName}]; + @throw [NSException exceptionWithName:exception.name reason:exception.reason userInfo:userInfo]; + } + } + return data; +} + +-(void) swizzled_setObject:(id) value forKey:(NSString*) defaultName +{ + id toSave = value; + //these are the default datatypes/class clusters already handled by NSUserDefaults + //(NSData gets a special handling by us and is therefore not listed here) + if( + [value isKindOfClass:[NSString class]] || + [value isKindOfClass:[NSNumber class]] || + [value isKindOfClass:[NSURL class]] || + [value isKindOfClass:[NSDictionary class]] || + [value isKindOfClass:[NSMutableDictionary class]] || + [value isKindOfClass:[NSArray class]] || + [value isKindOfClass:[NSMutableArray class]] + ) + ; //do nothing, already handled by original NSUserDefaults method + //every NSData should be double serialized (see swizzled_objectForKey: above for a detailed explanation) + //everything else will just be (single) serialized to NSData + else + toSave = [HelperTools serializeObject:value]; + return [self swizzled_setObject:toSave forKey:defaultName]; +} + +//see https://stackoverflow.com/a/13326633 and https://fek.io/blog/method-swizzling-in-obj-c-and-swift/ ++(void) load +{ + if(self == NSUserDefaults.self) + { + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + swizzle([self class], @selector(objectForKey:), @selector(swizzled_objectForKey:)); + swizzle([self class], @selector(setObject:forKey:), @selector(swizzled_setObject:forKey:)); + }); + } +} +@end + @implementation HelperTools +(void) initialize @@ -817,6 +878,7 @@ +(id) unserializeData:(NSData*) data [NSURL class], [OmemoState class], [MLContactSoftwareVersionInfo class], + [Quicksy_Country class], ]] fromData:data error:&error]; if(error) @throw [NSException exceptionWithName:@"NSError" reason:[NSString stringWithFormat:@"%@", error] userInfo:@{@"error": error}];