373 lines
13 KiB
Objective-C
373 lines
13 KiB
Objective-C
// Copyright 2019 Google
|
|
//
|
|
// 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 "Crashlytics/Crashlytics/FIRCLSUserDefaults/FIRCLSUserDefaults.h"
|
|
|
|
#import "Crashlytics/Crashlytics/Components/FIRCLSApplication.h"
|
|
#import "Crashlytics/Crashlytics/Helpers/FIRCLSLogger.h"
|
|
|
|
#define CLS_USER_DEFAULTS_SERIAL_DISPATCH_QUEUE "com.crashlytics.CLSUserDefaults.access"
|
|
#define CLS_USER_DEFAULTS_SYNC_QUEUE "com.crashlytics.CLSUserDefaults.io"
|
|
|
|
#define CLS_TARGET_CAN_WRITE_TO_DISK !TARGET_OS_TV
|
|
|
|
// These values are required to stay the same between versions of the SDK so
|
|
// that when end users upgrade, their crashlytics data is still saved on disk.
|
|
#if !CLS_TARGET_CAN_WRITE_TO_DISK
|
|
static NSString *const FIRCLSNSUserDefaultsDataDictionaryKey =
|
|
@"com.crashlytics.CLSUserDefaults.user-default-key.data-dictionary";
|
|
#endif
|
|
|
|
NSString *const FIRCLSUserDefaultsPathComponent = @"CLSUserDefaults";
|
|
|
|
/**
|
|
* This class is an isolated re-implementation of UserDefaults which isolates our storage
|
|
* from that of our customers. This solves a number of issues we have seen in production, firstly
|
|
* that customers often delete or clear UserDefaults, unintentionally deleting our data.
|
|
* Further, we have seen thread safety issues in production with UserDefaults, as well as a number
|
|
* of bugs related to accessing UserDefaults before the device has been unlocked due to the
|
|
* FileProtection of UserDefaults.
|
|
*/
|
|
@interface FIRCLSUserDefaults ()
|
|
@property(nonatomic, readwrite) BOOL synchronizeWroteToDisk;
|
|
#if CLS_TARGET_CAN_WRITE_TO_DISK
|
|
@property(nonatomic, copy, readonly) NSURL *directoryURL;
|
|
@property(nonatomic, copy, readonly) NSURL *fileURL;
|
|
#endif
|
|
@property(nonatomic, copy, readonly)
|
|
NSDictionary *persistedDataDictionary; // May only be safely accessed on the DictionaryQueue
|
|
@property(nonatomic, copy, readonly)
|
|
NSMutableDictionary *dataDictionary; // May only be safely accessed on the DictionaryQueue
|
|
@property(nonatomic, readonly) dispatch_queue_t
|
|
serialDictionaryQueue; // The queue on which all access to the dataDictionary occurs.
|
|
@property(nonatomic, readonly)
|
|
dispatch_queue_t synchronizationQueue; // The queue on which all disk access occurs.
|
|
|
|
@end
|
|
|
|
@implementation FIRCLSUserDefaults
|
|
|
|
#pragma mark - singleton
|
|
|
|
+ (instancetype)standardUserDefaults {
|
|
static FIRCLSUserDefaults *standardUserDefaults = nil;
|
|
static dispatch_once_t onceToken;
|
|
|
|
dispatch_once(&onceToken, ^{
|
|
standardUserDefaults = [[super allocWithZone:NULL] init];
|
|
});
|
|
|
|
return standardUserDefaults;
|
|
}
|
|
|
|
- (id)copyWithZone:(NSZone *)zone {
|
|
return self;
|
|
}
|
|
|
|
- (id)init {
|
|
if (self = [super init]) {
|
|
_serialDictionaryQueue =
|
|
dispatch_queue_create(CLS_USER_DEFAULTS_SERIAL_DISPATCH_QUEUE, DISPATCH_QUEUE_SERIAL);
|
|
_synchronizationQueue =
|
|
dispatch_queue_create(CLS_USER_DEFAULTS_SYNC_QUEUE, DISPATCH_QUEUE_SERIAL);
|
|
|
|
dispatch_sync(self.serialDictionaryQueue, ^{
|
|
#if CLS_TARGET_CAN_WRITE_TO_DISK
|
|
self->_directoryURL = [self generateDirectoryURL];
|
|
self->_fileURL = [[self->_directoryURL
|
|
URLByAppendingPathComponent:FIRCLSUserDefaultsPathComponent
|
|
isDirectory:NO] URLByAppendingPathExtension:@"plist"];
|
|
#endif
|
|
self->_persistedDataDictionary = [self loadDefaults];
|
|
if (!self->_persistedDataDictionary) {
|
|
self->_persistedDataDictionary = [NSDictionary dictionary];
|
|
}
|
|
self->_dataDictionary = [self->_persistedDataDictionary mutableCopy];
|
|
});
|
|
}
|
|
return self;
|
|
}
|
|
|
|
- (NSURL *)generateDirectoryURL {
|
|
NSURL *directoryBaseURL =
|
|
[[[NSFileManager defaultManager] URLsForDirectory:NSApplicationSupportDirectory
|
|
inDomains:NSUserDomainMask] lastObject];
|
|
NSString *hostAppBundleIdentifier = [self getEscapedAppBundleIdentifier];
|
|
return [self generateDirectoryURLForBaseURL:directoryBaseURL
|
|
hostAppBundleIdentifier:hostAppBundleIdentifier];
|
|
}
|
|
|
|
- (NSURL *)generateDirectoryURLForBaseURL:(NSURL *)directoryBaseURL
|
|
hostAppBundleIdentifier:(NSString *)hostAppBundleIdentifier {
|
|
NSURL *directoryURL = directoryBaseURL;
|
|
// On iOS NSApplicationSupportDirectory is contained in the app's bundle. On OSX, it is not (it is
|
|
// ~/Library/Application Support/). On OSX we create a directory
|
|
// ~/Library/Application Support/<app-identifier>/com.crashlytics/ for storing files.
|
|
// Mac App Store review process requires files to be written to
|
|
// ~/Library/Application Support/<app-identifier>/,
|
|
// so ~/Library/Application Support/com.crashlytics/<app-identifier>/ cannot be used.
|
|
#if !TARGET_OS_SIMULATOR && !TARGET_OS_EMBEDDED
|
|
if (hostAppBundleIdentifier) {
|
|
directoryURL = [directoryURL URLByAppendingPathComponent:hostAppBundleIdentifier];
|
|
}
|
|
#endif
|
|
directoryURL = [directoryURL URLByAppendingPathComponent:@"com.crashlytics"];
|
|
return directoryURL;
|
|
}
|
|
|
|
- (NSString *)getEscapedAppBundleIdentifier {
|
|
return FIRCLSApplicationGetBundleIdentifier();
|
|
}
|
|
|
|
#pragma mark - fetch object
|
|
|
|
- (id)objectForKey:(NSString *)key {
|
|
__block id result;
|
|
|
|
dispatch_sync(self.serialDictionaryQueue, ^{
|
|
result = [self->_dataDictionary objectForKey:key];
|
|
});
|
|
|
|
return result;
|
|
}
|
|
|
|
- (NSString *)stringForKey:(NSString *)key {
|
|
id result = [self objectForKey:key];
|
|
|
|
if (result != nil && [result isKindOfClass:[NSString class]]) {
|
|
return (NSString *)result;
|
|
} else {
|
|
return nil;
|
|
}
|
|
}
|
|
|
|
- (BOOL)boolForKey:(NSString *)key {
|
|
id result = [self objectForKey:key];
|
|
if (result != nil && [result isKindOfClass:[NSNumber class]]) {
|
|
return [(NSNumber *)result boolValue];
|
|
} else {
|
|
return NO;
|
|
}
|
|
}
|
|
|
|
// Defaults to 0
|
|
- (NSInteger)integerForKey:(NSString *)key {
|
|
id result = [self objectForKey:key];
|
|
if (result && [result isKindOfClass:[NSNumber class]]) {
|
|
return [(NSNumber *)result integerValue];
|
|
} else {
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
#pragma mark - set object
|
|
|
|
- (void)setObject:(id)object forKey:(NSString *)key {
|
|
dispatch_sync(self.serialDictionaryQueue, ^{
|
|
[self->_dataDictionary setValue:object forKey:key];
|
|
});
|
|
}
|
|
|
|
- (void)setString:(NSString *)string forKey:(NSString *)key {
|
|
[self setObject:string forKey:key];
|
|
}
|
|
|
|
- (void)setBool:(BOOL)boolean forKey:(NSString *)key {
|
|
[self setObject:[NSNumber numberWithBool:boolean] forKey:key];
|
|
}
|
|
|
|
- (void)setInteger:(NSInteger)integer forKey:(NSString *)key {
|
|
[self setObject:[NSNumber numberWithInteger:integer] forKey:key];
|
|
}
|
|
|
|
#pragma mark - removing objects
|
|
|
|
- (void)removeObjectForKey:(NSString *)key {
|
|
dispatch_sync(self.serialDictionaryQueue, ^{
|
|
[self->_dataDictionary removeObjectForKey:key];
|
|
});
|
|
}
|
|
|
|
- (void)removeAllObjects {
|
|
dispatch_sync(self.serialDictionaryQueue, ^{
|
|
[self->_dataDictionary removeAllObjects];
|
|
});
|
|
}
|
|
|
|
#pragma mark - dictionary representation
|
|
|
|
- (NSDictionary *)dictionaryRepresentation {
|
|
__block NSDictionary *result;
|
|
|
|
dispatch_sync(self.serialDictionaryQueue, ^{
|
|
result = [self->_dataDictionary copy];
|
|
});
|
|
|
|
return result;
|
|
}
|
|
|
|
#pragma mark - synchronization
|
|
|
|
- (void)synchronize {
|
|
__block BOOL dirty = NO;
|
|
|
|
// only write to the disk if the dictionaries have changed
|
|
dispatch_sync(self.serialDictionaryQueue, ^{
|
|
dirty = ![self->_persistedDataDictionary isEqualToDictionary:self->_dataDictionary];
|
|
});
|
|
|
|
_synchronizeWroteToDisk = dirty;
|
|
if (!dirty) {
|
|
return;
|
|
}
|
|
|
|
NSDictionary *state = [self dictionaryRepresentation];
|
|
dispatch_sync(self.synchronizationQueue, ^{
|
|
#if CLS_TARGET_CAN_WRITE_TO_DISK
|
|
BOOL isDirectory = NO;
|
|
BOOL pathExists = [[NSFileManager defaultManager] fileExistsAtPath:[self->_directoryURL path]
|
|
isDirectory:&isDirectory];
|
|
|
|
if (!pathExists) {
|
|
NSError *error;
|
|
if (![[NSFileManager defaultManager] createDirectoryAtURL:self->_directoryURL
|
|
withIntermediateDirectories:YES
|
|
attributes:nil
|
|
error:&error]) {
|
|
FIRCLSErrorLog(@"Failed to create directory with error: %@", error);
|
|
}
|
|
}
|
|
|
|
if (![state writeToURL:self->_fileURL atomically:YES]) {
|
|
FIRCLSErrorLog(@"Unable to open file for writing at path %@", [self->_fileURL path]);
|
|
} else {
|
|
#if TARGET_OS_IOS
|
|
// We disable NSFileProtection on our file in order to allow us to access it even if the
|
|
// device is locked.
|
|
NSError *error;
|
|
if (![[NSFileManager defaultManager]
|
|
setAttributes:@{NSFileProtectionKey : NSFileProtectionNone}
|
|
ofItemAtPath:[self->_fileURL path]
|
|
error:&error]) {
|
|
FIRCLSErrorLog(@"Error setting NSFileProtection: %@", error);
|
|
}
|
|
#endif
|
|
}
|
|
#else
|
|
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
|
|
[defaults setObject:state forKey:FIRCLSNSUserDefaultsDataDictionaryKey];
|
|
[defaults synchronize];
|
|
#endif
|
|
});
|
|
|
|
dispatch_sync(self.serialDictionaryQueue, ^{
|
|
self->_persistedDataDictionary = [self->_dataDictionary copy];
|
|
});
|
|
}
|
|
|
|
- (NSDictionary *)loadDefaults {
|
|
__block NSDictionary *state = nil;
|
|
dispatch_sync(self.synchronizationQueue, ^{
|
|
#if CLS_TARGET_CAN_WRITE_TO_DISK
|
|
BOOL isDirectory = NO;
|
|
BOOL fileExists = [[NSFileManager defaultManager] fileExistsAtPath:[self->_fileURL path]
|
|
isDirectory:&isDirectory];
|
|
|
|
if (fileExists && !isDirectory) {
|
|
state = [NSDictionary dictionaryWithContentsOfURL:self->_fileURL];
|
|
if (nil == state) {
|
|
FIRCLSErrorLog(@"Failed to read existing UserDefaults file");
|
|
}
|
|
} else if (!fileExists) {
|
|
// No file found. This is expected on first launch.
|
|
} else if (fileExists && isDirectory) {
|
|
FIRCLSErrorLog(@"Found directory where file expected. Removing conflicting directory");
|
|
|
|
NSError *error;
|
|
if (![[NSFileManager defaultManager] removeItemAtURL:self->_fileURL error:&error]) {
|
|
FIRCLSErrorLog(@"Error removing conflicting directory: %@", error);
|
|
}
|
|
}
|
|
#else
|
|
state = [[NSUserDefaults standardUserDefaults] dictionaryForKey:FIRCLSNSUserDefaultsDataDictionaryKey];
|
|
#endif
|
|
});
|
|
return state;
|
|
}
|
|
|
|
#pragma mark - migration
|
|
|
|
// This method migrates all keys specified from UserDefaults to FIRCLSUserDefaults
|
|
// To do so, we copy all known key-value pairs into FIRCLSUserDefaults, synchronize it, then
|
|
// remove the keys from UserDefaults and synchronize it.
|
|
- (void)migrateFromNSUserDefaults:(NSArray *)keysToMigrate {
|
|
BOOL didFindKeys = NO;
|
|
|
|
// First, copy all of the keysToMigrate which are stored UserDefaults
|
|
for (NSString *key in keysToMigrate) {
|
|
id oldValue = [[NSUserDefaults standardUserDefaults] objectForKey:(NSString *)key];
|
|
if (nil != oldValue) {
|
|
didFindKeys = YES;
|
|
[self setObject:oldValue forKey:key];
|
|
}
|
|
}
|
|
|
|
if (didFindKeys) {
|
|
// First synchronize FIRCLSUserDefaults such that all keysToMigrate in UserDefaults are stored
|
|
// in FIRCLSUserDefaults. At this point, data is duplicated.
|
|
[[FIRCLSUserDefaults standardUserDefaults] synchronize];
|
|
|
|
for (NSString *key in keysToMigrate) {
|
|
[[NSUserDefaults standardUserDefaults] removeObjectForKey:(NSString *)key];
|
|
}
|
|
|
|
// This should be our last interaction with UserDefaults. All data is migrated into
|
|
// FIRCLSUserDefaults
|
|
[[NSUserDefaults standardUserDefaults] synchronize];
|
|
}
|
|
}
|
|
|
|
// This method first queries FIRCLSUserDefaults to see if the key exist, and upon failure,
|
|
// searches for the key in UserDefaults, and migrates it if found.
|
|
- (id)objectForKeyByMigratingFromNSUserDefaults:(NSString *)keyToMigrateOrNil {
|
|
if (!keyToMigrateOrNil) {
|
|
return nil;
|
|
}
|
|
|
|
id clsUserDefaultsValue = [self objectForKey:keyToMigrateOrNil];
|
|
if (clsUserDefaultsValue != nil) {
|
|
return clsUserDefaultsValue; // if the value exists in FIRCLSUserDefaults, return it.
|
|
}
|
|
|
|
id oldNSUserDefaultsValue =
|
|
[[NSUserDefaults standardUserDefaults] objectForKey:keyToMigrateOrNil];
|
|
if (!oldNSUserDefaultsValue) {
|
|
return nil; // if the value also does not exist in UserDefaults, return nil.
|
|
}
|
|
|
|
// Otherwise, the key exists in UserDefaults. Migrate it to FIRCLSUserDefaults
|
|
// and then return the associated value.
|
|
|
|
// First store it in FIRCLSUserDefaults so in the event of a crash, data is not lost.
|
|
[self setObject:oldNSUserDefaultsValue forKey:keyToMigrateOrNil];
|
|
[[FIRCLSUserDefaults standardUserDefaults] synchronize];
|
|
|
|
[[NSUserDefaults standardUserDefaults] removeObjectForKey:keyToMigrateOrNil];
|
|
[[NSUserDefaults standardUserDefaults] synchronize];
|
|
|
|
return oldNSUserDefaultsValue;
|
|
}
|
|
|
|
@end
|