diff --git a/Incremental Store/EncryptedStore.h b/Incremental Store/EncryptedStore.h index fd927d9..81ec5a8 100755 --- a/Incremental Store/EncryptedStore.h +++ b/Incremental Store/EncryptedStore.h @@ -19,31 +19,204 @@ extern NSString * const EncryptedStoreErrorDomain; extern NSString * const EncryptedStoreErrorMessageKey; extern NSString * const EncryptedStoreDatabaseLocation; extern NSString * const EncryptedStoreCacheSize; - +extern NSString * const EncryptedStoreFileManagerOption; typedef NS_ENUM(NSInteger, EncryptedStoreError) { EncryptedStoreErrorIncorrectPasscode = 6000, EncryptedStoreErrorMigrationFailed }; +@interface EncryptedStoreFileManagerConfiguration : NSObject +#pragma mark - Initialization +- (instancetype)initWithOptions:(NSDictionary *)options; +#pragma mark - Properties +@property (nonatomic, readwrite) NSFileManager *fileManager; +@property (nonatomic, readwrite) NSBundle *bundle; +@property (nonatomic, readwrite) NSString *databaseName; +@property (nonatomic, readwrite) NSString *databaseExtension; +@property (nonatomic, readonly) NSString *databaseFilename; +@property (nonatomic, readwrite) NSURL *databaseURL; +@end + +@interface EncryptedStoreFileManagerConfiguration (OptionsKeys) ++ (NSString *)optionFileManager; ++ (NSString *)optionBundle; ++ (NSString *)optionDatabaseName; ++ (NSString *)optionDatabaseExtension; ++ (NSString *)optionDatabaseURL; +@end + +@interface EncryptedStoreFileManager : NSObject +#pragma mark - Initialization ++ (instancetype)defaultManager; +- (instancetype)initWithConfiguration:(EncryptedStoreFileManagerConfiguration *)configuration; + +#pragma mark - Setup +- (void)setupDatabaseWithOptions:(NSDictionary *)options error:(NSError * __autoreleasing*)error; + +#pragma mark - Getters +@property (nonatomic, readwrite) EncryptedStoreFileManagerConfiguration *configuration; +@property (nonatomic, readonly) NSURL *databaseURL; +@end + +@interface EncryptedStoreFileManager (FileManagerExtensions) +@property (nonatomic, readonly) NSURL *applicationSupportURL; +- (void)setAttributes:(NSDictionary *)attributes ofItemAtURL:(NSURL *)url error:(NSError * __autoreleasing*)error; +@end + @interface EncryptedStore : NSIncrementalStore +#pragma mark - Initialization + (NSPersistentStoreCoordinator *)makeStoreWithOptions:(NSDictionary *)options managedObjectModel:(NSManagedObjectModel *)objModel; + (NSPersistentStoreCoordinator *)makeStoreWithStructOptions:(EncryptedStoreOptions *) options managedObjectModel:(NSManagedObjectModel *)objModel; + (NSPersistentStoreCoordinator *)makeStore:(NSManagedObjectModel *) objModel passcode:(NSString *) passcode; -+ (NSPersistentStoreCoordinator *)makeStoreWithOptions:(NSDictionary *)options managedObjectModel:(NSManagedObjectModel *)objModel error:(NSError * __autoreleasing*)error; +//+ (NSPersistentStoreCoordinator *)makeStoreWithOptions:(NSDictionary *)options managedObjectModel:(NSManagedObjectModel *)objModel error:(NSError * __autoreleasing*)error; + (NSPersistentStoreCoordinator *)makeStoreWithStructOptions:(EncryptedStoreOptions *) options managedObjectModel:(NSManagedObjectModel *)objModel error:(NSError * __autoreleasing*)error; + (NSPersistentStoreCoordinator *)makeStore:(NSManagedObjectModel *) objModel passcode:(NSString *) passcode error:(NSError * __autoreleasing*)error; +#pragma mark - Passphrase manipulation +#pragma mark - Public + +/** + @discussion Check old passphrase and if success change old passphrase to new passphrase. + + @param oldPassphrase The old passhrase with which database was previously opened. + @param newPassphrase The new passhrase which is desired for database. + @param error Inout error. + @return The status of operation. + */ +- (BOOL)checkAndChangeDatabasePassphrase:(NSString *)oldPassphrase toNewPassphrase:(NSString *)newPassphrase error:(NSError *__autoreleasing*)error; + + +/** + @discussion Check database passphrase. + + @param passphrase The desired passphrase to test for. + @param error Inout error. + @return The status of operation. + */ +- (BOOL)checkDatabasePassphrase:(NSString *)passphrase error:(NSError *__autoreleasing*)error; + +#pragma mark - Internal + +/** + @brief Configure database with passhrase. + + @discussion Configure database with passphrase stored in options dictionary. + + @attention Internal usage. + + @pre (error != NULL) + + @param error Inout error. + @return The status of operation. + */ - (BOOL)configureDatabasePassphrase:(NSError *__autoreleasing*)error; + +/** + @brief Test database connection against simple sql request. + @discussion Test database connection against simple sql request. Success means database open state and correctness of previous passphrase manipulation operation. + + @attention Internal usage. + + @pre (error != NULL) + + @param error Inout error. + @return The status of operation. + */ - (BOOL)checkDatabaseStatusWithError:(NSError *__autoreleasing*)error; + + +/** + @brief + Primitive change passphrase operation. + + @discussion Ignores database state and tries to change database passphrase. + Behaviour is unknown if used before old passphrase validation. + + @attention Internal usage. + + @pre (error != NULL) + + @param passphrase The new passphrase. + @param error Inout error. + @return The status of operation. + */ - (BOOL)changeDatabasePassphrase:(NSString *)passphrase error:(NSError *__autoreleasing*)error; + + +/** + @brief Primitive set passphrase operation. + + @discussion Ignores database state and tries to set database passphrase. + One of first-call functions in database setup. + + @attention Internal usage. + + @pre (error != NULL) + + @param passphrase The desired first passphrase of database. + @param error Inout error. + @return The status of operation. + */ - (BOOL)setDatabasePassphrase:(NSString *)passphrase error:(NSError *__autoreleasing*)error; -// Warning! // This method could close database connection ( look at implementation for details ) + + +/** + @brief Validates database passphrase for correctness. + + @discussion Tries to reopen database on provided passphrase. + Closes database and try to open in on provided passphrase. + + @warning Could close database connection ( look at an implementation for details ). + + @pre (error != NULL) + + @param passphrase The desired passphrase to validate. + @param error Inout error. + @return The status of operation. + */ - (BOOL)validateDatabasePassphrase:(NSString *)passphrase error:(NSError *__autoreleasing*)error; + +/** + @brief Primitive database change passphrase operation. + + @discussion Tries to open database on provided oldPassphrase and in success it tries to change passphrase to new passphrase. + + @attention Internal usage. + + @pre (error != NULL) + + @param oldPassphrase: The old passphrase. + @param newPassphrase: The new passphrase. + @param error: Inout error. + @return The status of operation. + */ - (BOOL)changeDatabasePassphrase:(NSString *)oldPassphrase toNewPassphrase:(NSString *)newPassphrase error:(NSError *__autoreleasing*)error; @end + +@interface EncryptedStore (Initialization) ++ (NSPersistentStoreCoordinator *)makeStoreWithOptions:(NSDictionary *)options managedObjectModel:(NSManagedObjectModel *)objModel error:(NSError * __autoreleasing*)error; ++ (NSPersistentStoreCoordinator *)coordinator:(NSPersistentStoreCoordinator *)coordinator byAddingStoreAtURL:(NSURL *)url configuration:(NSString *)configuration options:(NSDictionary *)options error:(NSError * __autoreleasing*)error; ++ (NSPersistentStoreDescription *)makeDescriptionWithOptions:(NSDictionary *)options configuration:(NSString *)configuration error:(NSError * __autoreleasing*)error API_AVAILABLE(macosx(10.12),ios(10.0),tvos(10.0),watchos(3.0)); +@end + +@interface EncryptedStore (Configuration) +//alias to options. +@property (copy, nonatomic, readonly) NSDictionary *configurationOptions; +@property (strong, nonatomic, readonly) EncryptedStoreFileManager *fileManager; +@end + +@interface EncryptedStore (OptionsKeys) ++ (NSString *)optionType; ++ (NSString *)optionPassphraseKey; ++ (NSString *)optionErrorDomain; ++ (NSString *)optionErrorMessageKey; ++ (NSString *)optionDatabaseLocation; ++ (NSString *)optionCacheSize; ++ (NSString *)optionFileManager; +@end diff --git a/Incremental Store/EncryptedStore.m b/Incremental Store/EncryptedStore.m index d904a2c..b2773ea 100755 --- a/Incremental Store/EncryptedStore.m +++ b/Incremental Store/EncryptedStore.m @@ -21,6 +21,7 @@ NSString * const EncryptedStoreErrorMessageKey = @"EncryptedStoreErrorMessage"; NSString * const EncryptedStoreDatabaseLocation = @"EncryptedStoreDatabaseLocation"; NSString * const EncryptedStoreCacheSize = @"EncryptedStoreCacheSize"; +NSString * const EncryptedStoreFileManagerOption = @"EncryptedStoreFileManagerOption"; static void dbsqliteRegExp(sqlite3_context *context, int argc, const char **argv); static void dbsqliteStripCase(sqlite3_context *context, int argc, const char **argv); @@ -78,76 +79,209 @@ @interface NSEntityDescription (CMDTypeHash) @end -@implementation EncryptedStore { - - // database resources - sqlite3 *database; - - // cache money - NSMutableDictionary *objectIDCache; - NSMutableDictionary *nodeCache; - NSMutableDictionary *objectCountCache; - NSMutableDictionary *entityTypeCache; - +@implementation EncryptedStoreFileManagerConfiguration +- (instancetype)initWithOptions:(NSDictionary *)options { + if (self = [super init]) { + self.fileManager = options[self.class.optionFileManager] ?: [NSFileManager defaultManager]; + self.bundle = options[self.class.optionBundle] ?: NSBundle.mainBundle; + self.databaseName = options[self.class.optionDatabaseName] ?: [self databaseNameInBundle:self.bundle]; + self.databaseExtension = options[self.class.optionDatabaseExtension] ?: @"sqlite"; + self.databaseURL = options[self.class.optionDatabaseURL]; + } + return self; } -+ (NSPersistentStoreCoordinator *)makeStoreWithOptions:(NSDictionary *)options managedObjectModel:(NSManagedObjectModel *)objModel -{ - NSError * error; - return [self makeStoreWithOptions:options managedObjectModel:objModel error:&error]; +- (instancetype)init { + return [self initWithOptions:@{}]; } -+ (NSPersistentStoreCoordinator *)makeStoreWithStructOptions:(EncryptedStoreOptions *) options managedObjectModel:(NSManagedObjectModel *)objModel -{ - NSError * error; - return [self makeStoreWithStructOptions:options managedObjectModel:objModel error:&error]; + +- (NSString *)databaseFilename { + return [self.databaseName stringByAppendingPathExtension:self.databaseExtension]; } -+ (NSPersistentStoreCoordinator *)makeStore:(NSManagedObjectModel *)objModel passcode:(NSString *)passcode -{ - NSError * error; - return [self makeStore:objModel passcode:passcode error:&error]; + +- (NSString *)databaseNameInBundle:(NSBundle *)bundle { + NSString *bundleNameKey = (__bridge NSString *)kCFBundleNameKey; + NSString *databaseName = bundle.infoDictionary[bundleNameKey]; + return databaseName; } -+ (NSPersistentStoreCoordinator *)makeStoreWithOptions:(NSDictionary *)options managedObjectModel:(NSManagedObjectModel *)objModel error:(NSError *__autoreleasing *)error -{ - NSPersistentStoreCoordinator * persistentCoordinator = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:objModel]; - - // NSString* appSupportDir = [NSSearchPathForDirectoriesInDomains(NSApplicationSupportDirectory, NSUserDomainMask, YES) objectAtIndex:0]; - +@end +@implementation EncryptedStoreFileManagerConfiguration (OptionsKeys) ++ (NSString *)optionFileManager { + return NSStringFromSelector(_cmd); +} ++ (NSString *)optionBundle { + return NSStringFromSelector(_cmd); +} ++ (NSString *)optionDatabaseName { + return NSStringFromSelector(_cmd); +} ++ (NSString *)optionDatabaseExtension { + return NSStringFromSelector(_cmd); +} ++ (NSString *)optionDatabaseURL { + return NSStringFromSelector(_cmd); +} +@end + +@implementation EncryptedStoreFileManager +#pragma mark - Initialization ++ (instancetype)defaultManager { + return self.new; +} + +- (instancetype)initWithConfiguration:(EncryptedStoreFileManagerConfiguration *)configuration { + if (self = [super init]) { + self.configuration = configuration; + } + return self; +} + +#pragma mark - Getters +- (EncryptedStoreFileManagerConfiguration *)configuration { + if (!_configuration) { + _configuration = EncryptedStoreFileManagerConfiguration.new; + } + return _configuration; +} + +#pragma mark - Setup +- (void)setupDatabaseWithOptions:(NSDictionary *)options error:(NSError *__autoreleasing *)error { BOOL backup = YES; - NSURL *databaseURL; - id dburl = [options objectForKey:EncryptedStoreDatabaseLocation]; - if(dburl != nil) { - if ([dburl isKindOfClass:[NSString class]]){ - databaseURL = [NSURL URLWithString:[options objectForKey:EncryptedStoreDatabaseLocation]]; - backup = NO; - } - else if ([dburl isKindOfClass:[NSURL class]]){ - databaseURL = dburl; - backup = NO; - } + NSURL *databaseURL = nil; + + id dburl = [options objectForKey:EncryptedStoreDatabaseLocation] ?: self.configuration.databaseURL; + + if ([dburl isKindOfClass:[NSString class]]){ + databaseURL = [NSURL URLWithString:dburl]; + backup = NO; } + else if ([dburl isKindOfClass:[NSURL class]]){ + databaseURL = dburl; + backup = NO; + } + if (databaseURL) { + self.configuration.databaseURL = databaseURL; NSMutableDictionary *fileAttributes = [options mutableCopy]; [fileAttributes removeObjectsForKeys:@[EncryptedStorePassphraseKey, EncryptedStoreDatabaseLocation]]; - - [[NSFileManager defaultManager] setAttributes:[fileAttributes copy] ofItemAtPath:[databaseURL absoluteString] error:nil]; + [self setAttributes:fileAttributes ofItemAtURL:databaseURL error:error]; } if (backup){ - NSString *dbNameKey = (__bridge NSString *)kCFBundleNameKey; - NSString *dbName = NSBundle.mainBundle.infoDictionary[dbNameKey]; - NSFileManager *fileManager = [NSFileManager defaultManager]; - NSURL *applicationSupportURL = [[fileManager URLsForDirectory:NSDocumentDirectory inDomains:NSUserDomainMask] lastObject]; - [fileManager createDirectoryAtURL:applicationSupportURL withIntermediateDirectories:NO attributes:nil error:nil]; - databaseURL = [applicationSupportURL URLByAppendingPathComponent:[dbName stringByAppendingString:@".sqlite"]]; + [self.configuration.fileManager createDirectoryAtURL:self.applicationSupportURL withIntermediateDirectories:NO attributes:nil error:error]; + } +} + +- (NSURL *)databaseURL { + return self.configuration.databaseURL ?: [self.applicationSupportURL URLByAppendingPathComponent:self.configuration.databaseFilename]; +} +@end + +@implementation EncryptedStoreFileManager (FileManagerExtensions) +- (NSURL *)applicationSupportURL { + NSFileManager *manager = self.configuration.fileManager; + NSURL *applicationSupportURL = [[manager URLsForDirectory:NSDocumentDirectory inDomains:NSUserDomainMask] lastObject]; + return applicationSupportURL; +} + +- (void)setAttributes:(NSDictionary *)attributes ofItemAtURL:(NSURL *)url error:(NSError *__autoreleasing *)error { + [self.configuration.fileManager setAttributes:attributes ofItemAtPath:[[url absoluteString] copy] error:error]; +} +@end +@implementation EncryptedStore (OptionsKeys) ++ (NSString *)optionType { + return EncryptedStoreType; +} ++ (NSString *)optionPassphraseKey { + return EncryptedStorePassphraseKey; +} ++ (NSString *)optionErrorDomain { + return EncryptedStoreErrorDomain; +} ++ (NSString *)optionErrorMessageKey { + return EncryptedStoreErrorMessageKey; +} ++ (NSString *)optionDatabaseLocation { + return EncryptedStoreDatabaseLocation; +} ++ (NSString *)optionCacheSize { + return EncryptedStoreCacheSize; +} ++ (NSString *)optionFileManager { + return EncryptedStoreFileManagerOption; +} +@end + +@implementation EncryptedStore (Configuration) +- (NSDictionary *)configurationOptions { + return self.options; +} + +- (EncryptedStoreFileManager *)fileManager { + EncryptedStoreFileManager *fileManager = [self.configurationOptions objectForKey:self.class.optionFileManager]; + + if ([fileManager isKindOfClass:EncryptedStoreFileManager.class]) { + return fileManager; } - [persistentCoordinator addPersistentStoreWithType:EncryptedStoreType configuration:nil URL:databaseURL - options:options error:error]; + return EncryptedStoreFileManager.defaultManager; +} +@end +@implementation EncryptedStore (Initialization) ++ (NSPersistentStoreCoordinator *)makeStoreWithOptions:(NSDictionary *)options managedObjectModel:(NSManagedObjectModel *)objModel error:(NSError *__autoreleasing *)error +{ + NSPersistentStoreCoordinator * persistentCoordinator = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:objModel]; + + // NSString* appSupportDir = [NSSearchPathForDirectoriesInDomains(NSApplicationSupportDirectory, NSUserDomainMask, YES) objectAtIndex:0]; + EncryptedStoreFileManager *fileManager = [options objectForKey:self.optionFileManager] ?: [EncryptedStoreFileManager defaultManager]; + [fileManager setupDatabaseWithOptions:options error:error]; + if (error) { + NSError *theError = *error; + if (theError) { + NSLog(@"Error: %@\n%@\n%@", theError, [theError userInfo], [theError localizedDescription]); + } + } + + NSURL *databaseURL = fileManager.databaseURL; + // BOOL backup = YES; + // NSURL *databaseURL; + // id dburl = [options objectForKey:EncryptedStoreDatabaseLocation]; + // if(dburl != nil) { + // if ([dburl isKindOfClass:[NSString class]]){ + // databaseURL = [NSURL URLWithString:[options objectForKey:EncryptedStoreDatabaseLocation]]; + // backup = NO; + // } + // else if ([dburl isKindOfClass:[NSURL class]]){ + // databaseURL = dburl; + // backup = NO; + // } + // } + + // if (databaseURL) + // { + // NSMutableDictionary *fileAttributes = [options mutableCopy]; + // [fileAttributes removeObjectsForKeys:@[EncryptedStorePassphraseKey, EncryptedStoreDatabaseLocation]]; + + // [[NSFileManager defaultManager] setAttributes:[fileAttributes copy] ofItemAtPath:[databaseURL absoluteString] error:nil]; + // } + + // if (backup){ + // NSString *dbNameKey = (__bridge NSString *)kCFBundleNameKey; + // NSString *dbName = NSBundle.mainBundle.infoDictionary[dbNameKey]; + // NSFileManager *fileManager = [NSFileManager defaultManager]; + // NSURL *applicationSupportURL = [[fileManager URLsForDirectory:NSDocumentDirectory inDomains:NSUserDomainMask] lastObject]; + // [fileManager createDirectoryAtURL:applicationSupportURL withIntermediateDirectories:NO attributes:nil error:nil]; + // databaseURL = [applicationSupportURL URLByAppendingPathComponent:[dbName stringByAppendingString:@".sqlite"]]; + + // } + + persistentCoordinator = [self coordinator:persistentCoordinator byAddingStoreAtURL:databaseURL configuration:nil options:options error:error]; + if (*error) { NSLog(@"Unable to add persistent store."); @@ -157,6 +291,60 @@ + (NSPersistentStoreCoordinator *)makeStoreWithOptions:(NSDictionary *)options m return persistentCoordinator; } ++ (NSPersistentStoreCoordinator *)coordinator:(NSPersistentStoreCoordinator *)coordinator byAddingStoreAtURL:(NSURL *)url configuration:(NSString *)configuration options:(NSDictionary *)options error:(NSError * __autoreleasing*)error { + if (!coordinator) { + return nil; + } + + [coordinator addPersistentStoreWithType:EncryptedStoreType configuration:configuration URL:url options:options error:error]; + + return coordinator; +} + ++ (NSPersistentStoreDescription *)makeDescriptionWithOptions:(NSDictionary *)options configuration:(NSString *)configuration error:(NSError * __autoreleasing*)error { + NSPersistentStoreDescription *description = [NSPersistentStoreDescription new]; + EncryptedStoreFileManager *fileManager = [options objectForKey:self.class.optionFileManager] ?: [EncryptedStoreFileManager defaultManager]; + [fileManager setupDatabaseWithOptions:options error:error]; + description.type = self.optionType; + description.URL = fileManager.databaseURL; + description.configuration = configuration; + for (NSString *key in options) { + [description setOption:options[key] forKey:key]; + } + return description; +} + +@end + +@implementation EncryptedStore { + + // database resources + sqlite3 *database; + + // cache money + NSMutableDictionary *objectIDCache; + NSMutableDictionary *nodeCache; + NSMutableDictionary *objectCountCache; + NSMutableDictionary *entityTypeCache; + +} + ++ (NSPersistentStoreCoordinator *)makeStoreWithOptions:(NSDictionary *)options managedObjectModel:(NSManagedObjectModel *)objModel +{ + NSError * error; + return [self makeStoreWithOptions:options managedObjectModel:objModel error:&error]; +} ++ (NSPersistentStoreCoordinator *)makeStoreWithStructOptions:(EncryptedStoreOptions *) options managedObjectModel:(NSManagedObjectModel *)objModel +{ + NSError * error; + return [self makeStoreWithStructOptions:options managedObjectModel:objModel error:&error]; +} ++ (NSPersistentStoreCoordinator *)makeStore:(NSManagedObjectModel *)objModel passcode:(NSString *)passcode +{ + NSError * error; + return [self makeStore:objModel passcode:passcode error:&error]; +} + + (NSPersistentStoreCoordinator *)makeStoreWithStructOptions:(EncryptedStoreOptions *) options managedObjectModel:(NSManagedObjectModel *)objModel error:(NSError *__autoreleasing *)error { NSMutableDictionary *newOptions = [NSMutableDictionary dictionary]; @@ -599,8 +787,8 @@ - (id)newValueForRelationship:(NSRelationshipDescription *)relationship resolvedDestinationEntity = destinationEntity; } - NSManagedObjectID *objectID = [self newObjectIDForEntity:resolvedDestinationEntity referenceObject:value]; - [objectIDs addObject:objectID]; + NSManagedObjectID *newObjectID = [self newObjectIDForEntity:resolvedDestinationEntity referenceObject:value]; + [objectIDs addObject:newObjectID]; } } @@ -631,21 +819,23 @@ - (id)newValueForRelationship:(NSRelationshipDescription *)relationship - (void)managedObjectContextDidRegisterObjectsWithIDs:(NSArray *)objectIDs { [objectIDs enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) { - NSUInteger value = [[objectCountCache objectForKey:obj] unsignedIntegerValue]; - [objectCountCache setObject:@(value + 1) forKey:obj]; + NSUInteger value = [[self->objectCountCache objectForKey:obj] unsignedIntegerValue]; + [self->objectCountCache setObject:@(value + 1) forKey:obj]; }]; } - (void)managedObjectContextDidUnregisterObjectsWithIDs:(NSArray *)objectIDs { [objectIDs enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) { - NSNumber *value = [objectCountCache objectForKey:obj]; + NSNumber *value = [self->objectCountCache objectForKey:obj]; if (value) { NSUInteger newValue = ([value unsignedIntegerValue] - 1); if (newValue == 0) { - [objectCountCache removeObjectForKey:obj]; - [nodeCache removeObjectForKey:obj]; + [self->objectCountCache removeObjectForKey:obj]; + [self->nodeCache removeObjectForKey:obj]; + } + else { + [self->objectCountCache setObject:@(newValue) forKey:obj]; } - else { [objectCountCache setObject:@(newValue) forKey:obj]; } } }]; } @@ -699,28 +889,28 @@ - (BOOL)loadMetadata:(NSError **)error { #ifdef SQLITE_DETERMINISTIC //enable regexp - sqlite3_create_function(database, "REGEXP", 2, SQLITE_UTF8 | SQLITE_DETERMINISTIC, NULL, (void *)dbsqliteRegExp, NULL, NULL); + sqlite3_create_function(self->database, "REGEXP", 2, SQLITE_UTF8 | SQLITE_DETERMINISTIC, NULL, (void *)dbsqliteRegExp, NULL, NULL); //enable case insentitivity - sqlite3_create_function(database, "STRIP_CASE", 1, SQLITE_UTF8 | SQLITE_DETERMINISTIC, NULL, (void *)dbsqliteStripCase, NULL, NULL); + sqlite3_create_function(self->database, "STRIP_CASE", 1, SQLITE_UTF8 | SQLITE_DETERMINISTIC, NULL, (void *)dbsqliteStripCase, NULL, NULL); //enable diacritic insentitivity - sqlite3_create_function(database, "STRIP_DIACRITICS", 1, SQLITE_UTF8 | SQLITE_DETERMINISTIC, NULL, (void *)dbsqliteStripDiacritics, NULL, NULL); + sqlite3_create_function(self->database, "STRIP_DIACRITICS", 1, SQLITE_UTF8 | SQLITE_DETERMINISTIC, NULL, (void *)dbsqliteStripDiacritics, NULL, NULL); //enable combined case and diacritic insentitivity - sqlite3_create_function(database, "STRIP_CASE_DIACRITICS", 1, SQLITE_UTF8 | SQLITE_DETERMINISTIC, NULL, (void *)dbsqliteStripCaseDiacritics, NULL, NULL); + sqlite3_create_function(self->database, "STRIP_CASE_DIACRITICS", 1, SQLITE_UTF8 | SQLITE_DETERMINISTIC, NULL, (void *)dbsqliteStripCaseDiacritics, NULL, NULL); #else //enable regexp - sqlite3_create_function(database, "REGEXP", 2, SQLITE_UTF8, NULL, (void *)dbsqliteRegExp, NULL, NULL); + sqlite3_create_function(self->database, "REGEXP", 2, SQLITE_UTF8, NULL, (void *)dbsqliteRegExp, NULL, NULL); //enable case insentitivity - sqlite3_create_function(database, "STRIP_CASE", 1, SQLITE_UTF8, NULL, (void *)dbsqliteStripCase, NULL, NULL); + sqlite3_create_function(self->database, "STRIP_CASE", 1, SQLITE_UTF8, NULL, (void *)dbsqliteStripCase, NULL, NULL); //enable diacritic insentitivity - sqlite3_create_function(database, "STRIP_DIACRITICS", 1, SQLITE_UTF8, NULL, (void *)dbsqliteStripDiacritics, NULL, NULL); + sqlite3_create_function(self->database, "STRIP_DIACRITICS", 1, SQLITE_UTF8, NULL, (void *)dbsqliteStripDiacritics, NULL, NULL); //enable combined case and diacritic insentitivity - sqlite3_create_function(database, "STRIP_CASE_DIACRITICS", 1, SQLITE_UTF8, NULL, (void *)dbsqliteStripCaseDiacritics, NULL, NULL); + sqlite3_create_function(self->database, "STRIP_CASE_DIACRITICS", 1, SQLITE_UTF8, NULL, (void *)dbsqliteStripCaseDiacritics, NULL, NULL); #endif // ask if we have a metadata table @@ -778,7 +968,8 @@ - (BOOL)loadMetadata:(NSError **)error { // load the old model: NSMutableArray *bundles = [NSMutableArray array]; - [bundles addObject:[NSBundle mainBundle]]; + NSBundle *bundle = self.fileManager.configuration.bundle; + [bundles addObject:bundle]; NSManagedObjectModel *oldModel = [NSManagedObjectModel mergedModelFromBundles:bundles forStoreMetadata:metadata]; @@ -891,8 +1082,49 @@ - (BOOL)saveMetadata { return YES; } -#pragma mark - passphrase +#pragma mark - Passphrase manipulation +#pragma mark - Public +- (BOOL)checkAndChangeDatabasePassphrase:(NSString *)oldPassphrase toNewPassphrase:(NSString *)newPassphrase error:(NSError *__autoreleasing *)error { + + NSError *validateError = nil; + BOOL validateResult = [self validateDatabasePassphrase:oldPassphrase error:&validateError]; + + if (error) { + *error = validateError; + } + + BOOL checked = validateResult && validateError == nil; + + if (!checked) { + return checked; + } + + NSError *changeError = nil; + BOOL changeResult = [self changeDatabasePassphrase:oldPassphrase toNewPassphrase:newPassphrase error:&changeError]; + + BOOL changed = changeResult && changeError == nil; + + if (error) { + *error = changeError; + } + + return changed; +} + +- (BOOL)checkDatabasePassphrase:(NSString *)passphrase error:(NSError *__autoreleasing *)error { + NSError *theError = nil; + BOOL operationResult = [self validateDatabasePassphrase:passphrase error:&theError]; + BOOL checked = operationResult && theError == nil; + + if (error) { + *error = theError; + } + + return checked; +} + +#pragma mark - Internal - (BOOL)configureDatabasePassphrase:(NSError *__autoreleasing*)error { NSString *passphrase = [[self options] objectForKey:EncryptedStorePassphraseKey]; return [self setDatabasePassphrase:passphrase error:error]; @@ -917,7 +1149,7 @@ - (BOOL)checkDatabaseStatusWithError:(NSError *__autoreleasing*)error { *error = [NSError errorWithDomain:EncryptedStoreErrorDomain code:EncryptedStoreErrorIncorrectPasscode userInfo:userInfo]; } } - return result && (*error == nil); + return result && (error == NULL || *error == nil); } - (BOOL)changeDatabasePassphrase:(NSString *)passphrase error:(NSError *__autoreleasing*)error { @@ -963,7 +1195,7 @@ - (BOOL)setDatabasePassphrase:(NSString *)passphrase error:(NSError *__autorelea result = [self checkDatabaseStatusWithError:error]; } - return result && (*error == nil); + return result && (error == NULL || *error == nil); } - (BOOL)validateDatabasePassphrase:(NSString *)passphrase error:(NSError *__autoreleasing*)error { @@ -971,7 +1203,7 @@ - (BOOL)validateDatabasePassphrase:(NSString *)passphrase error:(NSError *__auto int status; status = sqlite3_close(database); BOOL result = status == SQLITE_OK; - + if (result) { // try to open status = sqlite3_open([[[self URL] path] UTF8String], &database); @@ -995,22 +1227,22 @@ - (BOOL)validateDatabasePassphrase:(NSString *)passphrase error:(NSError *__auto database = NULL; } } - + else { // could not close databse? hm - if (error) { + if (error) { NSMutableDictionary *userInfo = [@{NSLocalizedDescriptionKey : @"Could not close database :("} mutableCopy]; - // If we have a DB error keep it for extra info - NSError *underlyingError = [self databaseError]; - if (underlyingError) { - userInfo[NSUnderlyingErrorKey] = underlyingError; - } - *error = [NSError errorWithDomain:EncryptedStoreErrorDomain code:EncryptedStoreErrorIncorrectPasscode userInfo:userInfo]; - } + // If we have a DB error keep it for extra info + NSError *underlyingError = [self databaseError]; + if (underlyingError) { + userInfo[NSUnderlyingErrorKey] = underlyingError; } + *error = [NSError errorWithDomain:EncryptedStoreErrorDomain code:EncryptedStoreErrorIncorrectPasscode userInfo:userInfo]; + } + } return result && (*error == nil); - } +} - (BOOL)changeDatabasePassphrase:(NSString *)oldPassphrase toNewPassphrase:(NSString *)newPassphrase error:(NSError *__autoreleasing*)error { BOOL result; @@ -1240,17 +1472,17 @@ - (BOOL)migrateFromModel:(NSManagedObjectModel *)fromModel toModel:(NSManagedObj error:error]; } - [destinationEntity.directRelationshipsByName enumerateKeysAndObjectsUsingBlock:^(NSString * _Nonnull key, NSRelationshipDescription * _Nonnull obj, BOOL * _Nonnull stop) { + [destinationEntity.directRelationshipsByName enumerateKeysAndObjectsUsingBlock:^(NSString * _Nonnull key, NSRelationshipDescription * _Nonnull obj, BOOL * _Nonnull relationshipStop) { NSString *tableName = [self tableNameForRelationship:obj]; BOOL hasTable; if (![self hasTable:&hasTable withName:tableName error:error]) { success = NO; - *stop = YES; + *relationshipStop = YES; } if (!hasTable) { if (![self createTableForRelationship:obj error:error]) { success = NO; - *stop = YES; + *relationshipStop = YES; } } }]; @@ -1310,7 +1542,8 @@ - (BOOL)migrateFromModel:(NSManagedObjectModel *)fromModel toModel:(NSManagedObj - (BOOL)initializeDatabase:(NSError**)error { BOOL __block success = YES; NSMutableSet *manytomanys = [NSMutableSet set]; - + NSMutableSet *tableNames = [NSMutableSet new]; + if (success) { NSArray *entities = [self storeEntities]; [entities enumerateObjectsUsingBlock:^(NSEntityDescription *entity, NSUInteger idx, BOOL *stop) { @@ -1324,8 +1557,12 @@ - (BOOL)initializeDatabase:(NSError**)error { NSRelationshipDescription *relation = [relations objectForKey:key]; NSRelationshipDescription *inverse = [relation inverseRelationship]; if (relation.transient || inverse.transient) continue; - if ([relation isToMany] && [inverse isToMany] && ![manytomanys containsObject:inverse]) { - [manytomanys addObject:relation]; + if ([relation isToMany] && [inverse isToMany]) { + NSString *const tableName = [self tableNameForRelationship:relation]; + if (! [tableNames containsObject:tableName]) { + [manytomanys addObject:relation]; + [tableNames addObject:tableName]; + } } } }]; @@ -1779,8 +2016,8 @@ - (BOOL)alterRelationshipForSourceEntity:(NSEntityDescription *)sourceEntity return; } } else { - NSString *checkExistenceOfTable = [NSString stringWithFormat:@"SELECT count(*) FROM %@", newTableName]; - statement = [self preparedStatementForQuery:checkExistenceOfTable]; + NSString *newCheckExistenceOfTable = [NSString stringWithFormat:@"SELECT count(*) FROM %@", newTableName]; + statement = [self preparedStatementForQuery:newCheckExistenceOfTable]; sqlite3_step(statement); if (statement != NULL && sqlite3_finalize(statement) == SQLITE_OK) { @@ -1807,14 +2044,14 @@ - (BOOL)getTableColumnNames:(NSSet **)nameSet tableName:(NSString *)tableName er NSMutableSet *mNameSet = [NSMutableSet set]; while (sqlite3_step(tiPragma) == SQLITE_ROW) { //const int rowId = sqlite3_column_int(tiPragma, 0); - const char *name = sqlite3_column_text(tiPragma, 1); + const unsigned char *name = sqlite3_column_text(tiPragma, 1); //const char *type = sqlite3_column_text(tiPragma, 2); //const int canBeNull = sqlite3_column_int(tiPragma, 3); //const char *dftValue = sqlite3_column_text(tiPragma, 4); //const int pkOrder = sqlite3_column_int(tiPragma, 5); //NSLog(@"row[%d] name:[%s] type:[%s] null[%d] dft[%s] pkOrder[%d]", rowId, name, type, canBeNull, dftValue, pkOrder); - [mNameSet addObject:[NSString stringWithCString:name encoding:NSUTF8StringEncoding]]; + [mNameSet addObject:[NSString stringWithCString:(char*)name encoding:NSUTF8StringEncoding]]; } if (tiPragma == NULL || sqlite3_finalize(tiPragma) != SQLITE_OK) { if (error) *error = [self databaseError]; @@ -1843,7 +2080,7 @@ -(NSComparator)fixedLocaleCaseInsensitiveComparator -(NSString *)tableNameForRelationship:(NSRelationshipDescription *)relationship { NSRelationshipDescription *inverse = [relationship inverseRelationship]; - NSArray *names = [@[[relationship name],[inverse name]] sortedArrayUsingComparator:[self fixedLocaleCaseInsensitiveComparator]]; + NSArray *names = @[[relationship name],[inverse name]]; return [NSString stringWithFormat:@"ecd_%@",[names componentsJoinedByString:@"_"]]; } @@ -1967,7 +2204,7 @@ -(BOOL)relationships:(NSRelationshipDescription *)relationship firstIDColumn:(NS } - (BOOL)checkTableForMissingColumns:(NSDictionary *)metadata error:(NSError **)error { - NSManagedObjectModel *currentModel = [NSManagedObjectModel mergedModelFromBundles:@[[NSBundle mainBundle]] + NSManagedObjectModel *currentModel = [NSManagedObjectModel mergedModelFromBundles:@[self.fileManager.configuration.bundle] forStoreMetadata:metadata]; if (!currentModel) { @@ -1999,7 +2236,7 @@ - (BOOL)checkTableForMissingColumns:(NSDictionary *)metadata error:(NSError **)e } } - [entity.relationshipsByName.allValues enumerateObjectsUsingBlock:^(NSRelationshipDescription * _Nonnull relation, NSUInteger idx, BOOL * _Nonnull stop) { + [entity.relationshipsByName.allValues enumerateObjectsUsingBlock:^(NSRelationshipDescription * _Nonnull relation, NSUInteger relationshipIdx, BOOL * _Nonnull relationshipStop) { NSRelationshipDescription *inverse = relation.inverseRelationship; if (relation.transient || inverse.transient) return; if (relation.toMany && inverse.toMany && ![manytomany containsObject:inverse]) { @@ -2160,7 +2397,7 @@ - (BOOL)handleInsertedObjectsInSaveRequest:(NSSaveChangesRequest *)request error NSMutableArray *keys = [NSMutableArray array]; NSMutableArray *columns = [NSMutableArray arrayWithObject:@"'__objectid'"]; NSDictionary *properties = [entity propertiesByName]; - [properties enumerateKeysAndObjectsUsingBlock:^(id key, NSPropertyDescription *obj, BOOL *stop) { + [properties enumerateKeysAndObjectsUsingBlock:^(id key, NSPropertyDescription *obj, BOOL *propertyStop) { if (obj.transient) return; if ([obj isKindOfClass:[NSAttributeDescription class]]) { [keys addObject:key]; @@ -2253,7 +2490,7 @@ - (BOOL)handleInsertedObjectsInSaveRequest:(NSSaveChangesRequest *)request error // bind properties int __block columnIndex; - [keys enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) { + [keys enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *keyStop) { // SQL indexes start at 1 columnIndex = (int)idx + 1; NSPropertyDescription *property = [properties objectForKey:obj]; @@ -2345,7 +2582,7 @@ - (BOOL)handleUpdatedObjectsInSaveRequest:(NSSaveChangesRequest *)request cache: // enumerate changed properties NSDictionary *properties = [entity propertiesByName]; - [changedAttributes enumerateKeysAndObjectsUsingBlock:^(id key, id value, BOOL *stop) { + [changedAttributes enumerateKeysAndObjectsUsingBlock:^(id key, id value, BOOL *attributeStop) { id property = [properties objectForKey:key]; if ([property isKindOfClass:[NSAttributeDescription class]]) { [columns addObject:[NSString stringWithFormat:@"%@=?", key]]; @@ -2399,7 +2636,7 @@ - (BOOL)handleUpdatedObjectsInSaveRequest:(NSSaveChangesRequest *)request cache: sqlite3_stmt *statement = [self preparedStatementForQuery:string]; // bind values - [keys enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) { + [keys enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *keyStop) { id value = [changedAttributes objectForKey:obj]; id property = [properties objectForKey:obj]; #if USE_MANUAL_NODE_CACHE @@ -2607,9 +2844,16 @@ - (BOOL)handleDeletedRelationInSaveRequest:(NSManagedObject *)object error:(NSEr NSRelationshipDescription *desc = (NSRelationshipDescription *)prop; NSRelationshipDescription *inverse = [desc inverseRelationship]; if ([desc isToMany] && [inverse isToMany]) { - + NSEntityDescription *rootSourceEntity = [self rootForEntity:desc.entity]; + NSEntityDescription *rootDestinationEntity = [self rootForEntity:inverse.entity]; + NSString *entityName = [rootSourceEntity name]; + + if ([rootSourceEntity isEqual:rootDestinationEntity]) { + entityName = [entityName stringByAppendingString:@"_1"]; + } + NSString *string = [NSString stringWithFormat:@"DELETE FROM %@ WHERE %@__objectid=?;", - [self tableNameForRelationship:desc],[[self rootForEntity:[desc entity]] name]]; + [self tableNameForRelationship:desc],entityName]; sqlite3_stmt *statement = [self preparedStatementForQuery:string]; NSNumber *number = [self referenceObjectForObjectID:[object objectID]]; sqlite3_bind_int64(statement, 1, [number unsignedLongLongValue]); @@ -3822,19 +4066,19 @@ - (void)parseExpression:(NSExpression *)expression // Handle the case where the last component points to a relationship rather than a simple attribute __block NSDictionary * subProperties = properties; - __block id property = nil; + __block id localProperty = nil; [pathComponents enumerateObjectsUsingBlock:^(NSString * comp, NSUInteger idx, BOOL * stop) { - property = [subProperties objectForKey:comp]; - if ([property isKindOfClass:[NSRelationshipDescription class]]) { - NSEntityDescription * entity = [property destinationEntity]; - subProperties = entity.propertiesByName; + localProperty = [subProperties objectForKey:comp]; + if ([localProperty isKindOfClass:[NSRelationshipDescription class]]) { + NSEntityDescription * localEntity = [localProperty destinationEntity]; + subProperties = localEntity.propertiesByName; } else { - property = nil; + localProperty = nil; *stop = YES; } }]; - if ([property isKindOfClass:[NSRelationshipDescription class]]) { + if ([localProperty isKindOfClass:[NSRelationshipDescription class]]) { [request setReturnsDistinctResults:YES]; lastComponentName = @"__objectID"; } @@ -3933,17 +4177,17 @@ - (void)parseExpression:(NSExpression *)expression NSDictionary *exprOperator = @{ @"operator" : @"=", @"format" : @"%@" }; for (NSExpression *expr in expression.collection) { - id operand = nil; - id binding = nil; + id exprOperand = nil; + id exprBinding = nil; [self parseExpression:expr inPredicate:predicate inFetchRequest:request operator:exprOperator - operand:&operand - bindings:&binding]; + operand:&exprOperand + bindings:&exprBinding]; - if (operand) [subOperands addObject:operand]; - if (binding) [*bindings addObject:binding]; + if (exprOperand) [subOperands addObject:exprOperand]; + if (exprBinding) [*bindings addObject:exprBinding]; } *operand = [NSString stringWithFormat:[operator objectForKey:@"format"], diff --git a/README.md b/README.md index 919b38f..576645b 100644 --- a/README.md +++ b/README.md @@ -36,6 +36,85 @@ Provides a Core Data store that encrypts all data that is persisted. Besides th * Run `pod install` * In your application delegate source file (AppDelegate.m), add `#import "EncryptedStore.h"` +# Using EncryptedStoreFileManager +In case of strong coupling with file system functions and others default conventions FileManager was introduced. + +Have a look at components: + +* EncryptedStoreFileManagerConfiguration +* EncryptedStoreFileManager + +Various options are stored in Configuration. + +And FileManager could be passed to all functions as an option. + +``` +NSDictionary *options = @{ EncryptedStore.optionFileManager : fileManager }; +``` + +However, it should solve some dirty hacks. +Let's try. + +## Database lives in different bundle. + +``` +NSBundle *bundle = [NSBundle bundleWithIdentifier:"com.opensource.database_framework"]; +EncryptedStoreFileManagerConfiguration *configuration = [EncryptedStoreFileManagerConfiguration new]; +configuration.bundle = bundle; + +// or +[[EncryptedStoreFileManagerConfiguration alloc] initWithOptions: @{EncryptedStoreFileManagerConfiguration.optionBundle : bundle}]; + +// next, you need to bypassing configuration to setup store. +EncryptedStoreFileManager *fileManager = [[EncryptedStoreFileManager alloc] initWithConfiguration:configuration]; +NSDictionary *options = @{ EncryptedStore.optionFileManager : fileManager }; +``` + +## Complex setup and file system methods separation. + +By default, database file (sqlite) is stored on disk in Application Support Directory. +But you can configure file extension, file name and file url in `EncryptedStoreFileManagerConfiguration`. + +## Apply attributes to database file. +In general, this functionality is not needed. +It is a part of setup core data stack process. + +## Configure persistentContainer +`NSPersistentContainer` uses NSPersistentStoreDescriptions to configure stores. + +``` +NSManagedObjectModel *model = [NSManagedObjectModel new]; +NSPersistentContainer *container = [[NSPersistentContainer alloc] initWithName:@"abc" managedObjectModel:model]; +NSDictionary *options = @{ + self.optionPassphraseKey : @"123", + self.optionFileManager : [EncryptedStoreFileManager defaultManager] +}; +NSPersistentStoreDescription *description = [self makeDescriptionWithOptions:options configuration:nil error:nil]; + +container.persistentStoreDescriptions = @[description]; + +[container loadPersistentStoresWithCompletionHandler:^(NSPersistentStoreDescription *description, NSError * error) { + if (error) { + NSLog(@"error! %@", error); + } +}]; +``` + +But if you wish: + +``` +EncryptedStore *store = // retrieve store from coordinator. + +// set database file attributes +NSDictionary *attributes = // set attributes +NSError *error = nil; +[store.fileManager setAttributes:attributes ofItemAtURL:store.fileManager.databaseURL error:&error]; + +// inspect bundle +store.fileManager.configuration.bundle; +``` + + # Using EncryptedStore EncryptedStore is known to work successfully on iOS versions 6.0 through 9.2. diff --git a/exampleProjects/IncrementalStore/Incremental Store.xcodeproj/project.pbxproj b/exampleProjects/IncrementalStore/Incremental Store.xcodeproj/project.pbxproj index f8ebf30..cd92617 100644 --- a/exampleProjects/IncrementalStore/Incremental Store.xcodeproj/project.pbxproj +++ b/exampleProjects/IncrementalStore/Incremental Store.xcodeproj/project.pbxproj @@ -549,8 +549,18 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_LAUNCHIMAGE_NAME = LaunchImage; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_REPEATED_USE_OF_WEAK = YES; + CLANG_WARN_SUSPICIOUS_IMPLICIT_CONVERSION = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CODE_SIGN_IDENTITY = "iPhone Developer"; + GCC_NO_COMMON_BLOCKS = YES; GCC_PRECOMPILE_PREFIX_HEADER = YES; GCC_PREFIX_HEADER = "Incremental Store Demo/Incremental Store Demo-Prefix.pch"; + GCC_WARN_ABOUT_MISSING_NEWLINE = YES; + GCC_WARN_SHADOW = YES; HEADER_SEARCH_PATHS = "\"$PROJECT_DIR/../Incremental Store\"/**"; INFOPLIST_FILE = "Incremental Store Demo/Incremental Store Demo-Info.plist"; PRODUCT_BUNDLE_IDENTIFIER = "org.mitre.${PRODUCT_NAME:rfc1034identifier}"; @@ -564,8 +574,18 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_LAUNCHIMAGE_NAME = LaunchImage; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_REPEATED_USE_OF_WEAK = YES; + CLANG_WARN_SUSPICIOUS_IMPLICIT_CONVERSION = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CODE_SIGN_IDENTITY = "iPhone Developer"; + GCC_NO_COMMON_BLOCKS = YES; GCC_PRECOMPILE_PREFIX_HEADER = YES; GCC_PREFIX_HEADER = "Incremental Store Demo/Incremental Store Demo-Prefix.pch"; + GCC_WARN_ABOUT_MISSING_NEWLINE = YES; + GCC_WARN_SHADOW = YES; HEADER_SEARCH_PATHS = "\"$PROJECT_DIR/../Incremental Store\"/**"; INFOPLIST_FILE = "Incremental Store Demo/Incremental Store Demo-Info.plist"; PRODUCT_BUNDLE_IDENTIFIER = "org.mitre.${PRODUCT_NAME:rfc1034identifier}"; diff --git a/exampleProjects/IncrementalStore/Incremental Store.xcodeproj/xcshareddata/xcschemes/Incremental Store Demo.xcscheme b/exampleProjects/IncrementalStore/Incremental Store.xcodeproj/xcshareddata/xcschemes/Incremental Store Demo.xcscheme index 7215af2..e340c7e 100644 --- a/exampleProjects/IncrementalStore/Incremental Store.xcodeproj/xcshareddata/xcschemes/Incremental Store Demo.xcscheme +++ b/exampleProjects/IncrementalStore/Incremental Store.xcodeproj/xcshareddata/xcschemes/Incremental Store Demo.xcscheme @@ -26,7 +26,8 @@ buildConfiguration = "Debug" selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" - shouldUseLaunchSchemeArgsEnv = "YES"> + shouldUseLaunchSchemeArgsEnv = "YES" + codeCoverageEnabled = "YES"> diff --git a/exampleProjects/IncrementalStore/Model.xcdatamodeld/Model.xcdatamodel/contents b/exampleProjects/IncrementalStore/Model.xcdatamodeld/Model.xcdatamodel/contents index 79fa0f3..29f0a25 100644 --- a/exampleProjects/IncrementalStore/Model.xcdatamodeld/Model.xcdatamodel/contents +++ b/exampleProjects/IncrementalStore/Model.xcdatamodeld/Model.xcdatamodel/contents @@ -1,5 +1,10 @@ - + + + + + + @@ -10,7 +15,7 @@ - + @@ -21,8 +26,8 @@ - - + + @@ -30,10 +35,11 @@ + - + \ No newline at end of file diff --git a/exampleProjects/IncrementalStore/Tests/IncrementalStoreTests.m b/exampleProjects/IncrementalStore/Tests/IncrementalStoreTests.m index 2e6c912..434520a 100644 --- a/exampleProjects/IncrementalStore/Tests/IncrementalStoreTests.m +++ b/exampleProjects/IncrementalStore/Tests/IncrementalStoreTests.m @@ -1049,4 +1049,93 @@ - (void)test_stringComparision { XCTAssertEqualObjects(query(@"TRUEPREDICATE", nil), [NSSet setWithArray:expected]); } +- (void)test_fetchAccountsByManyToManyRelationship { + // Create three Account entities with transfer relationships set up this way: + // + // - account0 can transfer to account1 and account2 + // - account1 can transfer to account2 + // + // The fetch request is set up to fetch those entities that can send to at least one other Account AND + // can receive from at least one other Account. + + NSString *const entityName = @"Account"; + NSManagedObject *const account0 = [NSEntityDescription insertNewObjectForEntityForName:entityName + inManagedObjectContext:context]; + NSManagedObject *const account1 = [NSEntityDescription insertNewObjectForEntityForName:entityName + inManagedObjectContext:context]; + NSManagedObject *const account2 = [NSEntityDescription insertNewObjectForEntityForName:entityName + inManagedObjectContext:context]; + + NSString *const accountID0 = @"account0"; + NSString *const accountID1 = @"account1"; + NSString *const accountID2 = @"account2"; + NSString *const idKey = @"accountID"; + NSString *const transferFromAccountsKey = @"transferFromAccounts"; + NSString *const transferToAccountsKey = @"transferToAccounts"; + + [account0 setValue:accountID0 forKey:idKey]; + [account1 setValue:accountID1 forKey:idKey]; + [account2 setValue:accountID2 forKey:idKey]; + + NSMutableSet *const transferToAccounts0 = [account0 mutableSetValueForKey:transferToAccountsKey]; + [transferToAccounts0 addObject:account1]; + [transferToAccounts0 addObject:account2]; + + NSMutableSet *const transferToAccounts1 = [account1 mutableSetValueForKey:transferToAccountsKey]; + [transferToAccounts1 addObject:account2]; + + NSSet *transferFromAccounts; + NSSet *transferToAccounts; + + transferFromAccounts = [account0 valueForKey:transferFromAccountsKey]; + transferToAccounts = [account0 valueForKey:transferToAccountsKey]; + XCTAssertEqual(transferFromAccounts.count, 0, + @"The %@ entity with ID %@ should have an empty set for %@", entityName, accountID0, + transferFromAccountsKey); + XCTAssertEqual(transferToAccounts.count, 2, + @"The %@ entity with ID %@ should be able to send to two %@ entities", entityName, + accountID0, entityName); + + transferFromAccounts = [account1 valueForKey:transferFromAccountsKey]; + transferToAccounts = [account1 valueForKey:transferToAccountsKey]; + XCTAssertEqual(transferFromAccounts.count, 1, + @"The %@ entity with ID %@ should be able to receive from one other %@", entityName, + accountID1, entityName); + XCTAssertTrue([transferFromAccounts containsObject:account0], + @"The %@ entity with ID %@ should have %@ in its transfer-from set", entityName, + accountID1, accountID0); + XCTAssertEqual(transferToAccounts.count, 1, + @"The %@ entity with ID %@ should be able to send to one other %@", entityName, + accountID1, entityName); + + transferFromAccounts = [account2 valueForKey:transferFromAccountsKey]; + transferToAccounts = [account2 valueForKey:transferToAccountsKey]; + XCTAssertEqual(transferFromAccounts.count, 2, + @"The %@ entity with ID %@ should be able to receive from two other %@ entities", + entityName, accountID1, entityName); + XCTAssertTrue([transferFromAccounts containsObject:account0], + @"The %@ entity with ID %@ should have %@ in its transfer-from set", entityName, + accountID2, accountID0); + XCTAssertTrue([transferFromAccounts containsObject:account1], + @"The %@ entity with ID %@ should have %@ in its transfer-from set", entityName, + accountID2, accountID1); + XCTAssertEqual(transferToAccounts.count, 0, + @"The %@ entity with ID %@ should have an empty set for %@", entityName, accountID2, + transferToAccountsKey); + + [context save:nil]; + + NSFetchRequest *const fetchRequest = [NSFetchRequest fetchRequestWithEntityName:entityName]; + NSPredicate *const linkageCountPredicate = [NSPredicate predicateWithFormat:@"%K.@count > 0 AND %K.@count > 0", + transferToAccountsKey, transferFromAccountsKey]; + fetchRequest.predicate = linkageCountPredicate; + + NSError * __autoreleasing error; + NSArray *fetchedAccounts = [context executeFetchRequest:fetchRequest error:&error]; + XCTAssertNotNil(fetchedAccounts, @"Failed to fetch %@ entities: %@", entityName, error); + XCTAssertEqual(fetchedAccounts.count, 1, @"Should have only fetched one %@ (the one with ID %@)", + entityName, accountID1); + XCTAssertTrue([fetchedAccounts containsObject:account1], @"Expected %@ to be in %@", account1, fetchedAccounts); +} + @end