From 5fba1fe0466e28cef30cee1d0b65078886c8f107 Mon Sep 17 00:00:00 2001 From: Zorg Date: Sat, 26 Jun 2021 23:18:22 -0700 Subject: [PATCH 01/11] Add channels support to Sparkle This allows developers to post updates to specific channels like for beta updates. Updaters only get notifications from channels they're allowed to be on. --- Sparkle.xcodeproj/project.pbxproj | 4 ++-- Sparkle/SPUUpdaterDelegate.h | 29 +++++++++++++++++++++++++++++ Sparkle/SUAppcast.m | 8 ++++---- Sparkle/SUAppcastDriver.m | 23 +++++++++++++++++++++-- Sparkle/SUAppcastItem.h | 1 + Sparkle/SUAppcastItem.m | 8 ++++++++ Sparkle/SUConstants.h | 2 ++ Sparkle/SUConstants.m | 2 ++ Sparkle/Sparkle.h | 1 + 9 files changed, 70 insertions(+), 8 deletions(-) diff --git a/Sparkle.xcodeproj/project.pbxproj b/Sparkle.xcodeproj/project.pbxproj index 91729ac5..34fae351 100644 --- a/Sparkle.xcodeproj/project.pbxproj +++ b/Sparkle.xcodeproj/project.pbxproj @@ -81,7 +81,7 @@ 5D1AF5900FD767AD0065DB48 /* libxml2.dylib in Frameworks */ = {isa = PBXBuildFile; fileRef = 5D1AF58F0FD767AD0065DB48 /* libxml2.dylib */; }; 5D1AF59A0FD767E50065DB48 /* libz.dylib in Frameworks */ = {isa = PBXBuildFile; fileRef = 5D1AF5990FD767E50065DB48 /* libz.dylib */; }; 5F1510A21C96E591006E1629 /* testnamespaces.xml in Resources */ = {isa = PBXBuildFile; fileRef = 5F1510A11C96E591006E1629 /* testnamespaces.xml */; }; - 61299A5C09CA6D4500B7442F /* SUConstants.h in Headers */ = {isa = PBXBuildFile; fileRef = 61299A5B09CA6D4500B7442F /* SUConstants.h */; settings = {ATTRIBUTES = (); }; }; + 61299A5C09CA6D4500B7442F /* SUConstants.h in Headers */ = {isa = PBXBuildFile; fileRef = 61299A5B09CA6D4500B7442F /* SUConstants.h */; settings = {ATTRIBUTES = (Public, ); }; }; 61299A6009CA6EB100B7442F /* SUConstants.m in Sources */ = {isa = PBXBuildFile; fileRef = 61299A5F09CA6EB100B7442F /* SUConstants.m */; }; 61299B3609CB04E000B7442F /* Sparkle.h in Headers */ = {isa = PBXBuildFile; fileRef = 61299B3509CB04E000B7442F /* Sparkle.h */; settings = {ATTRIBUTES = (Public, ); }; }; 612DCBB00D488BC60015DBEA /* SUUpdatePermissionPrompt.m in Sources */ = {isa = PBXBuildFile; fileRef = 612DCBAE0D488BC60015DBEA /* SUUpdatePermissionPrompt.m */; }; @@ -2313,6 +2313,7 @@ isa = PBXHeadersBuildPhase; buildActionMask = 2147483647; files = ( + 61299A5C09CA6D4500B7442F /* SUConstants.h in Headers */, 61299B3609CB04E000B7442F /* Sparkle.h in Headers */, 72B767E61C9CFD7200A07552 /* SPUAutomaticUpdateDriver.h in Headers */, 72B767D61C9C8B5C00A07552 /* SPUBasicUpdateDriver.h in Headers */, @@ -2357,7 +2358,6 @@ 725602D51C83551C00DAA70E /* SUApplicationInfo.h in Headers */, 7214B8811D456A8500CB5CED /* SUBundleIcon.h in Headers */, 720767D31E2EE9C200F9A850 /* SUTouchBarForwardDeclarations.h in Headers */, - 61299A5C09CA6D4500B7442F /* SUConstants.h in Headers */, 55E6F33319EC9F6C00005E76 /* SUErrors.h in Headers */, 14652F8419A978C200959E44 /* SUExport.h in Headers */, 7267E5E51D3D90AA00D1BF90 /* SUFileManager.h in Headers */, diff --git a/Sparkle/SPUUpdaterDelegate.h b/Sparkle/SPUUpdaterDelegate.h index 18bad993..798da22f 100644 --- a/Sparkle/SPUUpdaterDelegate.h +++ b/Sparkle/SPUUpdaterDelegate.h @@ -160,6 +160,35 @@ typedef NS_ENUM(NSInteger, SPUUpdateCheck) */ - (void)updater:(SPUUpdater *)updater didFinishLoadingAppcast:(SUAppcast *)appcast; +/*! + Returns the set of Sparkle channels the updater is allowed to find new updates from. + + An appcast item can specify a set of channels its update is posted to. Without specifying any channels, appcast items are posted to the default channel. + For instance: + + 2.0 Beta 1 + + + + + + This example posts an update to the sparkle:beta channel, so only updaters that are allowed to use the sparkle:beta channel can find this update. + + If the is empty or not present, the update item is posted to the default channel and can be found by any complying updater. + + Sparkle allows you to use the 'sparkle:beta' channel name (SUAppcastElementBetaChannel) for distributing beta updates to your own applications. + + Otherwise you can use any other name you'd like; however, for other names you will need to use and define your own XML namespace tailored for your app. + To define your own namespace, add an attribute to rss element like xmlns:myapp="https://myappwebsite.net" + + Note to use this feature, all app versions that your users may update from in your feed must use a version of Sparkle that supports this feature. + This feature was added in Sparkle 2. + + \return The set of channel names the updater is allowed to find new updates in. An empty set is the default behavior, + which means the updater will only look for updates in the default channel. + */ +- (NSSet *)allowedChannelsForUpdater:(SPUUpdater *)updater; + /*! Returns the item in the appcast corresponding to the update that should be installed. diff --git a/Sparkle/SUAppcast.m b/Sparkle/SUAppcast.m index c3d927cc..ef2ee5a5 100644 --- a/Sparkle/SUAppcast.m +++ b/Sparkle/SUAppcast.m @@ -144,16 +144,16 @@ } [dict setObject:deltas forKey:name]; } - else if ([name isEqualToString:SUAppcastElementTags]) { - NSMutableArray *tags = [NSMutableArray array]; + else if ([name isEqualToString:SUAppcastElementTags] || [name isEqualToString:SUAppcastElementChannels]) { + NSMutableArray *names = [NSMutableArray array]; NSEnumerator *childEnum = [[node children] objectEnumerator]; for (NSXMLNode *child in childEnum) { NSString *childName = child.name; if (childName) { - [tags addObject:childName]; + [names addObject:childName]; } } - [dict setObject:tags forKey:name]; + [dict setObject:names forKey:name]; } else if ([name isEqualToString:SUAppcastElementInformationalUpdate]) { NSMutableSet *informationalUpdateVersions = [NSMutableSet set]; diff --git a/Sparkle/SUAppcastDriver.m b/Sparkle/SUAppcastDriver.m index 82834df5..4406e813 100644 --- a/Sparkle/SUAppcastDriver.m +++ b/Sparkle/SUAppcastDriver.m @@ -166,9 +166,28 @@ NSDictionary *userInfo = @{ SUUpdaterAppcastNotificationKey: loadedAppcast }; [[NSNotificationCenter defaultCenter] postNotificationName:SUUpdaterDidFinishLoadingAppCastNotification object:self.updater userInfo:userInfo]; - // We will never care about other OS's + NSSet *allowedChannels; + if ([self.updaterDelegate respondsToSelector:@selector(allowedChannelsForUpdater:)]) { + allowedChannels = [self.updaterDelegate allowedChannelsForUpdater:self.updater]; + } else { + allowedChannels = [NSSet set]; + } + SUAppcast *macOSAppcast = [loadedAppcast copyByFilteringItems:^(SUAppcastItem *item) { - return (BOOL)[item isMacOsUpdate]; + // We will never care about other OS's + BOOL macOSUpdate = [item isMacOsUpdate]; + if (!macOSUpdate) { + return NO; + } + + NSSet *channels = item.channels; + if (channels.count == 0) { + // Item is on the default channel + return YES; + } + + // Item channels and allowed channels must have one channel in common + return [channels intersectsSet:allowedChannels]; }]; id applicationVersionComparator = [self versionComparator]; diff --git a/Sparkle/SUAppcastItem.h b/Sparkle/SUAppcastItem.h index 4d3efba1..4fcaa58f 100644 --- a/Sparkle/SUAppcastItem.h +++ b/Sparkle/SUAppcastItem.h @@ -52,6 +52,7 @@ SU_EXPORT @interface SUAppcastItem : NSObject @property (copy, readonly, nullable) NSNumber* phasedRolloutInterval; @property (nonatomic, copy, readonly) NSString *installationType; @property (copy, readonly, nullable) NSString *minimumAutoupdateVersion; +@property (nonatomic, readonly) NSSet *channels; @property (getter=isDeltaUpdate, readonly) BOOL deltaUpdate; @property (getter=isCriticalUpdate, readonly) BOOL criticalUpdate; diff --git a/Sparkle/SUAppcastItem.m b/Sparkle/SUAppcastItem.m index b7aaee47..80900171 100644 --- a/Sparkle/SUAppcastItem.m +++ b/Sparkle/SUAppcastItem.m @@ -73,6 +73,7 @@ static NSString *SUAppcastItemStateKey = @"SUAppcastItemState"; @synthesize state = _state; @synthesize hasCriticalInformation = _hasCriticalInformation; @synthesize informationalUpdateVersions = _informationalUpdateVersions; +@synthesize channels = _channels; + (BOOL)supportsSecureCoding { @@ -344,6 +345,13 @@ static NSString *SUAppcastItemStateKey = @"SUAppcastItemState"; } return nil; } + + NSArray *channels = [dict objectForKey:SUAppcastElementChannels]; + if (channels == nil) { + _channels = [NSSet set]; + } else { + _channels = [NSSet setWithArray:channels]; + } _propertiesDictionary = [[NSDictionary alloc] initWithDictionary:dict]; _title = [(NSString *)[dict objectForKey:SURSSElementTitle] copy]; diff --git a/Sparkle/SUConstants.h b/Sparkle/SUConstants.h index a1e65b66..f147b6f1 100644 --- a/Sparkle/SUConstants.h +++ b/Sparkle/SUConstants.h @@ -86,6 +86,8 @@ extern NSString *const SUAppcastElementReleaseNotesLink; extern NSString *const SUAppcastElementTags; extern NSString *const SUAppcastElementPhasedRolloutInterval; extern NSString *const SUAppcastElementInformationalUpdate; +extern NSString *const SUAppcastElementChannels; +extern NSString *const SUAppcastElementBetaChannel; extern NSString *const SURSSAttributeURL; extern NSString *const SURSSAttributeLength; diff --git a/Sparkle/SUConstants.m b/Sparkle/SUConstants.m index 0a60324e..f72b648a 100644 --- a/Sparkle/SUConstants.m +++ b/Sparkle/SUConstants.m @@ -80,6 +80,8 @@ NSString *const SUAppcastElementReleaseNotesLink = @"sparkle:releaseNotesLink"; NSString *const SUAppcastElementTags = @"sparkle:tags"; NSString *const SUAppcastElementPhasedRolloutInterval = @"sparkle:phasedRolloutInterval"; NSString *const SUAppcastElementInformationalUpdate = @"sparkle:informationalUpdate"; +NSString *const SUAppcastElementChannels = @"sparkle:channels"; +NSString *const SUAppcastElementBetaChannel = @"sparkle:beta"; NSString *const SURSSAttributeURL = @"url"; NSString *const SURSSAttributeLength = @"length"; diff --git a/Sparkle/Sparkle.h b/Sparkle/Sparkle.h index a048d267..b9bd8afa 100644 --- a/Sparkle/Sparkle.h +++ b/Sparkle/Sparkle.h @@ -22,6 +22,7 @@ #import #import #import +#import #import #import #import From 267f2b82b13f1dc7fbae1924583f7803e73fb6f3 Mon Sep 17 00:00:00 2001 From: Zorg Date: Sat, 26 Jun 2021 23:49:45 -0700 Subject: [PATCH 02/11] Do not say no valid update could be found if there were no items There may not be any applicable items due to sparkle:channels now. --- Sparkle/Base.lproj/Sparkle.strings | 3 --- Sparkle/SPUBasicUpdateDriver.m | 12 ++++++------ Sparkle/ja.lproj/Sparkle.strings | 3 --- 3 files changed, 6 insertions(+), 12 deletions(-) diff --git a/Sparkle/Base.lproj/Sparkle.strings b/Sparkle/Base.lproj/Sparkle.strings index 86423b2a..c70941db 100644 --- a/Sparkle/Base.lproj/Sparkle.strings +++ b/Sparkle/Base.lproj/Sparkle.strings @@ -14,9 +14,6 @@ "%@ %@ is currently the newest version available.\n(You are currently running version %@.)" = "%1$@ %2$@ is currently the newest version available.\n(You are currently running version %3$@.)"; -/* An appcast feed error when checking for updates and no appcast items are retrieved */ -"No valid update information could be loaded." = "No valid update information could be loaded."; - "An error occurred while running the updater. Please try again later." = "An error occurred while running the updater. Please try again later."; "An error occurred while starting the installer. Please try again later." = "An error occurred while starting the installer. Please try again later."; diff --git a/Sparkle/SPUBasicUpdateDriver.m b/Sparkle/SPUBasicUpdateDriver.m index 859e9eb7..72ee92c4 100644 --- a/Sparkle/SPUBasicUpdateDriver.m +++ b/Sparkle/SPUBasicUpdateDriver.m @@ -203,17 +203,17 @@ } break; } - - recoveryOption = @"OK"; } else { // When no updates are found in the appcast, or latest appcast item info // was not provided (i.e, for a background update check) - // In the case no info was provided for a background check, the error isn't shown anywhere - localizedDescription = SULocalizedString(@"Update Error!", nil); - recoverySuggestion = SULocalizedString(@"No valid update information could be loaded.", nil); - recoveryOption = SULocalizedString(@"Cancel Update", nil); + // We will need to assume the user is up to date if the feed doen't have any applicable update items + // There could be update items on channels the updater is not subscribed to for example. But we can't tell the user about them. + localizedDescription = SULocalizedString(@"You're up-to-date!", "Status message shown when the user checks for updates but is already current or the feed doesn't contain any updates."); + recoverySuggestion = [NSString stringWithFormat:SULocalizedString(@"%@ %@ is currently the newest version available.", nil), [self.host name], [self.host displayVersion]]; } + recoveryOption = SULocalizedString(@"OK", nil); + NSError *notFoundError = [NSError errorWithDomain:SUSparkleErrorDomain diff --git a/Sparkle/ja.lproj/Sparkle.strings b/Sparkle/ja.lproj/Sparkle.strings index d7c2654f..f3a631ae 100644 --- a/Sparkle/ja.lproj/Sparkle.strings +++ b/Sparkle/ja.lproj/Sparkle.strings @@ -14,9 +14,6 @@ "%@ %@ is currently the newest version available.\n(You are currently running version %@.)" = "%1$@ %2$@は現在入手できる最新バージョンです。\n(現在使用中のバージョンは%3$@です。)"; -/* An appcast feed error when checking for updates and no appcast items are retrieved */ -"No valid update information could be loaded." = "有効なアップデート情報が取得できませんでした。"; - "An error occurred while running the updater. Please try again later." = "アップデータを実行中にエラーが発生しました。あとでやり直してください。"; "An error occurred while starting the installer. Please try again later." = "インストーラを始動中にエラーが発生しました。あとでやり直してください。"; From 9bb59b8c00ffc7f2b866ca19bbd0c3231ae43796 Mon Sep 17 00:00:00 2001 From: Zorg Date: Sun, 27 Jun 2021 00:28:09 -0700 Subject: [PATCH 03/11] Add tests for filtering allowed channels and macOS releases --- Sparkle.xcodeproj/project.pbxproj | 4 ++ Sparkle/SUAppcastDriver.m | 38 +++++++----- Tests/Resources/testappcast_channels.xml | 69 ++++++++++++++++++++++ Tests/SUAppcastTest.swift | 59 ++++++++++++++++++ Tests/Sparkle Unit Tests-Bridging-Header.h | 2 + 5 files changed, 156 insertions(+), 16 deletions(-) create mode 100644 Tests/Resources/testappcast_channels.xml diff --git a/Sparkle.xcodeproj/project.pbxproj b/Sparkle.xcodeproj/project.pbxproj index 34fae351..1904c153 100644 --- a/Sparkle.xcodeproj/project.pbxproj +++ b/Sparkle.xcodeproj/project.pbxproj @@ -188,6 +188,7 @@ 723C8A541E2D60DB00C14942 /* SUTouchBarButtonGroup.h in Headers */ = {isa = PBXBuildFile; fileRef = 723C8A521E2D60DB00C14942 /* SUTouchBarButtonGroup.h */; }; 723C8A551E2D60DB00C14942 /* SUTouchBarButtonGroup.m in Sources */ = {isa = PBXBuildFile; fileRef = 723C8A531E2D60DB00C14942 /* SUTouchBarButtonGroup.m */; }; 723C8A561E2D60DB00C14942 /* SUTouchBarButtonGroup.m in Sources */ = {isa = PBXBuildFile; fileRef = 723C8A531E2D60DB00C14942 /* SUTouchBarButtonGroup.m */; }; + 723EDC3F26885A8E000BCBA4 /* testappcast_channels.xml in Resources */ = {isa = PBXBuildFile; fileRef = 723EDC3E26885A8E000BCBA4 /* testappcast_channels.xml */; }; 72464F701E1F31E000FB341C /* SUOperatingSystem.m in Sources */ = {isa = PBXBuildFile; fileRef = 726F2CE41BC9C33D001971A4 /* SUOperatingSystem.m */; }; 72464F711E1F326A00FB341C /* SUOperatingSystem.m in Sources */ = {isa = PBXBuildFile; fileRef = 726F2CE41BC9C33D001971A4 /* SUOperatingSystem.m */; }; 72464F721E1F327C00FB341C /* SUOperatingSystem.m in Sources */ = {isa = PBXBuildFile; fileRef = 726F2CE41BC9C33D001971A4 /* SUOperatingSystem.m */; }; @@ -1082,6 +1083,7 @@ 723B5DA51CF7AB0100365F95 /* SPUDownloaderProtocol.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = SPUDownloaderProtocol.h; path = Downloader/SPUDownloaderProtocol.h; sourceTree = SOURCE_ROOT; }; 723C8A521E2D60DB00C14942 /* SUTouchBarButtonGroup.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SUTouchBarButtonGroup.h; sourceTree = ""; }; 723C8A531E2D60DB00C14942 /* SUTouchBarButtonGroup.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SUTouchBarButtonGroup.m; sourceTree = ""; }; + 723EDC3E26885A8E000BCBA4 /* testappcast_channels.xml */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xml; path = testappcast_channels.xml; sourceTree = ""; }; 7246E0A11C83B685003B4E75 /* SPUStandardUpdaterController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SPUStandardUpdaterController.h; sourceTree = ""; }; 7246E0A21C83B685003B4E75 /* SPUStandardUpdaterController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SPUStandardUpdaterController.m; sourceTree = ""; }; 724BB36C1D31D0B7005D534A /* org.sparkle-project.InstallerConnection.xpc */ = {isa = PBXFileReference; explicitFileType = "wrapper.xpc-service"; includeInIndex = 0; path = "org.sparkle-project.InstallerConnection.xpc"; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -1685,6 +1687,7 @@ 14958C6C19AEBC610061B14F /* test-pubkey.pem */, 5AF6C74E1AEA46D10014A3AB /* test.pkg */, 5AD0FA7E1C73F2E2004BCEFF /* testappcast.xml */, + 723EDC3E26885A8E000BCBA4 /* testappcast_channels.xml */, 725B3A81263FBF0C0041AB8E /* testappcast_minimumAutoupdateVersion.xml */, 722545B526805FF80036465C /* testappcast_info_updates.xml */, 5F1510A11C96E591006E1629 /* testnamespaces.xml */, @@ -2936,6 +2939,7 @@ files = ( 725B3A82263FBF0C0041AB8E /* testappcast_minimumAutoupdateVersion.xml in Resources */, 14958C6E19AEBC950061B14F /* signed-test-file.txt in Resources */, + 723EDC3F26885A8E000BCBA4 /* testappcast_channels.xml in Resources */, 72AC6B2E1B9B218C00F62325 /* SparkleTestCodeSignApp.dmg in Resources */, C23E885B1BE7B24F0050BB73 /* SparkleTestCodeSignApp.enc.dmg in Resources */, 72AC6B281B9AAD6700F62325 /* SparkleTestCodeSignApp.tar in Resources */, diff --git a/Sparkle/SUAppcastDriver.m b/Sparkle/SUAppcastDriver.m index 4406e813..c848fa34 100644 --- a/Sparkle/SUAppcastDriver.m +++ b/Sparkle/SUAppcastDriver.m @@ -173,22 +173,7 @@ allowedChannels = [NSSet set]; } - SUAppcast *macOSAppcast = [loadedAppcast copyByFilteringItems:^(SUAppcastItem *item) { - // We will never care about other OS's - BOOL macOSUpdate = [item isMacOsUpdate]; - if (!macOSUpdate) { - return NO; - } - - NSSet *channels = item.channels; - if (channels.count == 0) { - // Item is on the default channel - return YES; - } - - // Item channels and allowed channels must have one channel in common - return [channels intersectsSet:allowedChannels]; - }]; + SUAppcast *macOSAppcast = [[self class] filterAppcast:loadedAppcast forMacOSAndAllowedChannels:allowedChannels]; id applicationVersionComparator = [self versionComparator]; @@ -240,6 +225,27 @@ } } +// This method is used by unit tests ++ (SUAppcast *)filterAppcast:(SUAppcast *)appcast forMacOSAndAllowedChannels:(NSSet *)allowedChannels +{ + return [appcast copyByFilteringItems:^(SUAppcastItem *item) { + // We will never care about other OS's + BOOL macOSUpdate = [item isMacOsUpdate]; + if (!macOSUpdate) { + return NO; + } + + NSSet *channels = item.channels; + if (channels.count == 0) { + // Item is on the default channel + return YES; + } + + // Item channels and allowed channels must have one channel in common + return [channels intersectsSet:allowedChannels]; + }]; +} + // This method is used by unit tests + (SUAppcast *)filterSupportedAppcast:(SUAppcast *)appcast phasedUpdateGroup:(NSNumber * _Nullable)phasedUpdateGroup skippedUpdate:(SPUSkippedUpdate * _Nullable)skippedUpdate hostVersion:(NSString *)hostVersion versionComparator:(id)versionComparator testOSVersion:(BOOL)testOSVersion testMinimumAutoupdateVersion:(BOOL)testMinimumAutoupdateVersion { diff --git a/Tests/Resources/testappcast_channels.xml b/Tests/Resources/testappcast_channels.xml new file mode 100644 index 00000000..db0f1b73 --- /dev/null +++ b/Tests/Resources/testappcast_channels.xml @@ -0,0 +1,69 @@ + + + + For unit test only + + + Version 2.0 + desc + Sat, 26 Jul 2014 15:20:11 +0000 + + + + + + Version 3.0 + 86400 + + + + + + + + + + + + + + Version 4.0 + 4.0 + Sat, 26 Jul 2014 15:20:13 +0000 + + + + + + + + Version 5.0 + 5.0 + + + + + + + + + Version 6.0 + 6.0 + + + + + + + + diff --git a/Tests/SUAppcastTest.swift b/Tests/SUAppcastTest.swift index d3c25fd9..b27c35bb 100644 --- a/Tests/SUAppcastTest.swift +++ b/Tests/SUAppcastTest.swift @@ -76,6 +76,65 @@ class SUAppcastTest: XCTestCase { } } + func testChannelsAndMacOSReleases() { + let testURL = Bundle(for: SUAppcastTest.self).url(forResource: "testappcast_channels", withExtension: "xml")! + + do { + let testData = try Data(contentsOf: testURL) + + let versionComparator = SUStandardVersionComparator.default() + let hostVersion = "1.0" + let stateResolver = SPUAppcastItemStateResolver(hostVersion: hostVersion, applicationVersionComparator: versionComparator, standardVersionComparator: versionComparator) + + let appcast = try SUAppcast(xmlData: testData, relativeTo: nil, stateResolver: stateResolver) + XCTAssertEqual(5, appcast.items.count) + + do { + let filteredAppcast = SUAppcastDriver.filterAppcast(appcast, forMacOSAndAllowedChannels: ["sparkle:beta", "sparkle:nightly"]) + XCTAssertEqual(4, filteredAppcast.items.count) + + XCTAssertEqual("2.0", filteredAppcast.items[0].versionString) + XCTAssertEqual("3.0", filteredAppcast.items[1].versionString) + XCTAssertEqual("4.0", filteredAppcast.items[2].versionString) + XCTAssertEqual("5.0", filteredAppcast.items[3].versionString) + } + + do { + let filteredAppcast = SUAppcastDriver.filterAppcast(appcast, forMacOSAndAllowedChannels: []) + XCTAssertEqual(2, filteredAppcast.items.count) + XCTAssertEqual("2.0", filteredAppcast.items[0].versionString) + XCTAssertEqual("3.0", filteredAppcast.items[1].versionString) + } + + do { + let filteredAppcast = SUAppcastDriver.filterAppcast(appcast, forMacOSAndAllowedChannels: ["sparkle:beta"]) + XCTAssertEqual(4, filteredAppcast.items.count) + XCTAssertEqual("2.0", filteredAppcast.items[0].versionString) + XCTAssertEqual("3.0", filteredAppcast.items[1].versionString) + XCTAssertEqual("4.0", filteredAppcast.items[2].versionString) + XCTAssertEqual("5.0", filteredAppcast.items[3].versionString) + } + + do { + let filteredAppcast = SUAppcastDriver.filterAppcast(appcast, forMacOSAndAllowedChannels: ["sparkle:nightly"]) + XCTAssertEqual(3, filteredAppcast.items.count) + XCTAssertEqual("2.0", filteredAppcast.items[0].versionString) + XCTAssertEqual("3.0", filteredAppcast.items[1].versionString) + XCTAssertEqual("5.0", filteredAppcast.items[2].versionString) + } + + do { + let filteredAppcast = SUAppcastDriver.filterAppcast(appcast, forMacOSAndAllowedChannels: ["sparkle:madeup"]) + XCTAssertEqual("2.0", filteredAppcast.items[0].versionString) + XCTAssertEqual("3.0", filteredAppcast.items[1].versionString) + XCTAssertEqual(2, filteredAppcast.items.count) + } + } catch let err as NSError { + NSLog("%@", err) + XCTFail(err.localizedDescription) + } + } + func testCriticalUpdateVersion() { let testURL = Bundle(for: SUAppcastTest.self).url(forResource: "testappcast", withExtension: "xml")! diff --git a/Tests/Sparkle Unit Tests-Bridging-Header.h b/Tests/Sparkle Unit Tests-Bridging-Header.h index d0e9251e..395426db 100644 --- a/Tests/Sparkle Unit Tests-Bridging-Header.h +++ b/Tests/Sparkle Unit Tests-Bridging-Header.h @@ -42,6 +42,8 @@ static const char *SUAppleQuarantineIdentifier = "com.apple.quarantine"; + (SUAppcast *)filterSupportedAppcast:(SUAppcast *)appcast phasedUpdateGroup:(NSNumber * _Nullable)phasedUpdateGroup skippedUpdate:(SPUSkippedUpdate * _Nullable)skippedUpdate hostVersion:(NSString *)hostVersion versionComparator:(id)versionComparator testOSVersion:(BOOL)testOSVersion testMinimumAutoupdateVersion:(BOOL)testMinimumAutoupdateVersion; ++ (SUAppcast *)filterAppcast:(SUAppcast *)appcast forMacOSAndAllowedChannels:(NSSet *)allowedChannels; + @end @interface SUBinaryDeltaUnarchiver (Private) From bc7e2d48457915117f0e90af4b3e9878c76c66f0 Mon Sep 17 00:00:00 2001 From: Zorg Date: Sun, 27 Jun 2021 00:40:33 -0700 Subject: [PATCH 04/11] Export SUAppcastElementBetaChannel in new exported constants header --- Sparkle.xcodeproj/project.pbxproj | 8 ++++++-- Sparkle/SUConstants.h | 1 - Sparkle/SUConstants.m | 1 + Sparkle/SUExportedConstants.h | 25 +++++++++++++++++++++++++ Sparkle/Sparkle.h | 2 +- 5 files changed, 33 insertions(+), 4 deletions(-) create mode 100644 Sparkle/SUExportedConstants.h diff --git a/Sparkle.xcodeproj/project.pbxproj b/Sparkle.xcodeproj/project.pbxproj index 1904c153..75d99069 100644 --- a/Sparkle.xcodeproj/project.pbxproj +++ b/Sparkle.xcodeproj/project.pbxproj @@ -81,7 +81,7 @@ 5D1AF5900FD767AD0065DB48 /* libxml2.dylib in Frameworks */ = {isa = PBXBuildFile; fileRef = 5D1AF58F0FD767AD0065DB48 /* libxml2.dylib */; }; 5D1AF59A0FD767E50065DB48 /* libz.dylib in Frameworks */ = {isa = PBXBuildFile; fileRef = 5D1AF5990FD767E50065DB48 /* libz.dylib */; }; 5F1510A21C96E591006E1629 /* testnamespaces.xml in Resources */ = {isa = PBXBuildFile; fileRef = 5F1510A11C96E591006E1629 /* testnamespaces.xml */; }; - 61299A5C09CA6D4500B7442F /* SUConstants.h in Headers */ = {isa = PBXBuildFile; fileRef = 61299A5B09CA6D4500B7442F /* SUConstants.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 61299A5C09CA6D4500B7442F /* SUConstants.h in Headers */ = {isa = PBXBuildFile; fileRef = 61299A5B09CA6D4500B7442F /* SUConstants.h */; }; 61299A6009CA6EB100B7442F /* SUConstants.m in Sources */ = {isa = PBXBuildFile; fileRef = 61299A5F09CA6EB100B7442F /* SUConstants.m */; }; 61299B3609CB04E000B7442F /* Sparkle.h in Headers */ = {isa = PBXBuildFile; fileRef = 61299B3509CB04E000B7442F /* Sparkle.h */; settings = {ATTRIBUTES = (Public, ); }; }; 612DCBB00D488BC60015DBEA /* SUUpdatePermissionPrompt.m in Sources */ = {isa = PBXBuildFile; fileRef = 612DCBAE0D488BC60015DBEA /* SUUpdatePermissionPrompt.m */; }; @@ -189,6 +189,7 @@ 723C8A551E2D60DB00C14942 /* SUTouchBarButtonGroup.m in Sources */ = {isa = PBXBuildFile; fileRef = 723C8A531E2D60DB00C14942 /* SUTouchBarButtonGroup.m */; }; 723C8A561E2D60DB00C14942 /* SUTouchBarButtonGroup.m in Sources */ = {isa = PBXBuildFile; fileRef = 723C8A531E2D60DB00C14942 /* SUTouchBarButtonGroup.m */; }; 723EDC3F26885A8E000BCBA4 /* testappcast_channels.xml in Resources */ = {isa = PBXBuildFile; fileRef = 723EDC3E26885A8E000BCBA4 /* testappcast_channels.xml */; }; + 723EDC4126886279000BCBA4 /* SUExportedConstants.h in Headers */ = {isa = PBXBuildFile; fileRef = 723EDC40268861EC000BCBA4 /* SUExportedConstants.h */; settings = {ATTRIBUTES = (Public, ); }; }; 72464F701E1F31E000FB341C /* SUOperatingSystem.m in Sources */ = {isa = PBXBuildFile; fileRef = 726F2CE41BC9C33D001971A4 /* SUOperatingSystem.m */; }; 72464F711E1F326A00FB341C /* SUOperatingSystem.m in Sources */ = {isa = PBXBuildFile; fileRef = 726F2CE41BC9C33D001971A4 /* SUOperatingSystem.m */; }; 72464F721E1F327C00FB341C /* SUOperatingSystem.m in Sources */ = {isa = PBXBuildFile; fileRef = 726F2CE41BC9C33D001971A4 /* SUOperatingSystem.m */; }; @@ -1084,6 +1085,7 @@ 723C8A521E2D60DB00C14942 /* SUTouchBarButtonGroup.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SUTouchBarButtonGroup.h; sourceTree = ""; }; 723C8A531E2D60DB00C14942 /* SUTouchBarButtonGroup.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SUTouchBarButtonGroup.m; sourceTree = ""; }; 723EDC3E26885A8E000BCBA4 /* testappcast_channels.xml */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xml; path = testappcast_channels.xml; sourceTree = ""; }; + 723EDC40268861EC000BCBA4 /* SUExportedConstants.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SUExportedConstants.h; sourceTree = ""; }; 7246E0A11C83B685003B4E75 /* SPUStandardUpdaterController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SPUStandardUpdaterController.h; sourceTree = ""; }; 7246E0A21C83B685003B4E75 /* SPUStandardUpdaterController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SPUStandardUpdaterController.m; sourceTree = ""; }; 724BB36C1D31D0B7005D534A /* org.sparkle-project.InstallerConnection.xpc */ = {isa = PBXFileReference; explicitFileType = "wrapper.xpc-service"; includeInIndex = 0; path = "org.sparkle-project.InstallerConnection.xpc"; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -1854,6 +1856,7 @@ 721BC20D1D1CDE55002BC71E /* SPULocalCacheDirectory.m */, 7214B87F1D456A8500CB5CED /* SUBundleIcon.h */, 7214B8801D456A8500CB5CED /* SUBundleIcon.m */, + 723EDC40268861EC000BCBA4 /* SUExportedConstants.h */, 61299A5B09CA6D4500B7442F /* SUConstants.h */, 61299A5F09CA6EB100B7442F /* SUConstants.m */, 55E6F33219EC9F6C00005E76 /* SUErrors.h */, @@ -2316,7 +2319,6 @@ isa = PBXHeadersBuildPhase; buildActionMask = 2147483647; files = ( - 61299A5C09CA6D4500B7442F /* SUConstants.h in Headers */, 61299B3609CB04E000B7442F /* Sparkle.h in Headers */, 72B767E61C9CFD7200A07552 /* SPUAutomaticUpdateDriver.h in Headers */, 72B767D61C9C8B5C00A07552 /* SPUBasicUpdateDriver.h in Headers */, @@ -2331,6 +2333,7 @@ 721BC20E1D1CDE55002BC71E /* SPULocalCacheDirectory.h in Headers */, 7267E5B11D3D8AD500D1BF90 /* SPUMessageTypes.h in Headers */, 728337A61C9E6FF40085AA99 /* SPUProbeInstallStatus.h in Headers */, + 723EDC4126886279000BCBA4 /* SUExportedConstants.h in Headers */, 723C8A541E2D60DB00C14942 /* SUTouchBarButtonGroup.h in Headers */, 72B767D21C9C7B9300A07552 /* SPUProbingUpdateDriver.h in Headers */, 7229E1B91C97CC4D00CB50D0 /* SPUScheduledUpdateDriver.h in Headers */, @@ -2340,6 +2343,7 @@ 726E4A371C89116000C57C6A /* SPUStandardUserDriverDelegate.h in Headers */, 72EF30BE2675CF38008CE987 /* SPUAppcastItemStateResolver.h in Headers */, 72EF30C7267C716A008CE987 /* SUAppcastItem+Private.h in Headers */, + 61299A5C09CA6D4500B7442F /* SUConstants.h in Headers */, 726DF88E1C84277600188804 /* SPUUserUpdateState.h in Headers */, 72B767DA1C9CD2E400A07552 /* SPUUIBasedUpdateDriver.h in Headers */, 7229E1B61C97C91100CB50D0 /* SPUUpdateDriver.h in Headers */, diff --git a/Sparkle/SUConstants.h b/Sparkle/SUConstants.h index f147b6f1..c9bd7ef1 100644 --- a/Sparkle/SUConstants.h +++ b/Sparkle/SUConstants.h @@ -87,7 +87,6 @@ extern NSString *const SUAppcastElementTags; extern NSString *const SUAppcastElementPhasedRolloutInterval; extern NSString *const SUAppcastElementInformationalUpdate; extern NSString *const SUAppcastElementChannels; -extern NSString *const SUAppcastElementBetaChannel; extern NSString *const SURSSAttributeURL; extern NSString *const SURSSAttributeLength; diff --git a/Sparkle/SUConstants.m b/Sparkle/SUConstants.m index f72b648a..16c6914d 100644 --- a/Sparkle/SUConstants.m +++ b/Sparkle/SUConstants.m @@ -8,6 +8,7 @@ #import "SUConstants.h" #import "SUErrors.h" +#import "SUExportedConstants.h" #ifndef DEBUG #define DEBUG 0 diff --git a/Sparkle/SUExportedConstants.h b/Sparkle/SUExportedConstants.h new file mode 100644 index 00000000..0e44765d --- /dev/null +++ b/Sparkle/SUExportedConstants.h @@ -0,0 +1,25 @@ +// +// SUExportedConstants.h +// Sparkle +// +// Created by Mayur Pawashe on 6/27/21. +// Copyright © 2021 Sparkle Project. All rights reserved. +// + +#ifndef SUExportedConstants_h +#define SUExportedConstants_h + +#if __has_feature(modules) +#if __has_warning("-Watimport-in-framework-header") +#pragma clang diagnostic ignored "-Watimport-in-framework-header" +#endif +@import Foundation; +#else +#import +#endif + +#import + +SU_EXPORT extern NSString *const SUAppcastElementBetaChannel; + +#endif /* SUExportedConstants_h */ diff --git a/Sparkle/Sparkle.h b/Sparkle/Sparkle.h index b9bd8afa..8d7b640e 100644 --- a/Sparkle/Sparkle.h +++ b/Sparkle/Sparkle.h @@ -22,7 +22,7 @@ #import #import #import -#import +#import #import #import #import From 64484507b5fc5afabda5b5fa86558faac628f76b Mon Sep 17 00:00:00 2001 From: Zorg Date: Sun, 27 Jun 2021 00:47:54 -0700 Subject: [PATCH 05/11] Guard against delegate returning nil channels incorrectly --- Sparkle/SUAppcastDriver.m | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Sparkle/SUAppcastDriver.m b/Sparkle/SUAppcastDriver.m index c848fa34..2622ece0 100644 --- a/Sparkle/SUAppcastDriver.m +++ b/Sparkle/SUAppcastDriver.m @@ -169,6 +169,10 @@ NSSet *allowedChannels; if ([self.updaterDelegate respondsToSelector:@selector(allowedChannelsForUpdater:)]) { allowedChannels = [self.updaterDelegate allowedChannelsForUpdater:self.updater]; + if (allowedChannels == nil) { + SULog(SULogLevelError, @"Error: -allowedChannelsForUpdater: cannot return nil. Treating this as an empty set."); + allowedChannels = [NSSet set]; + } } else { allowedChannels = [NSSet set]; } From 29c9b72be60a7c0904b9cc5f2274c6c43bc9b434 Mon Sep 17 00:00:00 2001 From: Zorg Date: Sun, 27 Jun 2021 01:16:23 -0700 Subject: [PATCH 06/11] Refer developers to channels API where appropriate --- Sparkle/SPUUpdater.h | 4 +++- Sparkle/SPUUpdaterDelegate.h | 5 +++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/Sparkle/SPUUpdater.h b/Sparkle/SPUUpdater.h index e77c6eb2..2828521b 100644 --- a/Sparkle/SPUUpdater.h +++ b/Sparkle/SPUUpdater.h @@ -186,10 +186,12 @@ SU_EXPORT @interface SPUUpdater : NSObject Setting this property will persist in the host bundle's user defaults. To avoid this, you should consider instead implementing - -[SPUUpdaterDelegate feedURLStringForUpdater:] or -[SPUUpdaterDelegate feedParametersForUpdater:sendingSystemProfile:] + -[SPUUpdaterDelegate feedURLStringForUpdater:] Passing nil will remove any feed URL that has been set in the host bundle's user defaults. + For beta updates, you may consider migrating to -[SPUUpdaterDelegate allowedChannelsForUpdater:] in the future. + This method must be called on the main thread; calls from background threads will have no effect. */ - (void)setFeedURL:(NSURL * _Nullable)feedURL; diff --git a/Sparkle/SPUUpdaterDelegate.h b/Sparkle/SPUUpdaterDelegate.h index 798da22f..9ac1f6a4 100644 --- a/Sparkle/SPUUpdaterDelegate.h +++ b/Sparkle/SPUUpdaterDelegate.h @@ -192,6 +192,11 @@ typedef NS_ENUM(NSInteger, SPUUpdateCheck) /*! Returns the item in the appcast corresponding to the update that should be installed. + Please consider using or migrating to other supported features before adopting this method. + Specifically: + If you want to filter out certain tagged updates (like beta updates), consider -[SPUUpdaterDelegate allowedChannelsForUpdater:] instead. + If you want to treat certain updates as informational-only, consider supplying with a set of affected versions users are updating from. + If you're using special logic or extensions in your appcast, implement this to use your own logic for finding a valid update, if any, in the given appcast. From c9683341922f8c01fef3728191586474fc7d1546 Mon Sep 17 00:00:00 2001 From: Zorg Date: Sun, 27 Jun 2021 15:14:37 -0700 Subject: [PATCH 07/11] Allow only one channel in appcast feed items --- Sparkle.xcodeproj/project.pbxproj | 4 ---- Sparkle/SPUUpdaterDelegate.h | 15 +++++--------- Sparkle/SUAppcast.m | 2 +- Sparkle/SUAppcastDriver.m | 6 +++--- Sparkle/SUAppcastItem.h | 2 +- Sparkle/SUAppcastItem.m | 16 ++++++++++----- Sparkle/SUConstants.h | 2 +- Sparkle/SUConstants.m | 4 +--- Sparkle/SUExportedConstants.h | 25 ------------------------ Sparkle/Sparkle.h | 1 - Tests/Resources/testappcast_channels.xml | 20 +++++-------------- Tests/SUAppcastTest.swift | 11 +++++------ 12 files changed, 33 insertions(+), 75 deletions(-) delete mode 100644 Sparkle/SUExportedConstants.h diff --git a/Sparkle.xcodeproj/project.pbxproj b/Sparkle.xcodeproj/project.pbxproj index 75d99069..c14ae90d 100644 --- a/Sparkle.xcodeproj/project.pbxproj +++ b/Sparkle.xcodeproj/project.pbxproj @@ -189,7 +189,6 @@ 723C8A551E2D60DB00C14942 /* SUTouchBarButtonGroup.m in Sources */ = {isa = PBXBuildFile; fileRef = 723C8A531E2D60DB00C14942 /* SUTouchBarButtonGroup.m */; }; 723C8A561E2D60DB00C14942 /* SUTouchBarButtonGroup.m in Sources */ = {isa = PBXBuildFile; fileRef = 723C8A531E2D60DB00C14942 /* SUTouchBarButtonGroup.m */; }; 723EDC3F26885A8E000BCBA4 /* testappcast_channels.xml in Resources */ = {isa = PBXBuildFile; fileRef = 723EDC3E26885A8E000BCBA4 /* testappcast_channels.xml */; }; - 723EDC4126886279000BCBA4 /* SUExportedConstants.h in Headers */ = {isa = PBXBuildFile; fileRef = 723EDC40268861EC000BCBA4 /* SUExportedConstants.h */; settings = {ATTRIBUTES = (Public, ); }; }; 72464F701E1F31E000FB341C /* SUOperatingSystem.m in Sources */ = {isa = PBXBuildFile; fileRef = 726F2CE41BC9C33D001971A4 /* SUOperatingSystem.m */; }; 72464F711E1F326A00FB341C /* SUOperatingSystem.m in Sources */ = {isa = PBXBuildFile; fileRef = 726F2CE41BC9C33D001971A4 /* SUOperatingSystem.m */; }; 72464F721E1F327C00FB341C /* SUOperatingSystem.m in Sources */ = {isa = PBXBuildFile; fileRef = 726F2CE41BC9C33D001971A4 /* SUOperatingSystem.m */; }; @@ -1085,7 +1084,6 @@ 723C8A521E2D60DB00C14942 /* SUTouchBarButtonGroup.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SUTouchBarButtonGroup.h; sourceTree = ""; }; 723C8A531E2D60DB00C14942 /* SUTouchBarButtonGroup.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SUTouchBarButtonGroup.m; sourceTree = ""; }; 723EDC3E26885A8E000BCBA4 /* testappcast_channels.xml */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xml; path = testappcast_channels.xml; sourceTree = ""; }; - 723EDC40268861EC000BCBA4 /* SUExportedConstants.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SUExportedConstants.h; sourceTree = ""; }; 7246E0A11C83B685003B4E75 /* SPUStandardUpdaterController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SPUStandardUpdaterController.h; sourceTree = ""; }; 7246E0A21C83B685003B4E75 /* SPUStandardUpdaterController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SPUStandardUpdaterController.m; sourceTree = ""; }; 724BB36C1D31D0B7005D534A /* org.sparkle-project.InstallerConnection.xpc */ = {isa = PBXFileReference; explicitFileType = "wrapper.xpc-service"; includeInIndex = 0; path = "org.sparkle-project.InstallerConnection.xpc"; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -1856,7 +1854,6 @@ 721BC20D1D1CDE55002BC71E /* SPULocalCacheDirectory.m */, 7214B87F1D456A8500CB5CED /* SUBundleIcon.h */, 7214B8801D456A8500CB5CED /* SUBundleIcon.m */, - 723EDC40268861EC000BCBA4 /* SUExportedConstants.h */, 61299A5B09CA6D4500B7442F /* SUConstants.h */, 61299A5F09CA6EB100B7442F /* SUConstants.m */, 55E6F33219EC9F6C00005E76 /* SUErrors.h */, @@ -2333,7 +2330,6 @@ 721BC20E1D1CDE55002BC71E /* SPULocalCacheDirectory.h in Headers */, 7267E5B11D3D8AD500D1BF90 /* SPUMessageTypes.h in Headers */, 728337A61C9E6FF40085AA99 /* SPUProbeInstallStatus.h in Headers */, - 723EDC4126886279000BCBA4 /* SUExportedConstants.h in Headers */, 723C8A541E2D60DB00C14942 /* SUTouchBarButtonGroup.h in Headers */, 72B767D21C9C7B9300A07552 /* SPUProbingUpdateDriver.h in Headers */, 7229E1B91C97CC4D00CB50D0 /* SPUScheduledUpdateDriver.h in Headers */, diff --git a/Sparkle/SPUUpdaterDelegate.h b/Sparkle/SPUUpdaterDelegate.h index 9ac1f6a4..8eb81f26 100644 --- a/Sparkle/SPUUpdaterDelegate.h +++ b/Sparkle/SPUUpdaterDelegate.h @@ -163,23 +163,18 @@ typedef NS_ENUM(NSInteger, SPUUpdateCheck) /*! Returns the set of Sparkle channels the updater is allowed to find new updates from. - An appcast item can specify a set of channels its update is posted to. Without specifying any channels, appcast items are posted to the default channel. + An appcast item can specify a channel the update is posted to. Without specifying a channel, the appcast item is posted to the default channel. For instance: 2.0 Beta 1 - - - + beta - This example posts an update to the sparkle:beta channel, so only updaters that are allowed to use the sparkle:beta channel can find this update. + This example posts an update to the 'beta' channel, so only updaters that are allowed to use the 'beta' channel can find this update. - If the is empty or not present, the update item is posted to the default channel and can be found by any complying updater. + If the is not present, the update item is posted to the default channel and can be found by any updater. - Sparkle allows you to use the 'sparkle:beta' channel name (SUAppcastElementBetaChannel) for distributing beta updates to your own applications. - - Otherwise you can use any other name you'd like; however, for other names you will need to use and define your own XML namespace tailored for your app. - To define your own namespace, add an attribute to rss element like xmlns:myapp="https://myappwebsite.net" + You can pick any name you'd like for the channel. Note to use this feature, all app versions that your users may update from in your feed must use a version of Sparkle that supports this feature. This feature was added in Sparkle 2. diff --git a/Sparkle/SUAppcast.m b/Sparkle/SUAppcast.m index ef2ee5a5..57076764 100644 --- a/Sparkle/SUAppcast.m +++ b/Sparkle/SUAppcast.m @@ -144,7 +144,7 @@ } [dict setObject:deltas forKey:name]; } - else if ([name isEqualToString:SUAppcastElementTags] || [name isEqualToString:SUAppcastElementChannels]) { + else if ([name isEqualToString:SUAppcastElementTags]) { NSMutableArray *names = [NSMutableArray array]; NSEnumerator *childEnum = [[node children] objectEnumerator]; for (NSXMLNode *child in childEnum) { diff --git a/Sparkle/SUAppcastDriver.m b/Sparkle/SUAppcastDriver.m index 2622ece0..e4ccaf37 100644 --- a/Sparkle/SUAppcastDriver.m +++ b/Sparkle/SUAppcastDriver.m @@ -239,14 +239,14 @@ return NO; } - NSSet *channels = item.channels; - if (channels.count == 0) { + NSString *channel = item.channel; + if (channel == nil) { // Item is on the default channel return YES; } // Item channels and allowed channels must have one channel in common - return [channels intersectsSet:allowedChannels]; + return [allowedChannels containsObject:channel]; }]; } diff --git a/Sparkle/SUAppcastItem.h b/Sparkle/SUAppcastItem.h index 4fcaa58f..95b2dda1 100644 --- a/Sparkle/SUAppcastItem.h +++ b/Sparkle/SUAppcastItem.h @@ -52,7 +52,7 @@ SU_EXPORT @interface SUAppcastItem : NSObject @property (copy, readonly, nullable) NSNumber* phasedRolloutInterval; @property (nonatomic, copy, readonly) NSString *installationType; @property (copy, readonly, nullable) NSString *minimumAutoupdateVersion; -@property (nonatomic, readonly) NSSet *channels; +@property (nonatomic, readonly, nullable) NSString *channel; @property (getter=isDeltaUpdate, readonly) BOOL deltaUpdate; @property (getter=isCriticalUpdate, readonly) BOOL criticalUpdate; diff --git a/Sparkle/SUAppcastItem.m b/Sparkle/SUAppcastItem.m index 80900171..0489fd5d 100644 --- a/Sparkle/SUAppcastItem.m +++ b/Sparkle/SUAppcastItem.m @@ -73,7 +73,7 @@ static NSString *SUAppcastItemStateKey = @"SUAppcastItemState"; @synthesize state = _state; @synthesize hasCriticalInformation = _hasCriticalInformation; @synthesize informationalUpdateVersions = _informationalUpdateVersions; -@synthesize channels = _channels; +@synthesize channel = _channel; + (BOOL)supportsSecureCoding { @@ -131,6 +131,8 @@ static NSString *SUAppcastItemStateKey = @"SUAppcastItemState"; _propertiesDictionary = propertiesDictionary; _phasedRolloutInterval = [decoder decodeObjectOfClass:[NSNumber class] forKey:SUAppcastElementPhasedRolloutInterval]; + + _channel = [(NSString *)[decoder decodeObjectOfClass:[NSString class] forKey:SUAppcastElementChannel] copy]; } return self; @@ -207,6 +209,10 @@ static NSString *SUAppcastItemStateKey = @"SUAppcastItemState"; if (self.phasedRolloutInterval != nil) { [encoder encodeObject:self.phasedRolloutInterval forKey:SUAppcastElementPhasedRolloutInterval]; } + + if (self.channel != nil) { + [encoder encodeObject:self.channel forKey:SUAppcastElementChannel]; + } } - (BOOL)isDeltaUpdate @@ -346,11 +352,11 @@ static NSString *SUAppcastItemStateKey = @"SUAppcastItemState"; return nil; } - NSArray *channels = [dict objectForKey:SUAppcastElementChannels]; - if (channels == nil) { - _channels = [NSSet set]; + NSString *channel = [dict objectForKey:SUAppcastElementChannel]; + if (channel.length == 0) { + _channel = nil; } else { - _channels = [NSSet setWithArray:channels]; + _channel = channel; } _propertiesDictionary = [[NSDictionary alloc] initWithDictionary:dict]; diff --git a/Sparkle/SUConstants.h b/Sparkle/SUConstants.h index c9bd7ef1..b402346b 100644 --- a/Sparkle/SUConstants.h +++ b/Sparkle/SUConstants.h @@ -86,7 +86,7 @@ extern NSString *const SUAppcastElementReleaseNotesLink; extern NSString *const SUAppcastElementTags; extern NSString *const SUAppcastElementPhasedRolloutInterval; extern NSString *const SUAppcastElementInformationalUpdate; -extern NSString *const SUAppcastElementChannels; +extern NSString *const SUAppcastElementChannel; extern NSString *const SURSSAttributeURL; extern NSString *const SURSSAttributeLength; diff --git a/Sparkle/SUConstants.m b/Sparkle/SUConstants.m index 16c6914d..d584909c 100644 --- a/Sparkle/SUConstants.m +++ b/Sparkle/SUConstants.m @@ -8,7 +8,6 @@ #import "SUConstants.h" #import "SUErrors.h" -#import "SUExportedConstants.h" #ifndef DEBUG #define DEBUG 0 @@ -81,8 +80,7 @@ NSString *const SUAppcastElementReleaseNotesLink = @"sparkle:releaseNotesLink"; NSString *const SUAppcastElementTags = @"sparkle:tags"; NSString *const SUAppcastElementPhasedRolloutInterval = @"sparkle:phasedRolloutInterval"; NSString *const SUAppcastElementInformationalUpdate = @"sparkle:informationalUpdate"; -NSString *const SUAppcastElementChannels = @"sparkle:channels"; -NSString *const SUAppcastElementBetaChannel = @"sparkle:beta"; +NSString *const SUAppcastElementChannel = @"sparkle:channel"; NSString *const SURSSAttributeURL = @"url"; NSString *const SURSSAttributeLength = @"length"; diff --git a/Sparkle/SUExportedConstants.h b/Sparkle/SUExportedConstants.h deleted file mode 100644 index 0e44765d..00000000 --- a/Sparkle/SUExportedConstants.h +++ /dev/null @@ -1,25 +0,0 @@ -// -// SUExportedConstants.h -// Sparkle -// -// Created by Mayur Pawashe on 6/27/21. -// Copyright © 2021 Sparkle Project. All rights reserved. -// - -#ifndef SUExportedConstants_h -#define SUExportedConstants_h - -#if __has_feature(modules) -#if __has_warning("-Watimport-in-framework-header") -#pragma clang diagnostic ignored "-Watimport-in-framework-header" -#endif -@import Foundation; -#else -#import -#endif - -#import - -SU_EXPORT extern NSString *const SUAppcastElementBetaChannel; - -#endif /* SUExportedConstants_h */ diff --git a/Sparkle/Sparkle.h b/Sparkle/Sparkle.h index 8d7b640e..a048d267 100644 --- a/Sparkle/Sparkle.h +++ b/Sparkle/Sparkle.h @@ -22,7 +22,6 @@ #import #import #import -#import #import #import #import diff --git a/Tests/Resources/testappcast_channels.xml b/Tests/Resources/testappcast_channels.xml index db0f1b73..48729986 100644 --- a/Tests/Resources/testappcast_channels.xml +++ b/Tests/Resources/testappcast_channels.xml @@ -10,7 +10,7 @@ - + Version 3.0 86400 @@ -32,8 +32,7 @@ sparkle:edSignature="..." /> - - + @@ -41,29 +40,20 @@ 4.0 Sat, 26 Jul 2014 15:20:13 +0000 - - - + beta - + Version 5.0 5.0 - - - - + nightly Version 6.0 6.0 - - - - diff --git a/Tests/SUAppcastTest.swift b/Tests/SUAppcastTest.swift index b27c35bb..86e44dc0 100644 --- a/Tests/SUAppcastTest.swift +++ b/Tests/SUAppcastTest.swift @@ -90,7 +90,7 @@ class SUAppcastTest: XCTestCase { XCTAssertEqual(5, appcast.items.count) do { - let filteredAppcast = SUAppcastDriver.filterAppcast(appcast, forMacOSAndAllowedChannels: ["sparkle:beta", "sparkle:nightly"]) + let filteredAppcast = SUAppcastDriver.filterAppcast(appcast, forMacOSAndAllowedChannels: ["beta", "nightly"]) XCTAssertEqual(4, filteredAppcast.items.count) XCTAssertEqual("2.0", filteredAppcast.items[0].versionString) @@ -107,16 +107,15 @@ class SUAppcastTest: XCTestCase { } do { - let filteredAppcast = SUAppcastDriver.filterAppcast(appcast, forMacOSAndAllowedChannels: ["sparkle:beta"]) - XCTAssertEqual(4, filteredAppcast.items.count) + let filteredAppcast = SUAppcastDriver.filterAppcast(appcast, forMacOSAndAllowedChannels: ["beta"]) + XCTAssertEqual(3, filteredAppcast.items.count) XCTAssertEqual("2.0", filteredAppcast.items[0].versionString) XCTAssertEqual("3.0", filteredAppcast.items[1].versionString) XCTAssertEqual("4.0", filteredAppcast.items[2].versionString) - XCTAssertEqual("5.0", filteredAppcast.items[3].versionString) } do { - let filteredAppcast = SUAppcastDriver.filterAppcast(appcast, forMacOSAndAllowedChannels: ["sparkle:nightly"]) + let filteredAppcast = SUAppcastDriver.filterAppcast(appcast, forMacOSAndAllowedChannels: ["nightly"]) XCTAssertEqual(3, filteredAppcast.items.count) XCTAssertEqual("2.0", filteredAppcast.items[0].versionString) XCTAssertEqual("3.0", filteredAppcast.items[1].versionString) @@ -124,7 +123,7 @@ class SUAppcastTest: XCTestCase { } do { - let filteredAppcast = SUAppcastDriver.filterAppcast(appcast, forMacOSAndAllowedChannels: ["sparkle:madeup"]) + let filteredAppcast = SUAppcastDriver.filterAppcast(appcast, forMacOSAndAllowedChannels: ["madeup"]) XCTAssertEqual("2.0", filteredAppcast.items[0].versionString) XCTAssertEqual("3.0", filteredAppcast.items[1].versionString) XCTAssertEqual(2, filteredAppcast.items.count) From e5f1bd5e027f88b11e19fe9a2e01d1f4b2dc227c Mon Sep 17 00:00:00 2001 From: Zorg Date: Sun, 27 Jun 2021 22:05:13 -0700 Subject: [PATCH 08/11] Fix initializing recoveryOption --- Sparkle/SPUBasicUpdateDriver.m | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Sparkle/SPUBasicUpdateDriver.m b/Sparkle/SPUBasicUpdateDriver.m index 27ea84e5..2e1fe8fb 100644 --- a/Sparkle/SPUBasicUpdateDriver.m +++ b/Sparkle/SPUBasicUpdateDriver.m @@ -169,7 +169,6 @@ NSString *localizedDescription; NSString *recoverySuggestion; - NSString *recoveryOption; SPUNoUpdateFoundReason reason; if (latestAppcastItem != nil) { @@ -219,12 +218,15 @@ // was not provided (i.e, for a background update check) // We will need to assume the user is up to date if the feed doen't have any applicable update items // There could be update items on channels the updater is not subscribed to for example. But we can't tell the user about them. + // There could also only be update items available for other platforms or none at all. localizedDescription = SULocalizedString(@"You're up-to-date!", "Status message shown when the user checks for updates but is already current or the feed doesn't contain any updates."); recoverySuggestion = [NSString stringWithFormat:SULocalizedString(@"%@ %@ is currently the newest version available.", nil), [self.host name], [self.host displayVersion]]; - reason = SPUNoUpdateFoundReasonUnknown; + reason = SPUNoUpdateFoundReasonOnLatestVersion; } + NSString *recoveryOption = SULocalizedString(@"OK", nil); + NSMutableDictionary *userInfo = [NSMutableDictionary dictionaryWithDictionary:@{ NSLocalizedDescriptionKey: localizedDescription, @@ -237,8 +239,6 @@ userInfo[SPULatestAppcastItemFoundKey] = latestAppcastItem; } - recoveryOption = SULocalizedString(@"OK", nil); - NSError *notFoundError = [NSError errorWithDomain:SUSparkleErrorDomain From d8f5f584688f1518f546325853ab1933af4f067f Mon Sep 17 00:00:00 2001 From: Zorg Date: Sun, 27 Jun 2021 22:06:02 -0700 Subject: [PATCH 09/11] Restrict channel names to reasonable set of characters --- Sparkle/SPUUpdaterDelegate.h | 2 +- Sparkle/SUAppcastItem.m | 25 ++++++++++++++++++------- 2 files changed, 19 insertions(+), 8 deletions(-) diff --git a/Sparkle/SPUUpdaterDelegate.h b/Sparkle/SPUUpdaterDelegate.h index 8eb81f26..28aed683 100644 --- a/Sparkle/SPUUpdaterDelegate.h +++ b/Sparkle/SPUUpdaterDelegate.h @@ -174,7 +174,7 @@ typedef NS_ENUM(NSInteger, SPUUpdateCheck) If the is not present, the update item is posted to the default channel and can be found by any updater. - You can pick any name you'd like for the channel. + You can pick any name you'd like for the channel. The valid characters for channel names are letters, numbers, dashes, underscores, and periods. Note to use this feature, all app versions that your users may update from in your feed must use a version of Sparkle that supports this feature. This feature was added in Sparkle 2. diff --git a/Sparkle/SUAppcastItem.m b/Sparkle/SUAppcastItem.m index 25212b0a..3fa305a4 100644 --- a/Sparkle/SUAppcastItem.m +++ b/Sparkle/SUAppcastItem.m @@ -356,13 +356,6 @@ static NSString *SUAppcastItemStateKey = @"SUAppcastItemState"; } return nil; } - - NSString *channel = [dict objectForKey:SUAppcastElementChannel]; - if (channel.length == 0) { - _channel = nil; - } else { - _channel = channel; - } _propertiesDictionary = [[NSDictionary alloc] initWithDictionary:dict]; _dateString = [(NSString *)[dict objectForKey:SURSSElementPubDate] copy]; @@ -435,6 +428,24 @@ static NSString *SUAppcastItemStateKey = @"SUAppcastItemState"; _maximumSystemVersion = [(NSString *)[dict objectForKey:SUAppcastElementMaximumSystemVersion] copy]; _minimumAutoupdateVersion = [(NSString *)[dict objectForKey:SUAppcastElementMinimumAutoupdateVersion] copy]; + NSString *channel = [dict objectForKey:SUAppcastElementChannel]; + if (channel != nil) { + if (channel.length == 0) { + SULog(SULogLevelError, @"warning: Item with version '%@' has zero-length channel; this will be ignored.", newVersion); + _channel = nil; + } else { + // Reject characters in the channel name that may cause parsing problems in tools later + NSMutableCharacterSet *allowedCharacterSet = [NSMutableCharacterSet alphanumericCharacterSet]; + [allowedCharacterSet addCharactersInString:@"_.-"]; + if ([channel rangeOfCharacterFromSet:allowedCharacterSet.invertedSet].location != NSNotFound) { + SULog(SULogLevelError, @"warning: Item with version '%@' has channel with invalid name. This channel will be ignored. Only [a-zA-Z0-9._-] is allowed.", newVersion); + _channel = nil; + } else { + _channel = [channel copy]; + } + } + } + // Grab critical update information NSDictionary * _Nullable criticalUpdateDictionaryFromAppcast = (NSDictionary *)[dict objectForKey:SUAppcastElementCriticalUpdate]; NSArray *tags = [dict objectForKey:SUAppcastElementTags]; From 78a66c7ddabac07142db594f96f916baa4ee4f45 Mon Sep 17 00:00:00 2001 From: Zorg Date: Sun, 27 Jun 2021 22:40:16 -0700 Subject: [PATCH 10/11] Add channels support to sparkle-cli --- sparkle-cli/SPUCommandLineDriver.h | 2 +- sparkle-cli/SPUCommandLineDriver.m | 10 +++++++++- sparkle-cli/main.m | 20 ++++++++++++++++++-- 3 files changed, 28 insertions(+), 4 deletions(-) diff --git a/sparkle-cli/SPUCommandLineDriver.h b/sparkle-cli/SPUCommandLineDriver.h index bd387cea..56396381 100644 --- a/sparkle-cli/SPUCommandLineDriver.h +++ b/sparkle-cli/SPUCommandLineDriver.h @@ -14,7 +14,7 @@ NS_ASSUME_NONNULL_BEGIN @interface SPUCommandLineDriver : NSObject -- (nullable instancetype)initWithUpdateBundlePath:(NSString *)updateBundlePath applicationBundlePath:(nullable NSString *)applicationBundlePath customFeedURL:(nullable NSString *)customFeedURL updatePermissionResponse:(nullable SUUpdatePermissionResponse *)updatePermissionResponse deferInstallation:(BOOL)deferInstallation interactiveInstallation:(BOOL)interactiveInstallation verbose:(BOOL)verbose; +- (nullable instancetype)initWithUpdateBundlePath:(NSString *)updateBundlePath applicationBundlePath:(nullable NSString *)applicationBundlePath allowedChannels:(NSSet *)allowedChannels customFeedURL:(nullable NSString *)customFeedURL updatePermissionResponse:(nullable SUUpdatePermissionResponse *)updatePermissionResponse deferInstallation:(BOOL)deferInstallation interactiveInstallation:(BOOL)interactiveInstallation verbose:(BOOL)verbose; - (void)runAndCheckForUpdatesNow:(BOOL)checkForUpdatesNow; diff --git a/sparkle-cli/SPUCommandLineDriver.m b/sparkle-cli/SPUCommandLineDriver.m index 83e05c97..9f576185 100644 --- a/sparkle-cli/SPUCommandLineDriver.m +++ b/sparkle-cli/SPUCommandLineDriver.m @@ -16,6 +16,7 @@ @property (nonatomic, readonly) BOOL verbose; @property (nonatomic) BOOL probingForUpdates; @property (nonatomic, readonly) BOOL interactive; +@property (nonatomic, readonly) NSSet *allowedChannels; @property (nonatomic, copy, readonly, nullable) NSString *customFeedURL; @end @@ -26,9 +27,10 @@ @synthesize verbose = _verbose; @synthesize probingForUpdates = _probingForUpdates; @synthesize interactive = _interactive; +@synthesize allowedChannels = _allowedChannels; @synthesize customFeedURL = _customFeedURL; -- (instancetype)initWithUpdateBundlePath:(NSString *)updateBundlePath applicationBundlePath:(nullable NSString *)applicationBundlePath customFeedURL:(nullable NSString *)customFeedURL updatePermissionResponse:(nullable SUUpdatePermissionResponse *)updatePermissionResponse deferInstallation:(BOOL)deferInstallation interactiveInstallation:(BOOL)interactiveInstallation verbose:(BOOL)verbose +- (instancetype)initWithUpdateBundlePath:(NSString *)updateBundlePath applicationBundlePath:(nullable NSString *)applicationBundlePath allowedChannels:(NSSet *)allowedChannels customFeedURL:(nullable NSString *)customFeedURL updatePermissionResponse:(nullable SUUpdatePermissionResponse *)updatePermissionResponse deferInstallation:(BOOL)deferInstallation interactiveInstallation:(BOOL)interactiveInstallation verbose:(BOOL)verbose { self = [super init]; if (self != nil) { @@ -49,6 +51,7 @@ _verbose = verbose; _interactive = interactiveInstallation; + _allowedChannels = allowedChannels; _customFeedURL = [customFeedURL copy]; id userDriver = [[SPUCommandLineUserDriver alloc] initWithUpdatePermissionResponse:updatePermissionResponse deferInstallation:deferInstallation verbose:verbose]; @@ -84,6 +87,11 @@ } } +- (NSSet *)allowedChannelsForUpdater:(SPUUpdater *)__unused updater +{ + return self.allowedChannels; +} + - (nullable NSString *)feedURLStringForUpdater:(SPUUpdater *)__unused updater { return self.customFeedURL; diff --git a/sparkle-cli/main.m b/sparkle-cli/main.m index e6025bb1..904440c3 100644 --- a/sparkle-cli/main.m +++ b/sparkle-cli/main.m @@ -20,10 +20,11 @@ #define PROBE_FLAG "probe" #define INTERACTIVE_FLAG "interactive" #define FEED_URL_FLAG "feed-url" +#define CHANNELS_FLAG "channels" static void printUsage(char **argv) { - fprintf(stderr, "Usage: %s bundle [--%s ] [--%s] [--%s] [--%s] [--%s] [--%s] [--%s] [--%s]\n", argv[0], APPLICATION_FLAG, CHECK_NOW_FLAG, PROBE_FLAG, GRANT_AUTOMATIC_CHECKING_FLAG, SEND_PROFILE_FLAG, DEFER_FLAG, INTERACTIVE_FLAG, VERBOSE_FLAG); + fprintf(stderr, "Usage: %s bundle [--%s app-path] [--%s] [--%s] [--%s chan1,chan2,…] [--%s feed-url] [--%s] [--%s] [--%s] [--%s] [--%s]\n", argv[0], APPLICATION_FLAG, CHECK_NOW_FLAG, PROBE_FLAG, CHANNELS_FLAG, FEED_URL_FLAG, GRANT_AUTOMATIC_CHECKING_FLAG, SEND_PROFILE_FLAG, DEFER_FLAG, INTERACTIVE_FLAG, VERBOSE_FLAG); fprintf(stderr, "Description:\n"); fprintf(stderr, " Check if any new updates for a Sparkle supported bundle need to be installed.\n\n"); fprintf(stderr, " If any new updates need to be installed, the user application\n is terminated and the update is installed immediately unless --%s\n is specified. If the application was alive, then it will be relaunched after.\n\n", DEFER_FLAG); @@ -36,6 +37,7 @@ static void printUsage(char **argv) fprintf(stderr, " --%s\n Path to the application to watch for termination and to relaunch.\n If not provided, this is assumed to be the same as the bundle.\n", APPLICATION_FLAG); fprintf(stderr, " --%s\n Immediately checks for updates to install.\n Without this, updates are checked only when needed on a scheduled basis.\n", CHECK_NOW_FLAG); fprintf(stderr, " --%s\n Probe for updates. Check if any updates are available but do not install.\n An exit status of 0 is returned if a new update is available.\n", PROBE_FLAG); + fprintf(stderr, " --%s\n List of allowed Sparkle channels to look for updates in. By default,\n only the default channel is used.\n", CHANNELS_FLAG); fprintf(stderr, " --%s\n URL for appcast feed. This URL will be used for the feed instead of the one\n in the bundle's Info.plist or in the bundle's user defaults.\n", FEED_URL_FLAG); fprintf(stderr, " --%s\n Allows prompting the user for an authorization dialog prompt if the\n installer needs elevated privileges, or allows performing an interactive\n installer package.\n", INTERACTIVE_FLAG); fprintf(stderr, " --%s\n If update permission is requested, this enables automatic update checks.\n Note that this behavior may overwrite the user's defaults for the bundle.\n This option has no effect if --%s is passed, or if the\n user has replied to this request already, or if the developer configured\n to skip it.\n", GRANT_AUTOMATIC_CHECKING_FLAG, CHECK_NOW_FLAG); @@ -55,6 +57,7 @@ int main(int argc, char **argv) struct option longOptions[] = { {APPLICATION_FLAG, required_argument, NULL, 0}, + {CHANNELS_FLAG, required_argument, NULL, 0}, {FEED_URL_FLAG, required_argument, NULL, 0}, {DEFER_FLAG, no_argument, NULL, 0}, {VERBOSE_FLAG, no_argument, NULL, 0}, @@ -68,6 +71,7 @@ int main(int argc, char **argv) NSString *applicationPath = nil; NSString *feedURL = nil; + NSSet *channels = [NSSet set]; BOOL deferInstall = NO; BOOL verbose = NO; BOOL checkForUpdatesNow = NO; @@ -100,6 +104,18 @@ int main(int argc, char **argv) printUsage(argv); return EXIT_FAILURE; } + } else if (strcmp(CHANNELS_FLAG, longOptions[optionIndex].name) == 0) { + assert(optarg != NULL); + + NSString *channelsString = [[NSString alloc] initWithUTF8String:optarg]; + if (channelsString == nil) { + printUsage(argv); + return EXIT_FAILURE; + } + + if (channelsString.length > 0) { + channels = [NSSet setWithArray:[channelsString componentsSeparatedByString:@","]]; + } } else if (strcmp(DEFER_FLAG, longOptions[optionIndex].name) == 0) { deferInstall = YES; } else if (strcmp(VERBOSE_FLAG, longOptions[optionIndex].name) == 0) { @@ -146,7 +162,7 @@ int main(int argc, char **argv) updatePermissionResponse = [[SUUpdatePermissionResponse alloc] initWithAutomaticUpdateChecks:YES sendSystemProfile:sendProfile]; } - SPUCommandLineDriver *driver = [[SPUCommandLineDriver alloc] initWithUpdateBundlePath:updatePath applicationBundlePath:applicationPath customFeedURL:feedURL updatePermissionResponse:updatePermissionResponse deferInstallation:deferInstall interactiveInstallation:interactive verbose:verbose]; + SPUCommandLineDriver *driver = [[SPUCommandLineDriver alloc] initWithUpdateBundlePath:updatePath applicationBundlePath:applicationPath allowedChannels:channels customFeedURL:feedURL updatePermissionResponse:updatePermissionResponse deferInstallation:deferInstall interactiveInstallation:interactive verbose:verbose]; if (driver == nil) { fprintf(stderr, "Error: Failed to initialize updater. Are the bundle paths provided valid?\n"); return EXIT_FAILURE; From e0a7304a81dc52185fd73132f6915edb59950db2 Mon Sep 17 00:00:00 2001 From: Zorg Date: Sun, 27 Jun 2021 23:08:50 -0700 Subject: [PATCH 11/11] Remove outdated comment --- Sparkle/SUAppcastDriver.m | 1 - 1 file changed, 1 deletion(-) diff --git a/Sparkle/SUAppcastDriver.m b/Sparkle/SUAppcastDriver.m index e4ccaf37..d7f125e4 100644 --- a/Sparkle/SUAppcastDriver.m +++ b/Sparkle/SUAppcastDriver.m @@ -245,7 +245,6 @@ return YES; } - // Item channels and allowed channels must have one channel in common return [allowedChannels containsObject:channel]; }]; }