mpw C code is not thread-safe + bad performance long site queries.
This commit is contained in:
parent
01c21e95bb
commit
fd35fea8cf
2
External/Pearl
vendored
2
External/Pearl
vendored
@ -1 +1 @@
|
|||||||
Subproject commit 08c42ba0f96a95703ec67cbf5846bfe6680dd0a6
|
Subproject commit 65b4e3d9984d077f66e6ab15f2ffcc4965d07825
|
@ -96,25 +96,24 @@ const char *mpw_idForBuf(const void *buf, size_t length) {
|
|||||||
return mpw_hex( hash, 32 );
|
return mpw_hex( hash, 32 );
|
||||||
}
|
}
|
||||||
|
|
||||||
//static char **mpw_hex_buf = NULL;
|
static char **mpw_hex_buf = NULL;
|
||||||
//static unsigned int mpw_hex_buf_i = 0;
|
static unsigned int mpw_hex_buf_i = 0;
|
||||||
|
|
||||||
const char *mpw_hex(const void *buf, size_t length) {
|
const char *mpw_hex(const void *buf, size_t length) {
|
||||||
|
|
||||||
// FIXME
|
// FIXME
|
||||||
// if (!mpw_hex_buf) {
|
if (!mpw_hex_buf) {
|
||||||
// mpw_hex_buf = malloc( 10 * sizeof( char * ) );
|
mpw_hex_buf = malloc( 10 * sizeof( char * ) );
|
||||||
// for (uint8_t i = 0; i < 10; ++i)
|
for (uint8_t i = 0; i < 10; ++i)
|
||||||
// mpw_hex_buf[i] = NULL;
|
mpw_hex_buf[i] = NULL;
|
||||||
// }
|
}
|
||||||
// mpw_hex_buf_i = (mpw_hex_buf_i + 1) % 10;
|
mpw_hex_buf_i = (mpw_hex_buf_i + 1) % 10;
|
||||||
//
|
|
||||||
// mpw_hex_buf[mpw_hex_buf_i] = realloc( mpw_hex_buf[mpw_hex_buf_i], length * 2 + 1 );
|
|
||||||
// for (size_t kH = 0; kH < length; kH++)
|
|
||||||
// sprintf( &(mpw_hex_buf[mpw_hex_buf_i][kH * 2]), "%02X", ((const uint8_t *)buf)[kH] );
|
|
||||||
|
|
||||||
// return mpw_hex_buf[mpw_hex_buf_i];
|
mpw_hex_buf[mpw_hex_buf_i] = realloc( mpw_hex_buf[mpw_hex_buf_i], length * 2 + 1 );
|
||||||
return NULL;
|
for (size_t kH = 0; kH < length; kH++)
|
||||||
|
sprintf( &(mpw_hex_buf[mpw_hex_buf_i][kH * 2]), "%02X", ((const uint8_t *)buf)[kH] );
|
||||||
|
|
||||||
|
return mpw_hex_buf[mpw_hex_buf_i];
|
||||||
}
|
}
|
||||||
|
|
||||||
const char *mpw_hex_l(uint32_t number) {
|
const char *mpw_hex_l(uint32_t number) {
|
||||||
|
@ -28,8 +28,10 @@
|
|||||||
#define CRACKING_PER_SECOND 2495000000UL
|
#define CRACKING_PER_SECOND 2495000000UL
|
||||||
#define CRACKING_PRICE 350
|
#define CRACKING_PRICE 350
|
||||||
|
|
||||||
|
NSOperationQueue *_mpwQueue = nil;
|
||||||
|
|
||||||
@implementation MPAlgorithmV0 {
|
@implementation MPAlgorithmV0 {
|
||||||
BN_CTX *ctx;
|
BN_CTX *_ctx;
|
||||||
}
|
}
|
||||||
|
|
||||||
- (id)init {
|
- (id)init {
|
||||||
@ -37,15 +39,22 @@
|
|||||||
if (!(self = [super init]))
|
if (!(self = [super init]))
|
||||||
return nil;
|
return nil;
|
||||||
|
|
||||||
ctx = BN_CTX_new();
|
_ctx = BN_CTX_new();
|
||||||
|
|
||||||
|
static dispatch_once_t once = 0;
|
||||||
|
dispatch_once( &once, ^{
|
||||||
|
_mpwQueue = [NSOperationQueue new];
|
||||||
|
_mpwQueue.maxConcurrentOperationCount = 1;
|
||||||
|
_mpwQueue.name = @"mpw queue";
|
||||||
|
} );
|
||||||
|
|
||||||
return self;
|
return self;
|
||||||
}
|
}
|
||||||
|
|
||||||
- (void)dealloc {
|
- (void)dealloc {
|
||||||
|
|
||||||
BN_CTX_free( ctx );
|
BN_CTX_free( _ctx );
|
||||||
ctx = NULL;
|
_ctx = NULL;
|
||||||
}
|
}
|
||||||
|
|
||||||
- (MPAlgorithmVersion)version {
|
- (MPAlgorithmVersion)version {
|
||||||
@ -68,6 +77,19 @@
|
|||||||
return [(id<MPAlgorithm>)other version] == [self version];
|
return [(id<MPAlgorithm>)other version] == [self version];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
- (void)mpw_perform:(void ( ^ )(void))operationBlock {
|
||||||
|
|
||||||
|
if ([NSOperationQueue currentQueue] == _mpwQueue) {
|
||||||
|
operationBlock();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
NSOperation *operation = [NSBlockOperation blockOperationWithBlock:operationBlock];
|
||||||
|
if ([operation respondsToSelector:@selector( qualityOfService )])
|
||||||
|
operation.qualityOfService = NSQualityOfServiceUserInitiated;
|
||||||
|
[_mpwQueue addOperations:@[ operation ] waitUntilFinished:YES];
|
||||||
|
}
|
||||||
|
|
||||||
- (BOOL)tryMigrateUser:(MPUserEntity *)user inContext:(NSManagedObjectContext *)moc {
|
- (BOOL)tryMigrateUser:(MPUserEntity *)user inContext:(NSManagedObjectContext *)moc {
|
||||||
|
|
||||||
NSError *error = nil;
|
NSError *error = nil;
|
||||||
@ -107,12 +129,16 @@
|
|||||||
|
|
||||||
- (NSData *)keyDataForFullName:(NSString *)fullName withMasterPassword:(NSString *)masterPassword {
|
- (NSData *)keyDataForFullName:(NSString *)fullName withMasterPassword:(NSString *)masterPassword {
|
||||||
|
|
||||||
NSDate *start = [NSDate date];
|
__block NSData *keyData;
|
||||||
uint8_t const *masterKeyBytes = mpw_masterKeyForUser( fullName.UTF8String, masterPassword.UTF8String, [self version] );
|
[self mpw_perform:^{
|
||||||
NSData *keyData = [NSData dataWithBytes:masterKeyBytes length:MP_dkLen];
|
NSDate *start = [NSDate date];
|
||||||
trc( @"User: %@, password: %@ derives to key ID: %@ (took %0.2fs)", //
|
uint8_t const *masterKeyBytes = mpw_masterKeyForUser( fullName.UTF8String, masterPassword.UTF8String, [self version] );
|
||||||
fullName, masterPassword, [self keyIDForKeyData:keyData], -[start timeIntervalSinceNow] );
|
keyData = [NSData dataWithBytes:masterKeyBytes length:MP_dkLen];
|
||||||
mpw_free( masterKeyBytes, MP_dkLen );
|
trc( @"User: %@, password: %@ derives to key ID: %@ (took %0.2fs)", //
|
||||||
|
fullName, masterPassword, [self keyIDForKeyData:keyData], -[start timeIntervalSinceNow] );
|
||||||
|
mpw_free( masterKeyBytes, MP_dkLen );
|
||||||
|
}];
|
||||||
|
|
||||||
return keyData;
|
return keyData;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -317,10 +343,13 @@
|
|||||||
- (NSString *)generateContentForSiteNamed:(NSString *)name ofType:(MPSiteType)type withCounter:(NSUInteger)counter
|
- (NSString *)generateContentForSiteNamed:(NSString *)name ofType:(MPSiteType)type withCounter:(NSUInteger)counter
|
||||||
variant:(MPSiteVariant)variant context:(NSString *)context usingKey:(MPKey *)key {
|
variant:(MPSiteVariant)variant context:(NSString *)context usingKey:(MPKey *)key {
|
||||||
|
|
||||||
char const *contentBytes = mpw_passwordForSite( [key keyDataForAlgorithm:self].bytes,
|
__block NSString *content;
|
||||||
name.UTF8String, type, (uint32_t)counter, variant, context.UTF8String, [self version] );
|
[self mpw_perform:^{
|
||||||
NSString *content = [NSString stringWithCString:contentBytes encoding:NSUTF8StringEncoding];
|
char const *contentBytes = mpw_passwordForSite( [key keyDataForAlgorithm:self].bytes,
|
||||||
mpw_freeString( contentBytes );
|
name.UTF8String, type, (uint32_t)counter, variant, context.UTF8String, [self version] );
|
||||||
|
content = [NSString stringWithCString:contentBytes encoding:NSUTF8StringEncoding];
|
||||||
|
mpw_freeString( contentBytes );
|
||||||
|
}];
|
||||||
|
|
||||||
return content;
|
return content;
|
||||||
}
|
}
|
||||||
@ -382,7 +411,7 @@
|
|||||||
[PearlKeyChain deleteItemForQuery:siteQuery];
|
[PearlKeyChain deleteItemForQuery:siteQuery];
|
||||||
else
|
else
|
||||||
[PearlKeyChain addOrUpdateItemForQuery:siteQuery withAttributes:@{
|
[PearlKeyChain addOrUpdateItemForQuery:siteQuery withAttributes:@{
|
||||||
(__bridge id)kSecValueData : encryptedContent,
|
(__bridge id)kSecValueData : encryptedContent,
|
||||||
#if TARGET_OS_IPHONE
|
#if TARGET_OS_IPHONE
|
||||||
(__bridge id)kSecAttrAccessible : (__bridge id)kSecAttrAccessibleWhenUnlockedThisDeviceOnly,
|
(__bridge id)kSecAttrAccessible : (__bridge id)kSecAttrAccessibleWhenUnlockedThisDeviceOnly,
|
||||||
#endif
|
#endif
|
||||||
@ -562,7 +591,8 @@
|
|||||||
- (void)resolveAnswerForQuestion:(MPSiteQuestionEntity *)question usingKey:(MPKey *)siteKey
|
- (void)resolveAnswerForQuestion:(MPSiteQuestionEntity *)question usingKey:(MPKey *)siteKey
|
||||||
result:(void ( ^ )(NSString *result))resultBlock {
|
result:(void ( ^ )(NSString *result))resultBlock {
|
||||||
|
|
||||||
NSAssert( [[siteKey keyIDForAlgorithm:question.site.user.algorithm] isEqualToData:question.site.user.keyID], @"Site does not belong to current user." );
|
NSAssert( [[siteKey keyIDForAlgorithm:question.site.user.algorithm] isEqualToData:question.site.user.keyID],
|
||||||
|
@"Site does not belong to current user." );
|
||||||
NSString *name = question.site.name;
|
NSString *name = question.site.name;
|
||||||
NSString *keyword = question.keyword;
|
NSString *keyword = question.keyword;
|
||||||
id<MPAlgorithm> algorithm = nil;
|
id<MPAlgorithm> algorithm = nil;
|
||||||
@ -748,7 +778,7 @@
|
|||||||
|
|
||||||
if (strchr( charactersForClass, passwordCharacter )) {
|
if (strchr( charactersForClass, passwordCharacter )) {
|
||||||
// Found class for password character.
|
// Found class for password character.
|
||||||
characterEntropy = (BN_ULONG)strlen(charactersForClass);
|
characterEntropy = (BN_ULONG)strlen( charactersForClass );
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -190,14 +190,14 @@ PearlAssociatedObjectProperty( NSNumber*, StoreCorrupted, storeCorrupted );
|
|||||||
// When privateManagedObjectContext is saved, import the changes into mainManagedObjectContext.
|
// When privateManagedObjectContext is saved, import the changes into mainManagedObjectContext.
|
||||||
PearlAddNotificationObserverTo( self.mainManagedObjectContext, NSManagedObjectContextDidSaveNotification,
|
PearlAddNotificationObserverTo( self.mainManagedObjectContext, NSManagedObjectContextDidSaveNotification,
|
||||||
self.privateManagedObjectContext, nil, ^(NSManagedObjectContext *mainManagedObjectContext, NSNotification *note) {
|
self.privateManagedObjectContext, nil, ^(NSManagedObjectContext *mainManagedObjectContext, NSNotification *note) {
|
||||||
[mainManagedObjectContext performBlock:^{
|
[mainManagedObjectContext performBlock:^{
|
||||||
@try {
|
@try {
|
||||||
[mainManagedObjectContext mergeChangesFromContextDidSaveNotification:note];
|
[mainManagedObjectContext mergeChangesFromContextDidSaveNotification:note];
|
||||||
}
|
}
|
||||||
@catch (NSException *exception) {
|
@catch (NSException *exception) {
|
||||||
err( @"While merging changes:\n%@",[exception fullDescription] );
|
err( @"While merging changes:\n%@", [exception fullDescription] );
|
||||||
}
|
}
|
||||||
}];
|
}];
|
||||||
} );
|
} );
|
||||||
|
|
||||||
|
|
||||||
@ -821,10 +821,17 @@ PearlAssociatedObjectProperty( NSNumber*, StoreCorrupted, storeCorrupted );
|
|||||||
content = [site.algorithm exportPasswordForSite:site usingKey:self.key];
|
content = [site.algorithm exportPasswordForSite:site usingKey:self.key];
|
||||||
}
|
}
|
||||||
|
|
||||||
[export appendFormat:@"%@ %8ld %8s %25s\t%25s\t%@\n",
|
NSString *lastUsedExport = [[NSDateFormatter rfc3339DateFormatter] stringFromDate:lastUsed];
|
||||||
[[NSDateFormatter rfc3339DateFormatter] stringFromDate:lastUsed], (long)uses,
|
long usesExport = (long)uses;
|
||||||
[strf( @"%lu:%lu:%lu", (long)type, (long)[algorithm version], (long)counter ) UTF8String],
|
NSString *typeExport = strf( @"%lu:%lu:%lu", (long)type, (long)[algorithm version], (long)counter );
|
||||||
[(loginName?: @"") UTF8String], [siteName UTF8String], content?: @""];
|
NSString *loginNameExport = loginName?: @"";
|
||||||
|
NSString *contentExport = content?: @"";
|
||||||
|
[export appendFormat:@"%@ %8ld %8S %25S\t%25S\t%@\n",
|
||||||
|
lastUsedExport, usesExport,
|
||||||
|
(const unichar *)[typeExport cStringUsingEncoding:NSUTF16StringEncoding],
|
||||||
|
(const unichar *)[loginNameExport cStringUsingEncoding:NSUTF16StringEncoding],
|
||||||
|
(const unichar *)[siteName cStringUsingEncoding:NSUTF16StringEncoding],
|
||||||
|
contentExport];
|
||||||
}
|
}
|
||||||
|
|
||||||
return export;
|
return export;
|
||||||
|
@ -17,6 +17,7 @@
|
|||||||
//
|
//
|
||||||
|
|
||||||
#import <QuartzCore/QuartzCore.h>
|
#import <QuartzCore/QuartzCore.h>
|
||||||
|
#import <Foundation/Foundation.h>
|
||||||
#import "MPPasswordWindowController.h"
|
#import "MPPasswordWindowController.h"
|
||||||
#import "MPMacAppDelegate.h"
|
#import "MPMacAppDelegate.h"
|
||||||
#import "MPAppDelegate_Store.h"
|
#import "MPAppDelegate_Store.h"
|
||||||
@ -518,6 +519,7 @@
|
|||||||
|
|
||||||
- (void)updateSites {
|
- (void)updateSites {
|
||||||
|
|
||||||
|
NSAssert( [NSOperationQueue currentQueue] == [NSOperationQueue mainQueue], @"updateSites should be called on the main queue." );
|
||||||
if (![MPMacAppDelegate get].key) {
|
if (![MPMacAppDelegate get].key) {
|
||||||
self.sites = nil;
|
self.sites = nil;
|
||||||
return;
|
return;
|
||||||
@ -530,13 +532,18 @@
|
|||||||
} );
|
} );
|
||||||
|
|
||||||
NSString *queryString = self.siteField.stringValue;
|
NSString *queryString = self.siteField.stringValue;
|
||||||
NSString *queryPattern = [queryString stringByReplacingMatchesOfExpression:fuzzyRE withTemplate:@"*$1*"];
|
NSString *queryPattern;
|
||||||
|
if ([queryString length] < 13)
|
||||||
|
queryPattern = [queryString stringByReplacingMatchesOfExpression:fuzzyRE withTemplate:@"*$1*"];
|
||||||
|
else
|
||||||
|
// If query is too long, a wildcard per character makes the CoreData fetch take excessively long.
|
||||||
|
queryPattern = strf( @"*%@*", queryString );
|
||||||
NSMutableArray *fuzzyGroups = [NSMutableArray new];
|
NSMutableArray *fuzzyGroups = [NSMutableArray new];
|
||||||
[fuzzyRE enumerateMatchesInString:queryString options:0 range:NSMakeRange( 0, queryString.length )
|
[fuzzyRE enumerateMatchesInString:queryString options:0 range:NSMakeRange( 0, queryString.length )
|
||||||
usingBlock:^(NSTextCheckingResult *result, NSMatchingFlags flags, BOOL *stop) {
|
usingBlock:^(NSTextCheckingResult *result, NSMatchingFlags flags, BOOL *stop) {
|
||||||
[fuzzyGroups addObject:[queryString substringWithRange:result.range]];
|
[fuzzyGroups addObject:[queryString substringWithRange:result.range]];
|
||||||
}];
|
}];
|
||||||
[MPMacAppDelegate managedObjectContextPerformBlockAndWait:^(NSManagedObjectContext *context) {
|
[MPMacAppDelegate managedObjectContextPerformBlock:^(NSManagedObjectContext *context) {
|
||||||
NSFetchRequest *fetchRequest = [NSFetchRequest fetchRequestWithEntityName:NSStringFromClass( [MPSiteEntity class] )];
|
NSFetchRequest *fetchRequest = [NSFetchRequest fetchRequestWithEntityName:NSStringFromClass( [MPSiteEntity class] )];
|
||||||
fetchRequest.sortDescriptors = @[ [[NSSortDescriptor alloc] initWithKey:@"lastUsed" ascending:NO] ];
|
fetchRequest.sortDescriptors = @[ [[NSSortDescriptor alloc] initWithKey:@"lastUsed" ascending:NO] ];
|
||||||
fetchRequest.predicate = [NSPredicate predicateWithFormat:@"(%@ == '' OR name LIKE[cd] %@) AND user == %@",
|
fetchRequest.predicate = [NSPredicate predicateWithFormat:@"(%@ == '' OR name LIKE[cd] %@) AND user == %@",
|
||||||
@ -555,10 +562,16 @@
|
|||||||
[newSites addObject:[[MPSiteModel alloc] initWithEntity:site fuzzyGroups:fuzzyGroups]];
|
[newSites addObject:[[MPSiteModel alloc] initWithEntity:site fuzzyGroups:fuzzyGroups]];
|
||||||
exact |= [site.name isEqualToString:queryString];
|
exact |= [site.name isEqualToString:queryString];
|
||||||
}
|
}
|
||||||
if (!exact && [queryString length])
|
if (!exact && [queryString length]) {
|
||||||
[newSites addObject:[[MPSiteModel alloc] initWithName:queryString]];
|
MPUserEntity *activeUser = [[MPAppDelegate_Shared get] activeUserInContext:context];
|
||||||
|
[newSites addObject:[[MPSiteModel alloc] initWithName:queryString forUser:activeUser]];
|
||||||
|
}
|
||||||
|
|
||||||
dbg( @"newSites: %@", newSites );
|
dbg( @"newSites: %@", newSites );
|
||||||
self.sites = newSites;
|
if (![newSites isEqualToArray:self.sites])
|
||||||
|
PearlMainQueue( ^{
|
||||||
|
self.sites = newSites;
|
||||||
|
} );
|
||||||
}];
|
}];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -19,6 +19,7 @@
|
|||||||
#import <Foundation/Foundation.h>
|
#import <Foundation/Foundation.h>
|
||||||
#import "MPSiteEntity.h"
|
#import "MPSiteEntity.h"
|
||||||
#import "MPAlgorithm.h"
|
#import "MPAlgorithm.h"
|
||||||
|
#import "MPUserEntity.h"
|
||||||
|
|
||||||
@class MPSiteEntity;
|
@class MPSiteEntity;
|
||||||
|
|
||||||
@ -40,7 +41,7 @@
|
|||||||
@property (nonatomic, readonly) BOOL transient;
|
@property (nonatomic, readonly) BOOL transient;
|
||||||
|
|
||||||
- (instancetype)initWithEntity:(MPSiteEntity *)entity fuzzyGroups:(NSArray *)fuzzyGroups;
|
- (instancetype)initWithEntity:(MPSiteEntity *)entity fuzzyGroups:(NSArray *)fuzzyGroups;
|
||||||
- (instancetype)initWithName:(NSString *)siteName;
|
- (instancetype)initWithName:(NSString *)siteName forUser:(MPUserEntity *)user;
|
||||||
- (MPSiteEntity *)entityInContext:(NSManagedObjectContext *)moc;
|
- (MPSiteEntity *)entityInContext:(NSManagedObjectContext *)moc;
|
||||||
|
|
||||||
- (void)updateContent;
|
- (void)updateContent;
|
||||||
|
@ -39,12 +39,12 @@
|
|||||||
return self;
|
return self;
|
||||||
}
|
}
|
||||||
|
|
||||||
- (instancetype)initWithName:(NSString *)siteName {
|
- (instancetype)initWithName:(NSString *)siteName forUser:(MPUserEntity *)user {
|
||||||
|
|
||||||
if (!(self = [super init]))
|
if (!(self = [super init]))
|
||||||
return nil;
|
return nil;
|
||||||
|
|
||||||
[self setTransientSiteName:siteName];
|
[self setTransientSiteName:siteName forUser:user];
|
||||||
_initialized = YES;
|
_initialized = YES;
|
||||||
|
|
||||||
return self;
|
return self;
|
||||||
@ -84,7 +84,7 @@
|
|||||||
[self updateContent:entity];
|
[self updateContent:entity];
|
||||||
}
|
}
|
||||||
|
|
||||||
- (void)setTransientSiteName:(NSString *)siteName {
|
- (void)setTransientSiteName:(NSString *)siteName forUser:(MPUserEntity *)user {
|
||||||
|
|
||||||
_entityOID = nil;
|
_entityOID = nil;
|
||||||
|
|
||||||
@ -97,7 +97,7 @@
|
|||||||
self.name = siteName;
|
self.name = siteName;
|
||||||
self.algorithm = MPAlgorithmDefault;
|
self.algorithm = MPAlgorithmDefault;
|
||||||
self.lastUsed = nil;
|
self.lastUsed = nil;
|
||||||
self.type = [MPAppDelegate_Shared get].activeUserForMainThread.defaultType;
|
self.type = user.defaultType;
|
||||||
self.typeName = [self.algorithm nameOfType:self.type];
|
self.typeName = [self.algorithm nameOfType:self.type];
|
||||||
self.uses = @0;
|
self.uses = @0;
|
||||||
self.counter = 1;
|
self.counter = 1;
|
||||||
|
@ -392,7 +392,12 @@ typedef NS_OPTIONS( NSUInteger, MPPasswordsTips ) {
|
|||||||
} );
|
} );
|
||||||
|
|
||||||
NSString *queryString = self.query;
|
NSString *queryString = self.query;
|
||||||
NSString *queryPattern = [queryString stringByReplacingMatchesOfExpression:fuzzyRE withTemplate:@"*$1*"];
|
NSString *queryPattern;
|
||||||
|
if ([queryString length] < 13)
|
||||||
|
queryPattern = [queryString stringByReplacingMatchesOfExpression:fuzzyRE withTemplate:@"*$1*"];
|
||||||
|
else
|
||||||
|
// If query is too long, a wildcard per character makes the CoreData fetch take excessively long.
|
||||||
|
queryPattern = strf( @"*%@*", queryString );
|
||||||
NSMutableArray *fuzzyGroups = [NSMutableArray arrayWithCapacity:[queryString length]];
|
NSMutableArray *fuzzyGroups = [NSMutableArray arrayWithCapacity:[queryString length]];
|
||||||
[fuzzyRE enumerateMatchesInString:queryString options:0 range:NSMakeRange( 0, queryString.length )
|
[fuzzyRE enumerateMatchesInString:queryString options:0 range:NSMakeRange( 0, queryString.length )
|
||||||
usingBlock:^(NSTextCheckingResult *result, NSMatchingFlags flags, BOOL *stop) {
|
usingBlock:^(NSTextCheckingResult *result, NSMatchingFlags flags, BOOL *stop) {
|
||||||
|
@ -1 +1 @@
|
|||||||
Subproject commit 442e41896998e06e850b42f9f8ea4b33bb237bf1
|
Subproject commit 28a988ebe6d1b8052ecc5190b5f9a1fd658b6cf8
|
Loading…
Reference in New Issue
Block a user