AutorÃa | Ultima modificación | Ver Log |
// 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