From 0921cd39d28fc00d03664be5630317803e9c6a33 Mon Sep 17 00:00:00 2001 From: Michael Starke Date: Wed, 10 Oct 2018 19:23:45 +0200 Subject: [PATCH] compatibilty for plugins is now fetched from plugin repository --- MacPass.xcodeproj/project.pbxproj | 16 +++ MacPass/Base.lproj/PasswordEditWindow.xib | 2 +- MacPass/MPAppDelegate.m | 2 +- MacPass/MPDocument.m | 17 +-- MacPass/MPErrorRecoveryAttempter.m | 3 +- MacPass/MPPlugin.h | 3 +- MacPass/MPPlugin.m | 8 +- MacPass/MPPluginHost.h | 2 + MacPass/MPPluginHost.m | 30 +++- MacPass/MPPluginRepository.h | 6 +- MacPass/MPPluginRepository.m | 82 +++++++---- MacPass/MPPluginRepositoryItem.h | 20 ++- MacPass/MPPluginRepositoryItem.m | 58 +++++++- MacPass/MPPluginRepositoryItemVersionInfo.h | 8 ++ MacPass/MPPluginRepositoryItemVersionInfo.m | 61 ++++++-- MacPass/MPPluginSettingsController.m | 4 +- MacPass/MPPluginVersion.h | 31 ++++ MacPass/MPPluginVersion.m | 122 ++++++++++++++++ MacPassTests/MPTestPluginVersion.m | 150 ++++++++++++++++++++ 19 files changed, 559 insertions(+), 66 deletions(-) create mode 100644 MacPass/MPPluginVersion.h create mode 100644 MacPass/MPPluginVersion.m create mode 100644 MacPassTests/MPTestPluginVersion.m diff --git a/MacPass.xcodeproj/project.pbxproj b/MacPass.xcodeproj/project.pbxproj index e1fb4308..3fa40aa1 100644 --- a/MacPass.xcodeproj/project.pbxproj +++ b/MacPass.xcodeproj/project.pbxproj @@ -148,6 +148,8 @@ 4C6F228C19A4AA700012310C /* MPAutotypeDelay.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C6F228B19A4AA700012310C /* MPAutotypeDelay.m */; }; 4C701CBC178618A000581B88 /* 12_RemoteTemplate.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 4C701CBB178618A000581B88 /* 12_RemoteTemplate.pdf */; }; 4C7155D81A10DB6D00979307 /* IconSelection.xib in Resources */ = {isa = PBXBuildFile; fileRef = 4C7155DA1A10DB6D00979307 /* IconSelection.xib */; }; + 4C71BCB42167B75900B4CBDA /* MPTestPluginVersion.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C71BCB32167B75900B4CBDA /* MPTestPluginVersion.m */; }; + 4C71BCB72167B79C00B4CBDA /* MPPluginVersion.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C71BCB62167B79C00B4CBDA /* MPPluginVersion.m */; }; 4C735FC02035FCBF00708D53 /* MPPluginEntryActionContext.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C735FBF2035FCBF00708D53 /* MPPluginEntryActionContext.m */; }; 4C73B6F1215E64A7009787F7 /* MPWelcomeViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C73B6EF215E64A7009787F7 /* MPWelcomeViewController.m */; }; 4C76155C1764C04C0015A1A6 /* GeneralSettings.xib in Resources */ = {isa = PBXBuildFile; fileRef = 4C76155E1764C04C0015A1A6 /* GeneralSettings.xib */; }; @@ -183,6 +185,7 @@ 4C7BD07619FE94C900C7AA5C /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 4C7BD07519FE94C900C7AA5C /* Assets.xcassets */; }; 4C7F8B681A10B68400CCB83D /* WelcomeView.xib in Resources */ = {isa = PBXBuildFile; fileRef = 4C7F8B6A1A10B68400CCB83D /* WelcomeView.xib */; }; 4C80304A1E2FBAA300133E4C /* MPTestKeyMapper.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C8030491E2FBAA300133E4C /* MPTestKeyMapper.m */; }; + 4C81867D216664C70068DAFB /* MPPluginRepositoryItemVersionInfo.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C81867C216664C70068DAFB /* MPPluginRepositoryItemVersionInfo.m */; }; 4C82046A1FCDC07800EB24A4 /* MPPickfieldViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C8204681FCDC07800EB24A4 /* MPPickfieldViewController.m */; }; 4C82046E1FCDC8A100EB24A4 /* MPPickfieldTableModel.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C82046D1FCDC8A100EB24A4 /* MPPickfieldTableModel.m */; }; 4C83814215BF4677001AE468 /* MPDocumentWindowController.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C83814115BF4677001AE468 /* MPDocumentWindowController.m */; }; @@ -573,6 +576,9 @@ 4C7155E81A10DB7700979307 /* fr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fr; path = fr.lproj/IconSelection.strings; sourceTree = ""; }; 4C7155EA1A10DB7800979307 /* de */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = de; path = de.lproj/IconSelection.strings; sourceTree = ""; }; 4C7155EC1A10DB7900979307 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/IconSelection.strings; sourceTree = ""; }; + 4C71BCB32167B75900B4CBDA /* MPTestPluginVersion.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MPTestPluginVersion.m; sourceTree = ""; }; + 4C71BCB52167B79C00B4CBDA /* MPPluginVersion.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MPPluginVersion.h; sourceTree = ""; }; + 4C71BCB62167B79C00B4CBDA /* MPPluginVersion.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MPPluginVersion.m; sourceTree = ""; }; 4C735FBE2035FCBF00708D53 /* MPPluginEntryActionContext.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MPPluginEntryActionContext.h; sourceTree = ""; }; 4C735FBF2035FCBF00708D53 /* MPPluginEntryActionContext.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MPPluginEntryActionContext.m; sourceTree = ""; }; 4C73B6EE215E64A7009787F7 /* MPWelcomeViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MPWelcomeViewController.h; sourceTree = ""; }; @@ -643,6 +649,8 @@ 4C7F8B7A1A10B69700CCB83D /* de */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = de; path = de.lproj/WelcomeView.strings; sourceTree = ""; }; 4C7F8B7C1A10B69800CCB83D /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/WelcomeView.strings; sourceTree = ""; }; 4C8030491E2FBAA300133E4C /* MPTestKeyMapper.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MPTestKeyMapper.m; sourceTree = ""; }; + 4C81867B216664C70068DAFB /* MPPluginRepositoryItemVersionInfo.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MPPluginRepositoryItemVersionInfo.h; sourceTree = ""; }; + 4C81867C216664C70068DAFB /* MPPluginRepositoryItemVersionInfo.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MPPluginRepositoryItemVersionInfo.m; sourceTree = ""; }; 4C8204671FCDC07800EB24A4 /* MPPickfieldViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MPPickfieldViewController.h; sourceTree = ""; }; 4C8204681FCDC07800EB24A4 /* MPPickfieldViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MPPickfieldViewController.m; sourceTree = ""; }; 4C82046C1FCDC8A100EB24A4 /* MPPickfieldTableModel.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MPPickfieldTableModel.h; sourceTree = ""; }; @@ -1194,6 +1202,7 @@ 4C6BC65F1A36717E00BDDF3D /* MPDatabaseSearch.m */, 4C8030491E2FBAA300133E4C /* MPTestKeyMapper.m */, 4C8F0C721FCF1B7A00BE157F /* MPTestPickcharsParser.m */, + 4C71BCB32167B75900B4CBDA /* MPTestPluginVersion.m */, 4C45FB1F178E09ED0010007D /* Supporting Files */, ); path = MacPassTests; @@ -1663,8 +1672,12 @@ 4CA78BFF1FD58C92003C8560 /* MPPluginRepository.m */, 4CAD338D205169D30068587E /* MPPluginRepositoryItem.h */, 4CAD338E205169D30068587E /* MPPluginRepositoryItem.m */, + 4C81867B216664C70068DAFB /* MPPluginRepositoryItemVersionInfo.h */, + 4C81867C216664C70068DAFB /* MPPluginRepositoryItemVersionInfo.m */, 4C735FBE2035FCBF00708D53 /* MPPluginEntryActionContext.h */, 4C735FBF2035FCBF00708D53 /* MPPluginEntryActionContext.m */, + 4C71BCB52167B79C00B4CBDA /* MPPluginVersion.h */, + 4C71BCB62167B79C00B4CBDA /* MPPluginVersion.m */, ); name = Plugin; path = MacPass; @@ -1913,6 +1926,7 @@ files = ( 4C45FB2D178E0BCB0010007D /* MPDatabaseLoading.m in Sources */, 4C8DEAA21C314D2C00D24C32 /* MPTestAutotypeDelay.m in Sources */, + 4C71BCB42167B75900B4CBDA /* MPTestPluginVersion.m in Sources */, 4C8F0C731FCF1B7A00BE157F /* MPTestPickcharsParser.m in Sources */, 4C45FB30178E0CE20010007D /* MPTestDocument.m in Sources */, 4C6BC6601A36717E00BDDF3D /* MPDatabaseSearch.m in Sources */, @@ -1966,6 +1980,7 @@ 4C888C9316EB6F5E003D34A1 /* MPToolbarItem.m in Sources */, 4CA182741F963FF600DD4A4A /* MPTitlebarColorAccessoryViewController.m in Sources */, 4C888C9716EB754B003D34A1 /* MPActionHelper.m in Sources */, + 4C81867D216664C70068DAFB /* MPPluginRepositoryItemVersionInfo.m in Sources */, 4CDA35751EBA0CF2003CD59F /* NSString+MPComposedCharacterAdditions.m in Sources */, 4CE39ABF16ECE34A000FE29D /* MPIconSelectViewController.m in Sources */, 4CE39AC416ECE4F7000FE29D /* MPIconImageView.m in Sources */, @@ -2034,6 +2049,7 @@ 4C1FA07B18231900003A3F8C /* MPDocument+Autotype.m in Sources */, 4C4B7EE917A45EC6000234C7 /* MPDatePickingViewController.m in Sources */, 4C4B7EEE17A467E1000234C7 /* MPGroupInspectorViewController.m in Sources */, + 4C71BCB72167B79C00B4CBDA /* MPPluginVersion.m in Sources */, 4C7B63721C0CB51F00D7038C /* TTTCryptographyTransformers.m in Sources */, 4C4B7EF317A467FC000234C7 /* MPEntryInspectorViewController.m in Sources */, 4C1BDF2B1E4392640012A3F0 /* MPPluginDataViewController.m in Sources */, diff --git a/MacPass/Base.lproj/PasswordEditWindow.xib b/MacPass/Base.lproj/PasswordEditWindow.xib index eaa24110..a25beb4d 100644 --- a/MacPass/Base.lproj/PasswordEditWindow.xib +++ b/MacPass/Base.lproj/PasswordEditWindow.xib @@ -189,7 +189,7 @@ Gw - + diff --git a/MacPass/MPAppDelegate.m b/MacPass/MPAppDelegate.m index e8a61b41..eecae298 100644 --- a/MacPass/MPAppDelegate.m +++ b/MacPass/MPAppDelegate.m @@ -183,7 +183,7 @@ NSString *const MPDidChangeStoredKeyFilesSettings = @"com.hicknhack.macpass.MPDi [MPLockDaemon defaultDaemon]; [MPAutotypeDaemon defaultDaemon]; /* Create Plugin Manager */ - [MPPluginHost sharedHost]; + [MPPluginHost.sharedHost loadPlugins]; #if !defined(DEBUG) && !defined(NO_SPARKLE) /* Disable updates if in debug or nosparkle */ [SUUpdater sharedUpdater]; diff --git a/MacPass/MPDocument.m b/MacPass/MPDocument.m index e9c0538b..11572ea6 100644 --- a/MacPass/MPDocument.m +++ b/MacPass/MPDocument.m @@ -196,17 +196,6 @@ NSString *const MPDocumentGroupKey = @"MPDocumentGrou } - (BOOL)readFromURL:(NSURL *)url ofType:(NSString *)typeName error:(NSError **)outError { - /* FIXME: Lockfile handling - self.lockFileURL = [url URLByAppendingPathExtension:@"lock"]; - if([[NSFileManager defaultManager] fileExistsAtPath:[_lockFileURL path]]) { - self.readOnly = YES; - } - else { - [[NSFileManager defaultManager] createFileAtPath:[_lockFileURL path] contents:nil attributes:nil]; - _didLockFile = YES; - self.readOnly = NO; - } - */ /* Delete our old Tree, and just grab the data */ @@ -939,7 +928,7 @@ NSString *const MPDocumentGroupKey = @"MPDocumentGrou - (void)_cleanupLock { if(_didLockFile) { - [[NSFileManager defaultManager] removeItemAtURL:_lockFileURL error:nil]; + [NSFileManager.defaultManager removeItemAtURL:_lockFileURL error:nil]; _didLockFile = NO; } } @@ -971,11 +960,11 @@ NSString *const MPDocumentGroupKey = @"MPDocumentGrou #pragma mark MPModelChangeObserving - (void)willChangeModelProperty { - [[NSNotificationCenter defaultCenter] postNotificationName:MPDocumentWillChangeModelPropertyNotification object:self]; + [NSNotificationCenter.defaultCenter postNotificationName:MPDocumentWillChangeModelPropertyNotification object:self]; } - (void)didChangeModelProperty { - [[NSNotificationCenter defaultCenter] postNotificationName:MPDocumentDidChangeModelPropertyNotification object:self]; + [NSNotificationCenter.defaultCenter postNotificationName:MPDocumentDidChangeModelPropertyNotification object:self]; } @end diff --git a/MacPass/MPErrorRecoveryAttempter.m b/MacPass/MPErrorRecoveryAttempter.m index 201f869c..02947bfc 100644 --- a/MacPass/MPErrorRecoveryAttempter.m +++ b/MacPass/MPErrorRecoveryAttempter.m @@ -37,7 +37,7 @@ NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:[delegate methodSignatureForSelector:didRecoverSelector]]; __block void *contextInfoCopy = contextInfo; if(error.code == MPErrorNoPasswordOrKeyFile) { - if([delegate isKindOfClass:[MPDocument class]]) { + if([delegate isKindOfClass:MPDocument.class]) { MPDocument *document = delegate; BOOL didRecover = NO; if(recoveryOptionIndex == 0) { @@ -59,7 +59,6 @@ } } } - } /* Given that an error alert has been presented applicaton-modally to the user, and the user has chosen one of the error's recovery options, attempt recovery from the error, and return YES if error recovery was completely successful, NO otherwise. The recovery option index is an index into the error's array of localized recovery options. diff --git a/MacPass/MPPlugin.h b/MacPass/MPPlugin.h index 77fd1ac9..526c0ab4 100644 --- a/MacPass/MPPlugin.h +++ b/MacPass/MPPlugin.h @@ -35,7 +35,8 @@ FOUNDATION_EXPORT NSString *const MPPluginUnkownVersion; @property (copy, readonly) NSString *identifier; @property (copy, readonly) NSString *name; -@property (copy, readonly) NSString *version; +@property (nonatomic, copy, readonly, nullable) NSString *humanVersionString; +@property (nonatomic, copy, readonly) NSString *versionString; @property (nonatomic, strong, readonly) NSBundle *bundle; /** diff --git a/MacPass/MPPlugin.m b/MacPass/MPPlugin.m index 34516877..83305309 100644 --- a/MacPass/MPPlugin.m +++ b/MacPass/MPPlugin.m @@ -68,9 +68,13 @@ NSString *const MPPluginUnkownVersion = @"unkown.plugin.version"; return nil == name ? @"Unkown Plugin" : name; } -- (NSString *)version { +- (NSString *)humanVersionString { + return self.bundle.infoDictionary[@"CFBundleShortVersionString"]; +} + +- (NSString *)versionString { if(self.bundle) { - NSString *humanVersion = self.bundle.infoDictionary[@"CFBundleShortVersionString"]; + NSString *humanVersion = self.humanVersionString; NSString *version = self.bundle.infoDictionary[(NSString *)kCFBundleVersionKey]; if(humanVersion && version) { return [NSString stringWithFormat:@"%@ (%@)", humanVersion, version]; diff --git a/MacPass/MPPluginHost.h b/MacPass/MPPluginHost.h index 673adb39..8d47f1b6 100644 --- a/MacPass/MPPluginHost.h +++ b/MacPass/MPPluginHost.h @@ -48,6 +48,8 @@ FOUNDATION_EXPORT NSString *const MPPluginHostPluginBundleIdentifiyerKey; - (void)disablePlugin:(MPPlugin *)plugin; - (void)enablePlugin:(MPPlugin *)plugin; +- (void)loadPlugins; + /* - (NSArray *)autotypePlugins; - (NSArray *)entryContextMenuPlugins; diff --git a/MacPass/MPPluginHost.m b/MacPass/MPPluginHost.m index 3af47c29..8a9e85ed 100644 --- a/MacPass/MPPluginHost.m +++ b/MacPass/MPPluginHost.m @@ -26,6 +26,8 @@ #import "MPPlugin_Private.h" #import "MPPluginConstants.h" #import "MPPluginEntryActionContext.h" +#import "MPPluginRepository.h" +#import "MPPluginRepositoryItem.h" #import "NSApplication+MPAdditions.h" #import "MPSettingsHelper.h" @@ -79,8 +81,6 @@ NSString *const MPPluginHostPluginBundleIdentifiyerKey = @"MPPluginHostPluginBun _entryActionPluginIdentifiers = [[NSMutableArray alloc] init]; _customAttributePluginIdentifiers = [[NSMutableArray alloc] init]; - [self _loadPlugins]; - [self bind:NSStringFromSelector(@selector(loadUnsecurePlugins)) toObject:NSUserDefaultsController.sharedUserDefaultsController withKeyPath:[MPSettingsHelper defaultControllerPathForKey:kMPSettingsKeyLoadUnsecurePlugins] @@ -94,7 +94,7 @@ NSString *const MPPluginHostPluginBundleIdentifiyerKey = @"MPPluginHostPluginBun } - (NSString *)version { - NSString *version = NSBundle.mainBundle.infoDictionary[(NSString *)kCFBundleVersionKey]; + NSString *version = NSBundle.mainBundle.infoDictionary[@"CFBundleShortVersionString"]; return version; } @@ -131,7 +131,13 @@ NSString *const MPPluginHostPluginBundleIdentifiyerKey = @"MPPluginHostPluginBun #pragma mark - Plugin Loading -- (void)_loadPlugins { +- (void)loadPlugins { + [MPPluginRepository.defaultRepository fetchRepositoryDataCompletionHandler:^(NSArray * _Nonnull availablePlugins) { + [self _loadPlugins:availablePlugins]; + }]; +} + +- (void)_loadPlugins:(NSArray *)availablePlugins { NSURL *appSupportDir = [NSApp applicationSupportDirectoryURL:YES]; NSError *error; NSLog(@"Looking for external plugins at %@.", appSupportDir.path); @@ -190,6 +196,11 @@ NSString *const MPPluginHostPluginBundleIdentifiyerKey = @"MPPluginHostPluginBun continue; } + if(![self _isCompatiblePluginBundle:pluginBundle avaiablePlugins:availablePlugins ]) { + [self _addPluginForBundle:pluginBundle error:NSLocalizedString(@"PLUGIN_ERROR_HOST_VERSION_NOT_SUPPORTED", "Plugin is not with this version of MacPass")]; + continue; + } + if(![pluginBundle loadAndReturnError:&error]) { NSLog(@"Bundle Loading Error %@ %@", error.localizedDescription, error.localizedFailureReason); [self _addPluginForBundle:pluginBundle error:error.localizedDescription]; @@ -249,6 +260,17 @@ NSString *const MPPluginHostPluginBundleIdentifiyerKey = @"MPPluginHostPluginBun return NO; } +- (BOOL)_isCompatiblePluginBundle:(NSBundle *)bundle avaiablePlugins:(NSArray *)availablePlugins { + MPPluginRepositoryItem *repoItem; + for(MPPluginRepositoryItem *item in availablePlugins) { + if([item.bundleIdentifier isEqualToString:bundle.bundleIdentifier]) { + repoItem = item; + } + } + NSString *shortVersion = bundle.infoDictionary[@"CFBundleShortVersionString"]; + return [repoItem isPluginVersion:shortVersion compatibleWithHost:self]; +} + - (BOOL)_isValidPluginURL:(NSURL *)url { return (NSOrderedSame == [url.pathExtension compare:MPPluginFileExtension options:NSCaseInsensitiveSearch]); } diff --git a/MacPass/MPPluginRepository.h b/MacPass/MPPluginRepository.h index 8bdb4eb6..8a81dac4 100644 --- a/MacPass/MPPluginRepository.h +++ b/MacPass/MPPluginRepository.h @@ -20,6 +20,8 @@ // along with this program. If not, see . // +NS_ASSUME_NONNULL_BEGIN + @import Foundation; @class MPPluginRepositoryItem; @@ -27,7 +29,9 @@ @interface MPPluginRepository : NSObject @property (class, strong, readonly) MPPluginRepository *defaultRepository; -@property (nonatomic, copy) NSArray *availablePlugins; +- (void)fetchRepositoryDataCompletionHandler:(void (^)(NSArray *availablePlugins))completionHandler; @end + +NS_ASSUME_NONNULL_END diff --git a/MacPass/MPPluginRepository.m b/MacPass/MPPluginRepository.m index 676b21d0..392404b0 100644 --- a/MacPass/MPPluginRepository.m +++ b/MacPass/MPPluginRepository.m @@ -24,9 +24,16 @@ #import "MPConstants.h" #import "MPPluginRepositoryItem.h" -@implementation MPPluginRepository +const NSTimeInterval MPPluginRepositoryCacheTimeOut = 60*3; // 1 Minute cache time -@dynamic availablePlugins; +@interface MPPluginRepository () + +@property NSTimeInterval lastPluginCheckTime; +@property BOOL didLoadData; + +@end + +@implementation MPPluginRepository + (instancetype)defaultRepository { static MPPluginRepository *instance; @@ -39,38 +46,65 @@ - (instancetype)init { self = [super init]; + if(self) { + self.lastPluginCheckTime = NSDate.distantPast.timeIntervalSinceReferenceDate; + } return self; } -- (NSArray *)availablePlugins { +- (void)fetchRepositoryDataCompletionHandler:(void (^)(NSArray * _Nonnull))completionHandler { NSString *urlString = NSBundle.mainBundle.infoDictionary[MPBundlePluginRepositoryURLKey]; if(!urlString) { - return @[]; + if(completionHandler) { + completionHandler(@[]); + } + return; } NSURL *jsonURL = [NSURL URLWithString:urlString]; if(!jsonURL) { - return @[]; - } - NSError *error; - NSData *jsonData = [NSData dataWithContentsOfURL:jsonURL options:0 error:&error]; - if(!jsonData) { - return @[]; - } - id jsonRoot = [NSJSONSerialization JSONObjectWithData:jsonData options:0 error:&error]; - if(!jsonRoot || ![jsonRoot isKindOfClass:NSArray.class]) { - return @[]; - } - NSMutableArray *items = [[NSMutableArray alloc] init]; - for(id item in jsonRoot) { - if(![item isKindOfClass:NSDictionary.class]) { - continue; - } - MPPluginRepositoryItem *pluginItem = [MPPluginRepositoryItem pluginItemFromDictionary:item]; - if(pluginItem.isVaid) { - [items addObject:pluginItem]; + if(completionHandler) { + completionHandler(@[]); } + return; } - return [items copy]; + + NSURLSessionTask *downloadTask = [NSURLSession.sharedSession dataTaskWithURL:jsonURL completionHandler:^(NSData * _Nullable jsonData, NSURLResponse * _Nullable response, NSError * _Nullable error) { + if(![response isKindOfClass:NSHTTPURLResponse.class]) { + if(completionHandler) { + completionHandler(@[]); + } + return; + } + NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)response; + if(httpResponse.statusCode != 200 || jsonData.length == 0) { + if(completionHandler) { + completionHandler(@[]); + } + return; + } + id jsonRoot = [NSJSONSerialization JSONObjectWithData:jsonData options:0 error:&error]; + if(!jsonRoot || ![jsonRoot isKindOfClass:NSArray.class]) { + if(completionHandler) { + completionHandler(@[]); + } + return; + } + NSMutableArray *items = [[NSMutableArray alloc] init]; + for(id item in jsonRoot) { + if(![item isKindOfClass:NSDictionary.class]) { + continue; + } + MPPluginRepositoryItem *pluginItem = [MPPluginRepositoryItem pluginItemFromDictionary:item]; + if(pluginItem.isVaid) { + [items addObject:pluginItem]; + } + } + if(completionHandler) { + completionHandler([items copy]); + } + }]; + + [downloadTask resume]; } @end diff --git a/MacPass/MPPluginRepositoryItem.h b/MacPass/MPPluginRepositoryItem.h index 338b6e48..cfb590e9 100644 --- a/MacPass/MPPluginRepositoryItem.h +++ b/MacPass/MPPluginRepositoryItem.h @@ -22,18 +22,26 @@ #import +NS_ASSUME_NONNULL_BEGIN + +@class MPPluginHost; + @interface MPPluginRepositoryItem : NSObject -@property (copy,readonly) NSString *name; -@property (copy,readonly) NSString *currentVersion; -@property (copy,readonly) NSString *descriptionText; -@property (copy,readonly) NSURL *sourceURL; -@property (copy,readonly) NSURL *downloadURL; -@property (copy,readonly) NSURL *bundleIdentifier; + +@property (copy,readonly, nullable) NSString *name; +@property (copy,readonly, nullable) NSString *currentVersion; +@property (copy,readonly, nullable) NSString *descriptionText; +@property (copy,readonly, nullable) NSURL *sourceURL; +@property (copy,readonly, nullable) NSURL *downloadURL; +@property (copy,readonly, nullable) NSString *bundleIdentifier; @property (readonly, nonatomic, getter=isVaid) BOOL valid; + (instancetype)pluginItemFromDictionary:(NSDictionary *)dict; - (instancetype)initWithDictionary:(NSDictionary *)dict; +- (BOOL)isPluginVersion:(NSString * _Nullable )pluginVersionString compatibleWithHost:(MPPluginHost *)host; @end + +NS_ASSUME_NONNULL_END diff --git a/MacPass/MPPluginRepositoryItem.m b/MacPass/MPPluginRepositoryItem.m index 66ab9027..ceb757d2 100644 --- a/MacPass/MPPluginRepositoryItem.m +++ b/MacPass/MPPluginRepositoryItem.m @@ -21,6 +21,11 @@ // #import "MPPluginRepositoryItem.h" +#import "MPPluginRepositoryItemVersionInfo.h" +#import "MPPluginVersion.h" +#import "MPPluginHost.h" +#import "MPPlugin.h" +#import "NSError+Messages.h" NSString *const MPPluginItemNameKey = @"name"; @@ -29,6 +34,7 @@ NSString *const MPPluginItemDownloadURLKey = @"download"; NSString *const MPPluginItemSourceURLKey = @"source"; NSString *const MPPluginItemCurrentVersionKey = @"currentVersion"; NSString *const MPPluginItemBundleIdentifierKey = @"bundleIdentifier"; +NSString *const MPPluginItemCompatibiltyKey = @"compatibilty"; @interface MPPluginRepositoryItem () @@ -37,7 +43,9 @@ NSString *const MPPluginItemBundleIdentifierKey = @"bundleIdentifier"; @property (copy) NSString *descriptionText; @property (copy) NSURL *sourceURL; @property (copy) NSURL *downloadURL; -@property (copy) NSURL *bundleIdentifier; +@property (copy) NSString *bundleIdentifier; +@property (copy) NSArray *compatibilty; + @end @@ -58,6 +66,8 @@ NSString *const MPPluginItemBundleIdentifierKey = @"bundleIdentifier"; self.sourceURL = [NSURL URLWithString:dict[MPPluginItemSourceURLKey]]; self.currentVersion = dict[MPPluginItemCurrentVersionKey]; self.bundleIdentifier = dict[MPPluginItemBundleIdentifierKey]; + [self _buildVersionInfos:dict[MPPluginItemCompatibiltyKey]]; + } return self; } @@ -67,4 +77,50 @@ NSString *const MPPluginItemBundleIdentifierKey = @"bundleIdentifier"; return (self.name.length > 0 && self.downloadURL); } +- (BOOL)isPluginVersion:(NSString *)pluginVersionString compatibleWithHost:(MPPluginHost *)host { + if(pluginVersionString.length == 0) { + return NO; + } + MPPluginVersion *pluginVersion = [MPPluginVersion versionWithVersionString:pluginVersionString]; + if(!pluginVersion) { + return NO; + } + if(host.version.length == 0) { + return NO; + } + + MPPluginVersion *hostVersion = [MPPluginVersion versionWithVersionString:host.version]; + if(!hostVersion) { + return NO; + } + + NSMutableArray *matches = [[NSMutableArray alloc] init]; + for(MPPluginRepositoryItemVersionInfo *info in self.compatibilty) { + if(NSOrderedSame == [info.version compare:pluginVersion]) { + [matches addObject:info]; + } + } + + if(matches.count != 1) { + return NO; + } + + MPPluginRepositoryItemVersionInfo *matchingInfo = matches.firstObject; + return [matchingInfo isCompatibleWithHostVersion:hostVersion]; +} + +- (void)_buildVersionInfos:(NSArray*)infos { + NSMutableArray *tmp = [[NSMutableArray alloc] init]; + for(NSDictionary *dict in infos) { + if(![dict isKindOfClass:NSDictionary.class]) { + continue; + } + MPPluginRepositoryItemVersionInfo *info = [MPPluginRepositoryItemVersionInfo versionInfoWithDict:dict]; + if(info){ + [tmp addObject:info]; + } + } + self.compatibilty = [tmp copy]; +} + @end diff --git a/MacPass/MPPluginRepositoryItemVersionInfo.h b/MacPass/MPPluginRepositoryItemVersionInfo.h index da90c23e..cda567d1 100644 --- a/MacPass/MPPluginRepositoryItemVersionInfo.h +++ b/MacPass/MPPluginRepositoryItemVersionInfo.h @@ -10,11 +10,19 @@ NS_ASSUME_NONNULL_BEGIN +@class MPPluginVersion; + @interface MPPluginRepositoryItemVersionInfo : NSObject +@property (copy, readonly) MPPluginVersion *version; + ++ (instancetype)versionInfoWithDict:(NSDictionary *)dict; + - (instancetype)initWithDict:(NSDictionary *)dict NS_DESIGNATED_INITIALIZER; - (instancetype)init NS_UNAVAILABLE; +- (BOOL)isCompatibleWithHostVersion:(MPPluginVersion *)hostVersion; + @end NS_ASSUME_NONNULL_END diff --git a/MacPass/MPPluginRepositoryItemVersionInfo.m b/MacPass/MPPluginRepositoryItemVersionInfo.m index 0c33ee25..217d7328 100644 --- a/MacPass/MPPluginRepositoryItemVersionInfo.m +++ b/MacPass/MPPluginRepositoryItemVersionInfo.m @@ -7,30 +7,75 @@ // #import "MPPluginRepositoryItemVersionInfo.h" +#import "MPPluginVersion.h" NSString *const MPPluginItemCompatibiltyVersionKey = @"pluginVersion"; NSString *const MPPluginItemCompatibiltyMinimumHostVersionKey = @"minimumHostVersion"; -NSString *const MPPluginItemCompatibiltyMaxiumumHostVersionKey = @"maxiumumHostVersion"; +NSString *const MPPluginItemCompatibiltyMaxiumumHostVersionKey = @"maximumHostVersion"; @interface MPPluginRepositoryItemVersionInfo () -@property (copy) NSString *version; -@property (copy) NSString *minimumHostVersion; -@property (copy) NSString *maxiumHostVersion; - +@property (copy) MPPluginVersion *version; +@property (copy) MPPluginVersion *minimumHostVersion; +@property (copy) MPPluginVersion *maxiumHostVersion; @end @implementation MPPluginRepositoryItemVersionInfo ++ (instancetype)versionInfoWithDict:(NSDictionary *)dict { + return [[MPPluginRepositoryItemVersionInfo alloc] initWithDict:dict]; +} + - (instancetype)initWithDict:(NSDictionary *)dict { self = [super init]; if(self) { - self.version = dict[MPPluginItemCompatibiltyVersionKey]; - self.minimumHostVersion = dict[MPPluginItemCompatibiltyMinimumHostVersionKey]; - self.maxiumHostVersion = dict[MPPluginItemCompatibiltyMaxiumumHostVersionKey]; + NSString *versionString = dict[MPPluginItemCompatibiltyVersionKey]; + if(!versionString) { + NSLog(@"Version information is missing required %@ key.", MPPluginItemCompatibiltyVersionKey); + self = nil; + return self; + } + self.version = [MPPluginVersion versionWithVersionString:versionString]; + if(!self.version) { + NSLog(@"Malformed plugin version information: %@.", versionString); + self = nil; + return self; + } + NSString *minimumHostVersionString = dict[MPPluginItemCompatibiltyMinimumHostVersionKey]; + if(!minimumHostVersionString) { + NSLog(@"Version information is missing required %@ key.", MPPluginItemCompatibiltyMinimumHostVersionKey); + self = nil; + return self; + } + self.minimumHostVersion = [MPPluginVersion versionWithVersionString:minimumHostVersionString]; + if(!self.minimumHostVersion) { + NSLog(@"Malformed minimum host version information: %@.", minimumHostVersionString); + self = nil; + return self; + } + NSString *maxiumHostVersionString = dict[MPPluginItemCompatibiltyMaxiumumHostVersionKey]; + if(maxiumHostVersionString) { + self.maxiumHostVersion = [MPPluginVersion versionWithVersionString:maxiumHostVersionString]; + if(!self.maxiumHostVersion) { + NSLog(@"Malformed maxium host version information: %@.", maxiumHostVersionString); + self = nil; + return self; + } + } } return self; } +- (BOOL)isCompatibleWithHostVersion:(MPPluginVersion *)hostVersion { + if(NSOrderedDescending == [self.minimumHostVersion compare:hostVersion]) { + return NO; + } + if(!self.maxiumHostVersion) { + return YES; + } + return (NSOrderedAscending != [self.maxiumHostVersion compare:hostVersion]); +} + + @end diff --git a/MacPass/MPPluginSettingsController.m b/MacPass/MPPluginSettingsController.m index 9c2c2067..54842b58 100644 --- a/MacPass/MPPluginSettingsController.m +++ b/MacPass/MPPluginSettingsController.m @@ -26,6 +26,8 @@ #import "MPPlugin.h" #import "MPPlugin_Private.h" #import "MPPluginConstants.h" +#import "MPPluginRepository.h" +#import "MPPluginRepositoryItem.h" #import "MPConstants.h" #import "MPSettingsHelper.h" @@ -96,7 +98,7 @@ typedef NS_ENUM(NSUInteger, MPPluginSegmentType) { ? [NSString stringWithFormat:NSLocalizedString(@"PLUGIN_NAME_ERROR_%@", "Name for unloaded plugin with errors"), plugin.name] : [NSString stringWithFormat:NSLocalizedString(@"PLUGIN_NAME_DISABLED_%@", "name for disabled unloaded plugin"), plugin.name]); } - view.addionalTextField.stringValue = [NSString stringWithFormat:NSLocalizedString(@"PLUGIN_VERSION_%@", "Plugin version. Include a %@ placeholder for version string"), plugin.version]; + view.addionalTextField.stringValue = [NSString stringWithFormat:NSLocalizedString(@"PLUGIN_VERSION_%@", "Plugin version. Include a %@ placeholder for version string"), plugin.versionString]; return view; } diff --git a/MacPass/MPPluginVersion.h b/MacPass/MPPluginVersion.h new file mode 100644 index 00000000..862e3eba --- /dev/null +++ b/MacPass/MPPluginVersion.h @@ -0,0 +1,31 @@ +// +// MPPluginVersion.h +// MacPass +// +// Created by Michael Starke on 05.10.18. +// Copyright © 2018 HicknHack Software GmbH. All rights reserved. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +FOUNDATION_EXPORT NSString *MPPluginVersionWildcard; + +@interface MPPluginVersion : NSObject + +@property (nonatomic, copy, readonly) NSString *versionString; +@property (nonatomic, copy, readonly) NSString *mayorVersion; +@property (nonatomic, copy, readonly) NSString *minorVersion; +@property (nonatomic, copy, readonly) NSString *patchVersion; + ++ (instancetype)versionWithVersionString:(NSString *)versionString; + +- (instancetype)initWithVersionString:(NSString *)versionString NS_DESIGNATED_INITIALIZER; +- (instancetype)init NS_UNAVAILABLE; + +- (NSComparisonResult)compare:(MPPluginVersion *)version; + +@end + +NS_ASSUME_NONNULL_END diff --git a/MacPass/MPPluginVersion.m b/MacPass/MPPluginVersion.m new file mode 100644 index 00000000..30ca2cfb --- /dev/null +++ b/MacPass/MPPluginVersion.m @@ -0,0 +1,122 @@ +// +// MPPluginVersion.m +// MacPass +// +// Created by Michael Starke on 05.10.18. +// Copyright © 2018 HicknHack Software GmbH. All rights reserved. +// + +#import "MPPluginVersion.h" + +NSString *MPPluginVersionWildcard = @"*"; + +@interface MPPluginVersion () + +@property (nonatomic, copy) NSString *versionString; +@property (nonatomic, copy) NSString *mayorVersion; +@property (nonatomic, copy) NSString *minorVersion; +@property (nonatomic, copy) NSString *patchVersion; + +@end + +@implementation MPPluginVersion + ++ (instancetype)versionWithVersionString:(NSString *)versionString { + return [[MPPluginVersion alloc] initWithVersionString:versionString]; +} + +- (instancetype)initWithVersionString:(NSString *)versionString { + self = [super init]; + if(self) { + self.mayorVersion = @"0"; + self.minorVersion = @"0"; + self.patchVersion = @"0"; + + NSArray* components = [versionString componentsSeparatedByString:@"."]; + if(components.count >= 1) { + + NSString *mayorVersion = components[0].length > 0 ? components[0] : @"0"; + if([mayorVersion isEqualToString:MPPluginVersionWildcard]) { + self.mayorVersion = MPPluginVersionWildcard; + self.minorVersion = MPPluginVersionWildcard; + self.patchVersion = MPPluginVersionWildcard; + self.versionString = [NSString stringWithFormat:@"%@.%@.%@", self.mayorVersion, self.minorVersion, self.patchVersion]; + return self; + } + NSCharacterSet *invalidSet = NSCharacterSet.decimalDigitCharacterSet.invertedSet; + NSRange mayorRange = [mayorVersion rangeOfCharacterFromSet:invalidSet]; + if(mayorRange.location != NSNotFound) { + NSLog(@"Invalid Format for Mayor Version"); + self = nil; + return self; + } + self.mayorVersion = mayorVersion; + + if(components.count >= 2) { + NSString *minorVersion = components[1].length > 0 ? components[1] : @"0"; + if([minorVersion isEqualToString:MPPluginVersionWildcard]) { + self.minorVersion = MPPluginVersionWildcard; + self.patchVersion = MPPluginVersionWildcard; + self.versionString = [NSString stringWithFormat:@"%@.%@.%@", self.mayorVersion, self.minorVersion, self.patchVersion]; + return self; + } + NSRange minorRange = [minorVersion rangeOfCharacterFromSet:invalidSet]; + if(minorRange.location != NSNotFound) { + NSLog(@"Invalid Format for Minor Version"); + self.versionString = [NSString stringWithFormat:@"%@.%@.%@", self.mayorVersion, self.minorVersion, self.patchVersion]; + self = nil; + return self; + } + self.minorVersion = minorVersion; + + if(components.count == 3) { + NSString *patchVersion = components[2].length > 0 ? components[2] : @"0"; + if([patchVersion isEqualToString:MPPluginVersionWildcard]) { + self.patchVersion = MPPluginVersionWildcard; + self.versionString = [NSString stringWithFormat:@"%@.%@.%@", self.mayorVersion, self.minorVersion, self.patchVersion]; + return self; + } + NSRange patchRange = [patchVersion rangeOfCharacterFromSet:invalidSet]; + if(patchRange.location != NSNotFound) { + NSLog(@"Invalid Format for Patch Version"); + self = nil; + return self; + } + self.patchVersion = patchVersion; + } + } + } + self.versionString = [NSString stringWithFormat:@"%@.%@.%@", self.mayorVersion, self.minorVersion, self.patchVersion]; + } + return self; +} + +- (id)copyWithZone:(NSZone *)zone { + return self; +} + +- (NSComparisonResult)compare:(MPPluginVersion *)version { + if([self.versionString isEqualToString:version.versionString]) { + return NSOrderedSame; + } + + NSArray *myVersions = @[self.mayorVersion, self.minorVersion, self.patchVersion]; + NSArray *otherVersions = @[version.mayorVersion, version.minorVersion, version.patchVersion]; + + for(NSUInteger index = 0; index < 3; index++) { + NSString *myVersion = myVersions[index]; + NSString *otherVersion = otherVersions[index]; + + if([myVersion isEqualToString:MPPluginVersionWildcard] || [otherVersion isEqualToString:MPPluginVersionWildcard]) { + return NSOrderedSame; + } + + NSComparisonResult compare = [myVersion compare:otherVersion options:NSNumericSearch|NSCaseInsensitiveSearch]; + if(compare != NSOrderedSame) { + return compare; + } + } + return NSOrderedSame; +} + +@end diff --git a/MacPassTests/MPTestPluginVersion.m b/MacPassTests/MPTestPluginVersion.m new file mode 100644 index 00000000..2fe73a26 --- /dev/null +++ b/MacPassTests/MPTestPluginVersion.m @@ -0,0 +1,150 @@ +// +// MPTestPluginVersion.m +// MacPassTests +// +// Created by Michael Starke on 05.10.18. +// Copyright © 2018 HicknHack Software GmbH. All rights reserved. +// + +#import + +#import "MPPluginVersion.h" + +@interface MPTestPluginVersion : XCTestCase + +@end + +@implementation MPTestPluginVersion + +- (void)testVersionExtraction { + MPPluginVersion *version = [[MPPluginVersion alloc] initWithVersionString:@"1."]; + XCTAssertEqualObjects(@"1", version.mayorVersion); + XCTAssertEqualObjects(@"0", version.minorVersion); + XCTAssertEqualObjects(@"0", version.patchVersion); + XCTAssertEqualObjects(@"1.0.0", version.versionString); + + version = [[MPPluginVersion alloc] initWithVersionString:@"0.5"]; + XCTAssertEqualObjects(@"0", version.mayorVersion); + XCTAssertEqualObjects(@"5", version.minorVersion); + XCTAssertEqualObjects(@"0", version.patchVersion); + XCTAssertEqualObjects(@"0.5.0", version.versionString); + + version = [[MPPluginVersion alloc] initWithVersionString:@".5"]; + XCTAssertEqualObjects(@"0", version.mayorVersion); + XCTAssertEqualObjects(@"5", version.minorVersion); + XCTAssertEqualObjects(@"0", version.patchVersion); + XCTAssertEqualObjects(@"0.5.0", version.versionString); + + version = [[MPPluginVersion alloc] initWithVersionString:@"5"]; + XCTAssertEqualObjects(@"5", version.mayorVersion); + XCTAssertEqualObjects(@"0", version.minorVersion); + XCTAssertEqualObjects(@"0", version.patchVersion); + XCTAssertEqualObjects(@"5.0.0", version.versionString); + + version = [[MPPluginVersion alloc] initWithVersionString:@".1."]; + XCTAssertEqualObjects(@"0", version.mayorVersion); + XCTAssertEqualObjects(@"1", version.minorVersion); + XCTAssertEqualObjects(@"0", version.patchVersion); + XCTAssertEqualObjects(@"0.1.0", version.versionString); + + version = [[MPPluginVersion alloc] initWithVersionString:@".1.1"]; + XCTAssertEqualObjects(@"0", version.mayorVersion); + XCTAssertEqualObjects(@"1", version.minorVersion); + XCTAssertEqualObjects(@"1", version.patchVersion); + XCTAssertEqualObjects(@"0.1.1", version.versionString); + + version = [[MPPluginVersion alloc] initWithVersionString:@"..1"]; + XCTAssertEqualObjects(@"0", version.mayorVersion); + XCTAssertEqualObjects(@"0", version.minorVersion); + XCTAssertEqualObjects(@"1", version.patchVersion); + XCTAssertEqualObjects(@"0.0.1", version.versionString); + + version = [[MPPluginVersion alloc] initWithVersionString:@"1.0.0"]; + XCTAssertEqualObjects(@"1", version.mayorVersion); + XCTAssertEqualObjects(@"0", version.minorVersion); + XCTAssertEqualObjects(@"0", version.patchVersion); + XCTAssertEqualObjects(@"1.0.0", version.versionString); + + version = [[MPPluginVersion alloc] initWithVersionString:@"0.1.0"]; + XCTAssertEqualObjects(@"0", version.mayorVersion); + XCTAssertEqualObjects(@"1", version.minorVersion); + XCTAssertEqualObjects(@"0", version.patchVersion); + XCTAssertEqualObjects(@"0.1.0", version.versionString); + + version = [[MPPluginVersion alloc] initWithVersionString:@"1.1.1"]; + XCTAssertEqualObjects(@"1", version.mayorVersion); + XCTAssertEqualObjects(@"1", version.minorVersion); + XCTAssertEqualObjects(@"1", version.patchVersion); + XCTAssertEqualObjects(@"1.1.1", version.versionString); + + version = [[MPPluginVersion alloc] initWithVersionString:@"0.0.5"]; + XCTAssertEqualObjects(@"0", version.mayorVersion); + XCTAssertEqualObjects(@"0", version.minorVersion); + XCTAssertEqualObjects(@"5", version.patchVersion); + XCTAssertEqualObjects(@"0.0.5", version.versionString); + + version = [[MPPluginVersion alloc] initWithVersionString:@"1.0.3"]; + XCTAssertEqualObjects(@"1", version.mayorVersion); + XCTAssertEqualObjects(@"0", version.minorVersion); + XCTAssertEqualObjects(@"3", version.patchVersion); + XCTAssertEqualObjects(@"1.0.3", version.versionString); + + version = [[MPPluginVersion alloc] initWithVersionString:@"0.1.4"]; + XCTAssertEqualObjects(@"0", version.mayorVersion); + XCTAssertEqualObjects(@"1", version.minorVersion); + XCTAssertEqualObjects(@"4", version.patchVersion); + XCTAssertEqualObjects(@"0.1.4", version.versionString); + + version = [[MPPluginVersion alloc] initWithVersionString:@"0.1.*"]; + XCTAssertEqualObjects(@"0", version.mayorVersion); + XCTAssertEqualObjects(@"1", version.minorVersion); + XCTAssertEqualObjects(@"*", version.patchVersion); + XCTAssertEqualObjects(@"0.1.*", version.versionString); + + version = [[MPPluginVersion alloc] initWithVersionString:@"*.1.0"]; + XCTAssertEqualObjects(@"*", version.mayorVersion); + XCTAssertEqualObjects(@"*", version.minorVersion); + XCTAssertEqualObjects(@"*", version.patchVersion); + XCTAssertEqualObjects(@"*.*.*", version.versionString); + + version = [[MPPluginVersion alloc] initWithVersionString:@"1.*.0"]; + XCTAssertEqualObjects(@"1", version.mayorVersion); + XCTAssertEqualObjects(@"*", version.minorVersion); + XCTAssertEqualObjects(@"*", version.patchVersion); + XCTAssertEqualObjects(@"1.*.*", version.versionString); +} + +- (void)testeVersionCompare { + + NSArray *data = @[ + @[ @"*", @"*", @(NSOrderedSame)], + @[ @"1.0.1", @"*", @(NSOrderedSame)], + @[ @"*", @"1.0.1", @(NSOrderedSame)], + @[ @"1.*", @"1.*", @(NSOrderedSame)], + @[ @"1.1.*", @"1.1.*", @(NSOrderedSame)], + @[ @"1.1.1", @"1.1.1", @(NSOrderedSame)], + @[ @"1.0.1", @"*", @(NSOrderedSame)], + @[ @"0.10.*", @"0.10.1", @(NSOrderedSame)], + @[ @"10.*.1", @"10.99.*", @(NSOrderedSame)], + @[ @"0.9.89", @"0.9.89", @(NSOrderedSame)], + @[ @"0.0.1", @"0.0.2", @(NSOrderedAscending)], + @[ @"1.10.10", @"1.12.10", @(NSOrderedAscending)], + @[ @"20.0.1", @"20.1.0", @(NSOrderedAscending)], + @[ @"1.1.1", @"2.*", @(NSOrderedAscending)], + @[ @"1.*", @"2.0.*", @(NSOrderedAscending)], + @[ @"2.1.1", @"2.0.0", @(NSOrderedDescending)], + @[ @"0.1.1", @"0.1.0", @(NSOrderedDescending)], + @[ @"1.2.*", @"1.1.*", @(NSOrderedDescending)], + @[ @"2.*", @"1.*", @(NSOrderedDescending)] + ]; + for(NSUInteger index = 0; index < data.count; index++) { + NSArray *set = data[index]; + MPPluginVersion *versionA = [[MPPluginVersion alloc] initWithVersionString:set[0]]; + MPPluginVersion *versionB = [[MPPluginVersion alloc] initWithVersionString:set[1]]; + NSComparisonResult result = [set[2] integerValue]; + XCTAssertEqual(result, [versionA compare:versionB]); + } + +} + +@end