From e51eb32403081ecebfd4cf8493964ecd5adc05ae Mon Sep 17 00:00:00 2001
From: Zorg <zorgiepoo@gmail.com>
Date: Sat, 24 Apr 2021 21:42:33 -0700
Subject: [PATCH 1/4] Fix bestValidUpdateInAppcast: not handling delta updates
 correctly

All the top level appcast items are non-delta items. The client needs to give us back a non-delta update item. We then fetch the appropriate delta item ourselves.
---
 Sparkle/SPUBasicUpdateDriver.h     |  2 +-
 Sparkle/SPUBasicUpdateDriver.m     |  4 +-
 Sparkle/SPUCoreBasedUpdateDriver.m |  8 +--
 Sparkle/SUAppcast.h                |  3 +-
 Sparkle/SUAppcast.m                |  8 +--
 Sparkle/SUAppcastDriver.h          |  2 +-
 Sparkle/SUAppcastDriver.m          | 85 +++++++++++++++++++-----------
 7 files changed, 64 insertions(+), 48 deletions(-)

diff --git a/Sparkle/SPUBasicUpdateDriver.h b/Sparkle/SPUBasicUpdateDriver.h
index 8598024fc8..107dd0200f 100644
--- a/Sparkle/SPUBasicUpdateDriver.h
+++ b/Sparkle/SPUBasicUpdateDriver.h
@@ -38,7 +38,7 @@ NS_ASSUME_NONNULL_BEGIN
 
 - (void)resumeUpdate:(id<SPUResumableUpdate>)resumableUpdate completion:(SPUUpdateDriverCompletion)completionBlock;
 
-@property (nullable, nonatomic, readonly) SUAppcastItem *nonDeltaUpdateItem;
+@property (nullable, nonatomic, readonly) SUAppcastItem *secondaryUpdateItem;
 
 - (void)abortUpdateAndShowNextUpdateImmediately:(BOOL)shouldSignalShowingUpdate resumableUpdate:(id<SPUResumableUpdate> _Nullable)resumableUpdate error:(nullable NSError *)error;
 
diff --git a/Sparkle/SPUBasicUpdateDriver.m b/Sparkle/SPUBasicUpdateDriver.m
index 49929c1af5..2a3be1d4a4 100644
--- a/Sparkle/SPUBasicUpdateDriver.m
+++ b/Sparkle/SPUBasicUpdateDriver.m
@@ -112,9 +112,9 @@ - (void)resumeUpdate:(id<SPUResumableUpdate>)resumableUpdate completion:(SPUUpda
     [self notifyResumableUpdateItem:resumableUpdate.updateItem systemDomain:nil];
 }
 
-- (SUAppcastItem *)nonDeltaUpdateItem
+- (SUAppcastItem *)secondaryUpdateItem
 {
-    return self.appcastDriver.nonDeltaUpdateItem;
+    return self.appcastDriver.secondaryUpdateItem;
 }
 
 - (void)didFailToFetchAppcastWithError:(NSError *)error
diff --git a/Sparkle/SPUCoreBasedUpdateDriver.m b/Sparkle/SPUCoreBasedUpdateDriver.m
index a769fe08fe..4b5c435f9a 100644
--- a/Sparkle/SPUCoreBasedUpdateDriver.m
+++ b/Sparkle/SPUCoreBasedUpdateDriver.m
@@ -381,16 +381,16 @@ - (void)basicDriverIsRequestingAbortUpdateWithError:(nullable NSError *)error
 
 - (void)installerDidFailToApplyDeltaUpdate
 {
-    SUAppcastItem *nonDeltaUpdateItem = self.basicDriver.nonDeltaUpdateItem;
-    assert(nonDeltaUpdateItem != nil);
+    SUAppcastItem *secondaryUpdateItem = self.basicDriver.secondaryUpdateItem;
+    assert(secondaryUpdateItem != nil);
     
     BOOL backgroundDownload = self.downloadDriver.inBackground;
     
     [self clearDownloadedUpdate];
     
     // Fall back to the non-delta update. Note that we don't want to trigger another update was found event.
-    self.updateItem = nonDeltaUpdateItem;
-    [self downloadUpdateFromAppcastItem:nonDeltaUpdateItem inBackground:backgroundDownload];
+    self.updateItem = secondaryUpdateItem;
+    [self downloadUpdateFromAppcastItem:secondaryUpdateItem inBackground:backgroundDownload];
 }
 
 - (void)abortUpdateAndShowNextUpdateImmediately:(BOOL)shouldShowUpdateImmediately error:(nullable NSError *)error
diff --git a/Sparkle/SUAppcast.h b/Sparkle/SUAppcast.h
index 5ae0f0fabc..7030dff314 100644
--- a/Sparkle/SUAppcast.h
+++ b/Sparkle/SUAppcast.h
@@ -28,10 +28,9 @@ SU_EXPORT @interface SUAppcast : NSObject<NSURLDownloadDelegate>
 @property (copy, nullable) NSDictionary<NSString *, NSString *> *httpHeaders;
 
 - (void)fetchAppcastFromURL:(NSURL *)url inBackground:(BOOL)bg completionBlock:(void (^)(NSError *_Nullable))err;
-- (SUAppcast *)copyWithoutDeltaUpdates;
 - (SUAppcast *)copyByFilteringItems:(BOOL (^)(SUAppcastItem *))filterBlock;
 
-@property (readonly, copy, nullable) NSArray *items;
+@property (readonly, copy, nullable) NSArray<SUAppcastItem *> *items;
 @end
 
 NS_ASSUME_NONNULL_END
diff --git a/Sparkle/SUAppcast.m b/Sparkle/SUAppcast.m
index 640960e833..467854a73f 100644
--- a/Sparkle/SUAppcast.m
+++ b/Sparkle/SUAppcast.m
@@ -47,7 +47,7 @@ - (NSDictionary *)attributesAsDictionary
 
 @interface SUAppcast () <NSURLDownloadDelegate>
 @property (strong) void (^completionBlock)(NSError *);
-@property (copy) NSArray *items;
+@property (copy) NSArray<SUAppcastItem *> *items;
 - (void)reportError:(NSError *)error;
 - (NSXMLNode *)bestNodeInNodes:(NSArray *)nodes;
 @end
@@ -306,12 +306,6 @@ - (NSXMLNode *)bestNodeInNodes:(NSArray *)nodes
     return [nodes objectAtIndex:i];
 }
 
-- (SUAppcast *)copyWithoutDeltaUpdates {
-    return [self copyByFilteringItems:^(SUAppcastItem *item) {
-        return (BOOL)![item isDeltaUpdate];
-    }];
-}
-
 - (SUAppcast *)copyByFilteringItems:(BOOL (^)(SUAppcastItem *))filterBlock
 {
     SUAppcast *other = [SUAppcast new];
diff --git a/Sparkle/SUAppcastDriver.h b/Sparkle/SUAppcastDriver.h
index b5b2dbf280..d9a6a3cfa6 100644
--- a/Sparkle/SUAppcastDriver.h
+++ b/Sparkle/SUAppcastDriver.h
@@ -28,7 +28,7 @@ NS_ASSUME_NONNULL_BEGIN
 
 - (void)loadAppcastFromURL:(NSURL *)appcastURL userAgent:(NSString *)userAgent httpHeaders:(NSDictionary * _Nullable)httpHeaders inBackground:(BOOL)background includesSkippedUpdates:(BOOL)includesSkippedUpdates;
 
-@property (nullable, nonatomic, readonly) SUAppcastItem *nonDeltaUpdateItem;
+@property (nullable, nonatomic, readonly) SUAppcastItem *secondaryUpdateItem;
 
 @end
 
diff --git a/Sparkle/SUAppcastDriver.m b/Sparkle/SUAppcastDriver.m
index 24a0e69296..9dfb4a01e3 100644
--- a/Sparkle/SUAppcastDriver.m
+++ b/Sparkle/SUAppcastDriver.m
@@ -16,6 +16,7 @@
 #import "SUHost.h"
 #import "SUConstants.h"
 #import "SUPhasedUpdateGroupInfo.h"
+#import "SULog.h"
 
 
 #include "AppKitPrevention.h"
@@ -26,7 +27,7 @@ @interface SUAppcastDriver ()
 @property (nonatomic, copy) NSString *userAgent;
 @property (nullable, nonatomic, readonly, weak) id updater;
 @property (nullable, nonatomic, readonly, weak) id <SPUUpdaterDelegate> updaterDelegate;
-@property (nullable, nonatomic) SUAppcastItem *nonDeltaUpdateItem;
+@property (nullable, nonatomic) SUAppcastItem *secondaryUpdateItem;
 @property (nullable, nonatomic, readonly, weak) id <SUAppcastDriverDelegate> delegate;
 
 @end
@@ -37,7 +38,7 @@ @implementation SUAppcastDriver
 @synthesize userAgent = _userAgent;
 @synthesize updater = _updater;
 @synthesize updaterDelegate = _updaterDelegate;
-@synthesize nonDeltaUpdateItem = _nonDeltaUpdateItem;
+@synthesize secondaryUpdateItem = _secondaryUpdateItem;
 @synthesize delegate = _delegate;
 
 - (instancetype)initWithHost:(SUHost *)host updater:(id)updater updaterDelegate:(id <SPUUpdaterDelegate>)updaterDelegate delegate:(id <SUAppcastDriverDelegate>)delegate
@@ -68,6 +69,24 @@ - (void)loadAppcastFromURL:(NSURL *)appcastURL userAgent:(NSString *)userAgent h
     }];
 }
 
+- (SUAppcastItem * _Nullable)preferredUpdateForRegularAppcastItem:(SUAppcastItem * _Nullable)regularItem secondaryUpdate:(SUAppcastItem * __autoreleasing _Nullable *)secondaryUpdate
+{
+    if (regularItem == nil) {
+        return nil;
+    }
+    
+    SUAppcastItem *deltaItem = [[self class] deltaUpdateFromAppcastItem:regularItem hostVersion:self.host.version];
+    
+    if (deltaItem != nil) {
+        if (secondaryUpdate != NULL) {
+            *secondaryUpdate = regularItem;
+        }
+        return deltaItem;
+    } else {
+        return regularItem;
+    }
+}
+
 - (void)appcastDidFinishLoading:(SUAppcast *)loadedAppcast inBackground:(BOOL)background includesSkippedUpdates:(BOOL)includesSkippedUpdates
 {
     [self.delegate didFinishLoadingAppcast:loadedAppcast];
@@ -79,35 +98,33 @@ - (void)appcastDidFinishLoading:(SUAppcast *)loadedAppcast inBackground:(BOOL)ba
     SUAppcast *supportedAppcast = [[self class] filterSupportedAppcast:loadedAppcast phasedUpdateGroup:phasedUpdateGroup];
     
     SUAppcastItem *item = nil;
-    SUAppcastItem *nonDeltaUpdateItem = nil;
+    SUAppcastItem *secondaryItem = nil;
     
-    // Now we have to find the best valid update in the appcast.
+    // Find the best valid update in the appcast by asking the delegate
     if ([self.updaterDelegate respondsToSelector:@selector((bestValidUpdateInAppcast:forUpdater:))])
     {
-        item = [self.updaterDelegate bestValidUpdateInAppcast:supportedAppcast forUpdater:(id _Nonnull)self.updater];
-    }
-    
-    if (item != nil)
-    {
-        // Does the delegate want to handle it?
-        if ([item isDeltaUpdate]) {
-            nonDeltaUpdateItem = [self.updaterDelegate bestValidUpdateInAppcast:[supportedAppcast copyWithoutDeltaUpdates] forUpdater:(id _Nonnull)self.updater];
+        SUAppcastItem *regularItem = [self.updaterDelegate bestValidUpdateInAppcast:supportedAppcast forUpdater:(id _Nonnull)self.updater];
+        
+        assert(!regularItem.deltaUpdate);
+        if (regularItem.deltaUpdate) {
+            // Client would have to go out of their way to examine the .deltaUpdates to return one
+            // This is very unlikely, and we need them to give us a regular update item back
+            SULog(SULogLevelError, @"Error: -bestValidUpdateInAppcast:forUpdater: cannot return a delta update item");
+        } else {
+            item = [self preferredUpdateForRegularAppcastItem:regularItem secondaryUpdate:&secondaryItem];
         }
     }
-    else // If not, we'll take care of it ourselves.
+    
+    // Take care of finding best appcast item ourselves if delegate does not
+    if (item == nil)
     {
-        // Find the best supported update
-        SUAppcastItem *deltaUpdateItem = nil;
-        item = [[self class] bestItemFromAppcastItems:supportedAppcast.items getDeltaItem:&deltaUpdateItem withHostVersion:self.host.version comparator:[self versionComparator]];
+        SUAppcastItem *regularItem = [[self class] bestItemFromAppcastItems:supportedAppcast.items comparator:[self versionComparator]];
         
-        if (item && deltaUpdateItem) {
-            nonDeltaUpdateItem = item;
-            item = deltaUpdateItem;
-        }
+        item = [self preferredUpdateForRegularAppcastItem:regularItem secondaryUpdate:&secondaryItem];
     }
     
     if ([self itemContainsValidUpdate:item inBackground:background includesSkippedUpdates:includesSkippedUpdates]) {
-        self.nonDeltaUpdateItem = nonDeltaUpdateItem;
+        self.secondaryUpdateItem = secondaryItem;
         [self.delegate didFindValidUpdateWithAppcastItem:item preventsAutoupdate:[self itemPreventsAutoupdate:item]];
     } else {
         NSComparisonResult hostToLatestAppcastItemComparisonResult = (item != nil) ? [[self versionComparator] compareVersion:[self.host version] toVersion:[item versionString]] : 0;
@@ -125,9 +142,12 @@ + (SUAppcast *)filterSupportedAppcast:(SUAppcast *)appcast phasedUpdateGroup:(NS
     }];
 }
 
-// This method is used by unit tests
-// This method should not do *any* filtering, only version comparing
-+ (SUAppcastItem *)bestItemFromAppcastItems:(NSArray *)appcastItems getDeltaItem:(SUAppcastItem * __autoreleasing *)deltaItem withHostVersion:(NSString *)hostVersion comparator:(id<SUVersionComparison>)comparator
++ (SUAppcastItem * _Nullable)deltaUpdateFromAppcastItem:(SUAppcastItem *)appcastItem hostVersion:(NSString *)hostVersion
+{
+    return appcastItem.deltaUpdates[hostVersion];
+}
+
++ (SUAppcastItem * _Nullable)bestItemFromAppcastItems:(NSArray *)appcastItems comparator:(id<SUVersionComparison>)comparator
 {
     SUAppcastItem *item = nil;
     for(SUAppcastItem *candidate in appcastItems) {
@@ -135,14 +155,17 @@ + (SUAppcastItem *)bestItemFromAppcastItems:(NSArray *)appcastItems getDeltaItem
             item = candidate;
         }
     }
-    
-    if (item && deltaItem) {
-        SUAppcastItem *deltaUpdateItem = [item deltaUpdates][hostVersion];
-        if (deltaUpdateItem) {
-            *deltaItem = deltaUpdateItem;
-        }
+    return item;
+}
+
+// This method is used by unit tests
+// This method should not do *any* filtering, only version comparing
++ (SUAppcastItem *)bestItemFromAppcastItems:(NSArray *)appcastItems getDeltaItem:(SUAppcastItem * __autoreleasing *)deltaItem withHostVersion:(NSString *)hostVersion comparator:(id<SUVersionComparison>)comparator
+{
+    SUAppcastItem *item = [self bestItemFromAppcastItems:appcastItems comparator:comparator];
+    if (item != nil && deltaItem != NULL) {
+        *deltaItem = [self deltaUpdateFromAppcastItem:item hostVersion:hostVersion];
     }
-    
     return item;
 }
 

From 71557fac858668a016197491899449bf30fcd76f Mon Sep 17 00:00:00 2001
From: Zorg <zorgiepoo@gmail.com>
Date: Sun, 25 Apr 2021 12:15:44 -0700
Subject: [PATCH 2/4] Include secondary non-delta update to resumable updates

---
 Sparkle/SPUAutomaticUpdateDriver.m |  6 +++---
 Sparkle/SPUBasicUpdateDriver.h     |  4 +---
 Sparkle/SPUBasicUpdateDriver.m     | 21 ++++++++-------------
 Sparkle/SPUCoreBasedUpdateDriver.h |  6 +++---
 Sparkle/SPUCoreBasedUpdateDriver.m | 25 +++++++++++++++----------
 Sparkle/SPUDownloadDriver.h        |  2 +-
 Sparkle/SPUDownloadDriver.m        |  7 +++++--
 Sparkle/SPUDownloadedUpdate.h      |  2 +-
 Sparkle/SPUDownloadedUpdate.m      |  4 +++-
 Sparkle/SPUInformationalUpdate.h   |  2 +-
 Sparkle/SPUInformationalUpdate.m   |  4 +++-
 Sparkle/SPUProbingUpdateDriver.m   |  2 +-
 Sparkle/SPUResumableUpdate.h       |  1 +
 Sparkle/SPUUIBasedUpdateDriver.m   |  6 +++---
 Sparkle/SUAppcastDriver.h          |  4 +---
 Sparkle/SUAppcastDriver.m          |  5 +----
 16 files changed, 51 insertions(+), 50 deletions(-)

diff --git a/Sparkle/SPUAutomaticUpdateDriver.m b/Sparkle/SPUAutomaticUpdateDriver.m
index c6626fa344..8d2ce908fc 100644
--- a/Sparkle/SPUAutomaticUpdateDriver.m
+++ b/Sparkle/SPUAutomaticUpdateDriver.m
@@ -77,15 +77,15 @@ - (void)resumeUpdate:(id<SPUResumableUpdate>)__unused resumableUpdate completion
     SULog(SULogLevelError, @"Error: resumeDownloadedUpdate:completion: called on SPUAutomaticUpdateDriver");
 }
 
-- (void)basicDriverDidFindUpdateWithAppcastItem:(SUAppcastItem *)updateItem preventsAutoupdate:(BOOL)preventsAutoupdate
+- (void)basicDriverDidFindUpdateWithAppcastItem:(SUAppcastItem *)updateItem secondaryAppcastItem:(SUAppcastItem *)secondaryUpdateItem preventsAutoupdate:(BOOL)preventsAutoupdate
 {
     self.updateItem = updateItem;
     
     if (updateItem.isInformationOnlyUpdate || preventsAutoupdate) {
-        [self.coreDriver deferInformationalUpdate:updateItem preventsAutoupdate:preventsAutoupdate];
+        [self.coreDriver deferInformationalUpdate:updateItem secondaryUpdate:secondaryUpdateItem preventsAutoupdate:preventsAutoupdate];
         [self abortUpdate];
     } else {
-        [self.coreDriver downloadUpdateFromAppcastItem:updateItem inBackground:YES];
+        [self.coreDriver downloadUpdateFromAppcastItem:updateItem secondaryAppcastItem:secondaryUpdateItem inBackground:YES];
     }
 }
 
diff --git a/Sparkle/SPUBasicUpdateDriver.h b/Sparkle/SPUBasicUpdateDriver.h
index 107dd0200f..dcde2abfef 100644
--- a/Sparkle/SPUBasicUpdateDriver.h
+++ b/Sparkle/SPUBasicUpdateDriver.h
@@ -16,7 +16,7 @@ NS_ASSUME_NONNULL_BEGIN
 
 @protocol SPUBasicUpdateDriverDelegate <NSObject>
 
-- (void)basicDriverDidFindUpdateWithAppcastItem:(SUAppcastItem *)appcastItem preventsAutoupdate:(BOOL)preventsAutoupdate systemDomain:(NSNumber * _Nullable)systemDomain;
+- (void)basicDriverDidFindUpdateWithAppcastItem:(SUAppcastItem *)appcastItem secondaryAppcastItem:(SUAppcastItem *)secondaryAppcastItem preventsAutoupdate:(BOOL)preventsAutoupdate systemDomain:(NSNumber * _Nullable)systemDomain;
 
 - (void)basicDriverIsRequestingAbortUpdateWithError:(nullable NSError *)error;
 
@@ -38,8 +38,6 @@ NS_ASSUME_NONNULL_BEGIN
 
 - (void)resumeUpdate:(id<SPUResumableUpdate>)resumableUpdate completion:(SPUUpdateDriverCompletion)completionBlock;
 
-@property (nullable, nonatomic, readonly) SUAppcastItem *secondaryUpdateItem;
-
 - (void)abortUpdateAndShowNextUpdateImmediately:(BOOL)shouldSignalShowingUpdate resumableUpdate:(id<SPUResumableUpdate> _Nullable)resumableUpdate error:(nullable NSError *)error;
 
 @end
diff --git a/Sparkle/SPUBasicUpdateDriver.m b/Sparkle/SPUBasicUpdateDriver.m
index 2a3be1d4a4..c6427f1ca8 100644
--- a/Sparkle/SPUBasicUpdateDriver.m
+++ b/Sparkle/SPUBasicUpdateDriver.m
@@ -79,7 +79,7 @@ - (void)checkForUpdatesAtAppcastURL:(NSURL *)appcastURL withUserAgent:(NSString
     }
 }
 
-- (void)notifyResumableUpdateItem:(SUAppcastItem *)updateItem systemDomain:(NSNumber * _Nullable)systemDomain
+- (void)notifyResumableUpdateItem:(SUAppcastItem *)updateItem secondaryUpdateItem:(SUAppcastItem *)secondaryUpdateItem systemDomain:(NSNumber * _Nullable)systemDomain
 {
     if (updateItem == nil) {
         [self.delegate basicDriverIsRequestingAbortUpdateWithError:[NSError errorWithDomain:SUSparkleErrorDomain code:SUResumeAppcastError userInfo:@{ NSLocalizedDescriptionKey: SULocalizedString(@"Failed to resume installing update.", nil) }]];
@@ -88,7 +88,7 @@ - (void)notifyResumableUpdateItem:(SUAppcastItem *)updateItem systemDomain:(NSNu
         [self notifyFinishLoadingAppcast];
         
         SUAppcastItem *nonNullUpdateItem = updateItem;
-        [self notifyFoundValidUpdateWithAppcastItem:nonNullUpdateItem preventsAutoupdate:NO systemDomain:systemDomain];
+        [self notifyFoundValidUpdateWithAppcastItem:nonNullUpdateItem secondaryAppcastItem:secondaryUpdateItem preventsAutoupdate:NO systemDomain:systemDomain];
     }
 }
 
@@ -100,7 +100,7 @@ - (void)resumeInstallingUpdateWithCompletion:(SPUUpdateDriverCompletion)completi
     assert(hostBundleIdentifier != nil);
     [SPUProbeInstallStatus probeInstallerUpdateItemForHostBundleIdentifier:hostBundleIdentifier completion:^(SPUInstallationInfo * _Nullable installationInfo) {
         dispatch_async(dispatch_get_main_queue(), ^{
-            [self notifyResumableUpdateItem:installationInfo.appcastItem systemDomain:@(installationInfo.systemDomain)];
+            [self notifyResumableUpdateItem:installationInfo.appcastItem secondaryUpdateItem:nil systemDomain:@(installationInfo.systemDomain)];
         });
     }];
 }
@@ -109,12 +109,7 @@ - (void)resumeUpdate:(id<SPUResumableUpdate>)resumableUpdate completion:(SPUUpda
 {
     self.completionBlock = completionBlock;
     
-    [self notifyResumableUpdateItem:resumableUpdate.updateItem systemDomain:nil];
-}
-
-- (SUAppcastItem *)secondaryUpdateItem
-{
-    return self.appcastDriver.secondaryUpdateItem;
+    [self notifyResumableUpdateItem:resumableUpdate.updateItem secondaryUpdateItem:resumableUpdate.secondaryUpdateItem systemDomain:nil];
 }
 
 - (void)didFailToFetchAppcastWithError:(NSError *)error
@@ -142,7 +137,7 @@ - (void)didFinishLoadingAppcast:(SUAppcast *)appcast
     }
 }
 
-- (void)notifyFoundValidUpdateWithAppcastItem:(SUAppcastItem *)updateItem preventsAutoupdate:(BOOL)preventsAutoupdate systemDomain:(NSNumber * _Nullable)systemDomain
+- (void)notifyFoundValidUpdateWithAppcastItem:(SUAppcastItem *)updateItem secondaryAppcastItem:(SUAppcastItem *)secondaryUpdateItem preventsAutoupdate:(BOOL)preventsAutoupdate systemDomain:(NSNumber * _Nullable)systemDomain
 {
     if (!self.aborted) {
         [[NSNotificationCenter defaultCenter] postNotificationName:SUUpdaterDidFindValidUpdateNotification
@@ -153,13 +148,13 @@ - (void)notifyFoundValidUpdateWithAppcastItem:(SUAppcastItem *)updateItem preven
             [self.updaterDelegate updater:self.updater didFindValidUpdate:updateItem];
         }
         
-        [self.delegate basicDriverDidFindUpdateWithAppcastItem:updateItem preventsAutoupdate:preventsAutoupdate systemDomain:systemDomain];
+        [self.delegate basicDriverDidFindUpdateWithAppcastItem:updateItem secondaryAppcastItem:secondaryUpdateItem preventsAutoupdate:preventsAutoupdate systemDomain:systemDomain];
     }
 }
 
-- (void)didFindValidUpdateWithAppcastItem:(SUAppcastItem *)updateItem preventsAutoupdate:(BOOL)preventsAutoupdate
+- (void)didFindValidUpdateWithAppcastItem:(SUAppcastItem *)updateItem secondaryAppcastItem:(SUAppcastItem *)secondaryAppcastItem preventsAutoupdate:(BOOL)preventsAutoupdate
 {
-    [self notifyFoundValidUpdateWithAppcastItem:updateItem preventsAutoupdate:preventsAutoupdate systemDomain:nil];
+    [self notifyFoundValidUpdateWithAppcastItem:updateItem secondaryAppcastItem:secondaryAppcastItem preventsAutoupdate:preventsAutoupdate systemDomain:nil];
 }
 
 - (void)didNotFindUpdateWithLatestAppcastItem:(nullable SUAppcastItem *)latestAppcastItem hostToLatestAppcastItemComparisonResult:(NSComparisonResult)hostToLatestAppcastItemComparisonResult
diff --git a/Sparkle/SPUCoreBasedUpdateDriver.h b/Sparkle/SPUCoreBasedUpdateDriver.h
index 57806ee8e4..f936e9b00c 100644
--- a/Sparkle/SPUCoreBasedUpdateDriver.h
+++ b/Sparkle/SPUCoreBasedUpdateDriver.h
@@ -17,7 +17,7 @@ NS_ASSUME_NONNULL_BEGIN
 
 @protocol SPUCoreBasedUpdateDriverDelegate <NSObject>
 
-- (void)basicDriverDidFindUpdateWithAppcastItem:(SUAppcastItem *)updateItem preventsAutoupdate:(BOOL)preventsAutoupdate;
+- (void)basicDriverDidFindUpdateWithAppcastItem:(SUAppcastItem *)updateItem secondaryAppcastItem:(SUAppcastItem *)secondaryAppcastItem preventsAutoupdate:(BOOL)preventsAutoupdate;
 
 - (void)installerDidFinishPreparationAndWillInstallImmediately:(BOOL)willInstallImmediately silently:(BOOL)willInstallSilently;
 
@@ -61,9 +61,9 @@ NS_ASSUME_NONNULL_BEGIN
 
 - (void)resumeUpdate:(id<SPUResumableUpdate>)resumableUpdate completion:(SPUUpdateDriverCompletion)completionBlock;
 
-- (void)downloadUpdateFromAppcastItem:(SUAppcastItem *)updateItem inBackground:(BOOL)background;
+- (void)downloadUpdateFromAppcastItem:(SUAppcastItem *)updateItem secondaryAppcastItem:(SUAppcastItem * _Nullable)secondaryUpdateItem inBackground:(BOOL)background;
 
-- (void)deferInformationalUpdate:(SUAppcastItem *)updateItem preventsAutoupdate:(BOOL)preventsAutoupdate;
+- (void)deferInformationalUpdate:(SUAppcastItem *)updateItem secondaryUpdate:(SUAppcastItem *)secondaryUpdateItem preventsAutoupdate:(BOOL)preventsAutoupdate;
 
 - (void)extractDownloadedUpdate;
 
diff --git a/Sparkle/SPUCoreBasedUpdateDriver.m b/Sparkle/SPUCoreBasedUpdateDriver.m
index 4b5c435f9a..b095524ffe 100644
--- a/Sparkle/SPUCoreBasedUpdateDriver.m
+++ b/Sparkle/SPUCoreBasedUpdateDriver.m
@@ -32,6 +32,7 @@ @interface SPUCoreBasedUpdateDriver () <SPUBasicUpdateDriverDelegate, SPUDownloa
 @property (nonatomic, readonly) SPUInstallerDriver *installerDriver;
 @property (nonatomic, weak, readonly) id<SPUCoreBasedUpdateDriverDelegate> delegate;
 @property (nonatomic) SUAppcastItem *updateItem;
+@property (nonatomic) SUAppcastItem *secondaryUpdateItem;
 @property (nonatomic) id<SPUResumableUpdate> resumableUpdate;
 @property (nonatomic) SPUDownloadedUpdate *downloadedUpdateForRemoval;
 
@@ -53,6 +54,7 @@ @implementation SPUCoreBasedUpdateDriver
 @synthesize installerDriver = _installerDriver;
 @synthesize delegate = _delegate;
 @synthesize updateItem = _updateItem;
+@synthesize secondaryUpdateItem = _secondaryUpdateItem;
 @synthesize host = _host;
 @synthesize resumingInstallingUpdate = _resumingInstallingUpdate;
 @synthesize silentInstall = _silentInstall;
@@ -147,33 +149,34 @@ - (void)basicDriverDidFinishLoadingAppcast
     }
 }
 
-- (void)basicDriverDidFindUpdateWithAppcastItem:(SUAppcastItem *)updateItem preventsAutoupdate:(BOOL)preventsAutoupdate systemDomain:(NSNumber * _Nullable)systemDomain
+- (void)basicDriverDidFindUpdateWithAppcastItem:(SUAppcastItem *)updateItem secondaryAppcastItem:(SUAppcastItem *)secondaryUpdateItem preventsAutoupdate:(BOOL)preventsAutoupdate systemDomain:(NSNumber * _Nullable)systemDomain
 {
     self.updateItem = updateItem;
+    self.secondaryUpdateItem = secondaryUpdateItem;
     
     if (self.resumingInstallingUpdate) {
         assert(systemDomain != nil);
         [self.installerDriver resumeInstallingUpdateWithUpdateItem:updateItem systemDomain:systemDomain.boolValue];
-        [self.delegate basicDriverDidFindUpdateWithAppcastItem:updateItem preventsAutoupdate:preventsAutoupdate];
+        [self.delegate basicDriverDidFindUpdateWithAppcastItem:updateItem secondaryAppcastItem:secondaryUpdateItem preventsAutoupdate:preventsAutoupdate];
     } else {
         if (!self.preventsInstallerInteraction) {
             // Simple case - delegate allows interaction, so we should continue along
-            [self.delegate basicDriverDidFindUpdateWithAppcastItem:updateItem preventsAutoupdate:preventsAutoupdate];
+            [self.delegate basicDriverDidFindUpdateWithAppcastItem:updateItem secondaryAppcastItem:secondaryUpdateItem preventsAutoupdate:preventsAutoupdate];
         } else {
             // Package type installations will always require installer interaction as long as we don't support running as root
             // If it's not a package type installation, we should be okay since we did an auth check before checking for updates above
             if (![updateItem.installationType isEqualToString:SPUInstallationTypeApplication]) {
                 [self.delegate coreDriverIsRequestingAbortUpdateWithError:[NSError errorWithDomain:SUSparkleErrorDomain code:SUNotAllowedInteractionError userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"A new update is available but cannot be installed because interaction has been prevented."] }]];
             } else {
-                [self.delegate basicDriverDidFindUpdateWithAppcastItem:updateItem preventsAutoupdate:preventsAutoupdate];
+                [self.delegate basicDriverDidFindUpdateWithAppcastItem:updateItem secondaryAppcastItem:secondaryUpdateItem preventsAutoupdate:preventsAutoupdate];
             }
         }
     }
 }
 
-- (void)downloadUpdateFromAppcastItem:(SUAppcastItem *)updateItem inBackground:(BOOL)background
+- (void)downloadUpdateFromAppcastItem:(SUAppcastItem *)updateItem secondaryAppcastItem:(SUAppcastItem * _Nullable)secondaryUpdateItem inBackground:(BOOL)background
 {
-    self.downloadDriver = [[SPUDownloadDriver alloc] initWithUpdateItem:updateItem host:self.host userAgent:self.userAgent httpHeaders:self.httpHeaders inBackground:background delegate:self];
+    self.downloadDriver = [[SPUDownloadDriver alloc] initWithUpdateItem:updateItem secondaryUpdateItem:secondaryUpdateItem host:self.host userAgent:self.userAgent httpHeaders:self.httpHeaders inBackground:background delegate:self];
     
     if ([self.updaterDelegate respondsToSelector:@selector((updater:willDownloadUpdate:withRequest:))]) {
         [self.updaterDelegate updater:self.updater
@@ -222,9 +225,9 @@ - (void)downloadDriverDidDownloadUpdate:(SPUDownloadedUpdate *)downloadedUpdate
     [self extractUpdate:downloadedUpdate];
 }
 
-- (void)deferInformationalUpdate:(SUAppcastItem *)updateItem preventsAutoupdate:(BOOL)preventsAutoupdate
+- (void)deferInformationalUpdate:(SUAppcastItem *)updateItem secondaryUpdate:(SUAppcastItem *)secondaryUpdateItem preventsAutoupdate:(BOOL)preventsAutoupdate
 {
-    self.resumableUpdate = [[SPUInformationalUpdate alloc] initWithAppcastItem:updateItem preventsAutoupdate:preventsAutoupdate];
+    self.resumableUpdate = [[SPUInformationalUpdate alloc] initWithAppcastItem:updateItem secondaryAppcastItem:secondaryUpdateItem preventsAutoupdate:preventsAutoupdate];
 }
 
 - (void)extractDownloadedUpdate
@@ -381,7 +384,7 @@ - (void)basicDriverIsRequestingAbortUpdateWithError:(nullable NSError *)error
 
 - (void)installerDidFailToApplyDeltaUpdate
 {
-    SUAppcastItem *secondaryUpdateItem = self.basicDriver.secondaryUpdateItem;
+    SUAppcastItem *secondaryUpdateItem = self.secondaryUpdateItem;
     assert(secondaryUpdateItem != nil);
     
     BOOL backgroundDownload = self.downloadDriver.inBackground;
@@ -390,7 +393,9 @@ - (void)installerDidFailToApplyDeltaUpdate
     
     // Fall back to the non-delta update. Note that we don't want to trigger another update was found event.
     self.updateItem = secondaryUpdateItem;
-    [self downloadUpdateFromAppcastItem:secondaryUpdateItem inBackground:backgroundDownload];
+    self.secondaryUpdateItem = nil;
+    
+    [self downloadUpdateFromAppcastItem:secondaryUpdateItem secondaryAppcastItem:nil inBackground:backgroundDownload];
 }
 
 - (void)abortUpdateAndShowNextUpdateImmediately:(BOOL)shouldShowUpdateImmediately error:(nullable NSError *)error
diff --git a/Sparkle/SPUDownloadDriver.h b/Sparkle/SPUDownloadDriver.h
index 43fd6667af..14934806b5 100644
--- a/Sparkle/SPUDownloadDriver.h
+++ b/Sparkle/SPUDownloadDriver.h
@@ -28,7 +28,7 @@ NS_ASSUME_NONNULL_BEGIN
 
 @interface SPUDownloadDriver : NSObject
 
-- (instancetype)initWithUpdateItem:(SUAppcastItem *)updateItem host:(SUHost *)host userAgent:(NSString *)userAgent httpHeaders:(NSDictionary * _Nullable)httpHeaders inBackground:(BOOL)background delegate:(id<SPUDownloadDriverDelegate>)delegate;
+- (instancetype)initWithUpdateItem:(SUAppcastItem *)updateItem secondaryUpdateItem:(SUAppcastItem * _Nullable)secondaryUpdateItem host:(SUHost *)host userAgent:(NSString *)userAgent httpHeaders:(NSDictionary * _Nullable)httpHeaders inBackground:(BOOL)background delegate:(id<SPUDownloadDriverDelegate>)delegate;
 
 - (instancetype)initWithHost:(SUHost *)host;
 
diff --git a/Sparkle/SPUDownloadDriver.m b/Sparkle/SPUDownloadDriver.m
index 7624510aad..7b7c3c039f 100644
--- a/Sparkle/SPUDownloadDriver.m
+++ b/Sparkle/SPUDownloadDriver.m
@@ -28,6 +28,7 @@ @interface SPUDownloadDriver () <SPUDownloaderDelegate>
 @property (nonatomic) id<SPUDownloaderProtocol> downloader;
 @property (nonatomic) NSXPCConnection *connection;
 @property (nonatomic, readonly) SUAppcastItem *updateItem;
+@property (nonatomic, readonly) SUAppcastItem *secondaryUpdateItem;
 @property (nonatomic, readonly) SUHost *host;
 @property (nonatomic, copy) NSString *temporaryDirectory;
 @property (nonatomic, copy) NSString *downloadName;
@@ -43,6 +44,7 @@ @implementation SPUDownloadDriver
 @synthesize downloader = _downloader;
 @synthesize connection = _connection;
 @synthesize updateItem = _updateItem;
+@synthesize secondaryUpdateItem = _secondaryUpdateItem;
 @synthesize request = _request;
 @synthesize inBackground = _inBackground;
 @synthesize host = _host;
@@ -106,11 +108,12 @@ - (instancetype)initWithHost:(SUHost *)host
     return self;
 }
 
-- (instancetype)initWithUpdateItem:(SUAppcastItem *)updateItem host:(SUHost *)host userAgent:(NSString *)userAgent httpHeaders:(NSDictionary * _Nullable)httpHeaders inBackground:(BOOL)background delegate:(id<SPUDownloadDriverDelegate>)delegate
+- (instancetype)initWithUpdateItem:(SUAppcastItem *)updateItem secondaryUpdateItem:(SUAppcastItem * _Nullable)secondaryUpdateItem host:(SUHost *)host userAgent:(NSString *)userAgent httpHeaders:(NSDictionary * _Nullable)httpHeaders inBackground:(BOOL)background delegate:(id<SPUDownloadDriverDelegate>)delegate
 {
     self = [self initWithHost:host];
     if (self != nil) {
         _updateItem = updateItem;
+        _secondaryUpdateItem = secondaryUpdateItem;
         _delegate = delegate;
         
         _inBackground = background;
@@ -188,7 +191,7 @@ - (void)downloaderDidFinishWithTemporaryDownloadData:(SPUDownloadData * _Nullabl
             SULog(SULogLevelError, @"Warning: Downloader's expected content length (%llu) != Appcast item's length (%llu)", self.expectedContentLength, self.updateItem.contentLength);
         }
         
-        SPUDownloadedUpdate *downloadedUpdate = [[SPUDownloadedUpdate alloc] initWithAppcastItem:self.updateItem downloadName:self.downloadName temporaryDirectory:self.temporaryDirectory];
+        SPUDownloadedUpdate *downloadedUpdate = [[SPUDownloadedUpdate alloc] initWithAppcastItem:self.updateItem secondaryAppcastItem:self.secondaryUpdateItem downloadName:self.downloadName temporaryDirectory:self.temporaryDirectory];
         
         [self.delegate downloadDriverDidDownloadUpdate:downloadedUpdate];
     });
diff --git a/Sparkle/SPUDownloadedUpdate.h b/Sparkle/SPUDownloadedUpdate.h
index 349d03a48a..2dc0a842c4 100644
--- a/Sparkle/SPUDownloadedUpdate.h
+++ b/Sparkle/SPUDownloadedUpdate.h
@@ -13,7 +13,7 @@ NS_ASSUME_NONNULL_BEGIN
 
 @interface SPUDownloadedUpdate : NSObject <SPUResumableUpdate>
 
-- (instancetype)initWithAppcastItem:(SUAppcastItem *)updateItem downloadName:(NSString *)downloadName temporaryDirectory:(NSString *)temporaryDirectory;
+- (instancetype)initWithAppcastItem:(SUAppcastItem *)updateItem secondaryAppcastItem:(SUAppcastItem * _Nullable)secondaryItem downloadName:(NSString *)downloadName temporaryDirectory:(NSString *)temporaryDirectory;
 
 @property (nonatomic, copy, readonly) NSString *downloadName;
 @property (nonatomic, copy, readonly) NSString *temporaryDirectory;
diff --git a/Sparkle/SPUDownloadedUpdate.m b/Sparkle/SPUDownloadedUpdate.m
index 07b8cdb804..f3d7b520c1 100644
--- a/Sparkle/SPUDownloadedUpdate.m
+++ b/Sparkle/SPUDownloadedUpdate.m
@@ -16,15 +16,17 @@ @implementation SPUDownloadedUpdate
 // If we ever enable auto-synthesize in the future, we'll still need this synthesize
 // because the property is declared in a protocol
 @synthesize updateItem = _updateItem;
+@synthesize secondaryUpdateItem = _secondaryUpdateItem;
 
 @synthesize downloadName = _downloadName;
 @synthesize temporaryDirectory = _temporaryDirectory;
 
-- (instancetype)initWithAppcastItem:(SUAppcastItem *)updateItem downloadName:(NSString *)downloadName temporaryDirectory:(NSString *)temporaryDirectory
+- (instancetype)initWithAppcastItem:(SUAppcastItem *)updateItem secondaryAppcastItem:(SUAppcastItem * _Nullable)secondaryUpdateItem downloadName:(NSString *)downloadName temporaryDirectory:(NSString *)temporaryDirectory
 {
     self = [super init];
     if (self != nil) {
         _updateItem = updateItem;
+        _secondaryUpdateItem = secondaryUpdateItem;
         _downloadName = [downloadName copy];
         _temporaryDirectory = [temporaryDirectory copy];
     }
diff --git a/Sparkle/SPUInformationalUpdate.h b/Sparkle/SPUInformationalUpdate.h
index c26b03f48f..0d54f99d09 100644
--- a/Sparkle/SPUInformationalUpdate.h
+++ b/Sparkle/SPUInformationalUpdate.h
@@ -13,7 +13,7 @@ NS_ASSUME_NONNULL_BEGIN
 
 @interface SPUInformationalUpdate : NSObject <SPUResumableUpdate>
 
-- (instancetype)initWithAppcastItem:(SUAppcastItem *)updateItem preventsAutoupdate:(BOOL)preventsAutoupdate;
+- (instancetype)initWithAppcastItem:(SUAppcastItem *)updateItem secondaryAppcastItem:(SUAppcastItem *)secondaryUpdateItem preventsAutoupdate:(BOOL)preventsAutoupdate;
 
 @end
 
diff --git a/Sparkle/SPUInformationalUpdate.m b/Sparkle/SPUInformationalUpdate.m
index 6dd3b15c51..e9eec62f3d 100644
--- a/Sparkle/SPUInformationalUpdate.m
+++ b/Sparkle/SPUInformationalUpdate.m
@@ -16,13 +16,15 @@ @implementation SPUInformationalUpdate
 // If we ever enable auto-synthesize in the future, we'll still need this synthesize
 // because the property is declared in a protocol
 @synthesize updateItem = _updateItem;
+@synthesize secondaryUpdateItem = _secondaryUpdateItem;
 @synthesize preventsAutoupdate = _preventsAutoupdate;
 
-- (instancetype)initWithAppcastItem:(SUAppcastItem *)updateItem preventsAutoupdate:(BOOL)preventsAutoupdate
+- (instancetype)initWithAppcastItem:(SUAppcastItem *)updateItem secondaryAppcastItem:(SUAppcastItem *)secondaryUpdateItem preventsAutoupdate:(BOOL)preventsAutoupdate
 {
     self = [super init];
     if (self != nil) {
         _updateItem = updateItem;
+        _secondaryUpdateItem = secondaryUpdateItem;
         _preventsAutoupdate = preventsAutoupdate;
     }
     return self;
diff --git a/Sparkle/SPUProbingUpdateDriver.m b/Sparkle/SPUProbingUpdateDriver.m
index 6cd8fc3b30..bb377ef7da 100644
--- a/Sparkle/SPUProbingUpdateDriver.m
+++ b/Sparkle/SPUProbingUpdateDriver.m
@@ -54,7 +54,7 @@ - (void)resumeUpdate:(id<SPUResumableUpdate>)resumableUpdate completion:(SPUUpda
     [self.basicDriver resumeUpdate:resumableUpdate completion:completionBlock];
 }
 
-- (void)basicDriverDidFindUpdateWithAppcastItem:(SUAppcastItem *)__unused appcastItem preventsAutoupdate:(BOOL)__unused preventsAutoupdate systemDomain:(NSNumber * _Nullable)__unused systemDomain
+- (void)basicDriverDidFindUpdateWithAppcastItem:(SUAppcastItem *)__unused appcastItem secondaryAppcastItem:(SUAppcastItem *)__unused secondaryAppcastItem preventsAutoupdate:(BOOL)__unused preventsAutoupdate systemDomain:(NSNumber * _Nullable)__unused systemDomain
 {
     // Stop as soon as we have an answer
     [self abortUpdate];
diff --git a/Sparkle/SPUResumableUpdate.h b/Sparkle/SPUResumableUpdate.h
index 65ad7db66d..895a80c888 100644
--- a/Sparkle/SPUResumableUpdate.h
+++ b/Sparkle/SPUResumableUpdate.h
@@ -15,6 +15,7 @@ NS_ASSUME_NONNULL_BEGIN
 @protocol SPUResumableUpdate <NSObject>
 
 @property (nonatomic, readonly) SUAppcastItem *updateItem;
+@property (nonatomic, readonly, nullable) SUAppcastItem *secondaryUpdateItem;
 @property (nonatomic, readonly) BOOL preventsAutoupdate;
 
 @end
diff --git a/Sparkle/SPUUIBasedUpdateDriver.m b/Sparkle/SPUUIBasedUpdateDriver.m
index ea29f6d0f6..54ae8de44b 100644
--- a/Sparkle/SPUUIBasedUpdateDriver.m
+++ b/Sparkle/SPUUIBasedUpdateDriver.m
@@ -105,7 +105,7 @@ - (void)basicDriverDidFinishLoadingAppcast
     }
 }
 
-- (void)basicDriverDidFindUpdateWithAppcastItem:(SUAppcastItem *)updateItem preventsAutoupdate:(BOOL)preventsAutoupdate
+- (void)basicDriverDidFindUpdateWithAppcastItem:(SUAppcastItem *)updateItem secondaryAppcastItem:(SUAppcastItem *)secondaryUpdateItem preventsAutoupdate:(BOOL)preventsAutoupdate
 {
     id <SPUUpdaterDelegate> updaterDelegate = self.updaterDelegate;
     if ([self.userDriver respondsToSelector:@selector(showUpdateFoundWithAppcastItem:userInitiated:state:reply:)]) {
@@ -143,7 +143,7 @@ - (void)basicDriverDidFindUpdateWithAppcastItem:(SUAppcastItem *)updateItem prev
                                 [self.coreDriver finishInstallationWithResponse:validatedChoice displayingUserInterface:!self.preventsInstallerInteraction];
                                 break;
                             case SPUUserUpdateStateNotDownloaded:
-                                [self.coreDriver downloadUpdateFromAppcastItem:updateItem inBackground:NO];
+                                [self.coreDriver downloadUpdateFromAppcastItem:updateItem secondaryAppcastItem:secondaryUpdateItem inBackground:NO];
                                 break;
                             case SPUUserUpdateStateInformational:
                                 assert(false);
@@ -235,7 +235,7 @@ - (void)basicDriverDidFindUpdateWithAppcastItem:(SUAppcastItem *)updateItem prev
                     [self.host setObject:nil forUserDefaultsKey:SUSkippedVersionKey];
                     switch (choice) {
                         case SPUInstallUpdateChoice:
-                            [self.coreDriver downloadUpdateFromAppcastItem:updateItem inBackground:NO];
+                            [self.coreDriver downloadUpdateFromAppcastItem:updateItem secondaryAppcastItem:secondaryUpdateItem inBackground:NO];
                             break;
                         case SPUSkipThisVersionChoice:
                             [self.host setObject:[updateItem versionString] forUserDefaultsKey:SUSkippedVersionKey];
diff --git a/Sparkle/SUAppcastDriver.h b/Sparkle/SUAppcastDriver.h
index d9a6a3cfa6..e96686783d 100644
--- a/Sparkle/SUAppcastDriver.h
+++ b/Sparkle/SUAppcastDriver.h
@@ -17,7 +17,7 @@ NS_ASSUME_NONNULL_BEGIN
 
 - (void)didFailToFetchAppcastWithError:(NSError *)error;
 - (void)didFinishLoadingAppcast:(SUAppcast *)appcast;
-- (void)didFindValidUpdateWithAppcastItem:(SUAppcastItem *)appcastItem preventsAutoupdate:(BOOL)preventsAutoupdate;
+- (void)didFindValidUpdateWithAppcastItem:(SUAppcastItem *)appcastItem secondaryAppcastItem:(SUAppcastItem *)secondaryAppcastItem preventsAutoupdate:(BOOL)preventsAutoupdate;
 - (void)didNotFindUpdateWithLatestAppcastItem:(nullable SUAppcastItem *)latestAppcastItem hostToLatestAppcastItemComparisonResult:(NSComparisonResult)hostToLatestAppcastItemComparisonResult;
 
 @end
@@ -28,8 +28,6 @@ NS_ASSUME_NONNULL_BEGIN
 
 - (void)loadAppcastFromURL:(NSURL *)appcastURL userAgent:(NSString *)userAgent httpHeaders:(NSDictionary * _Nullable)httpHeaders inBackground:(BOOL)background includesSkippedUpdates:(BOOL)includesSkippedUpdates;
 
-@property (nullable, nonatomic, readonly) SUAppcastItem *secondaryUpdateItem;
-
 @end
 
 NS_ASSUME_NONNULL_END
diff --git a/Sparkle/SUAppcastDriver.m b/Sparkle/SUAppcastDriver.m
index 9dfb4a01e3..69728e454b 100644
--- a/Sparkle/SUAppcastDriver.m
+++ b/Sparkle/SUAppcastDriver.m
@@ -27,7 +27,6 @@ @interface SUAppcastDriver ()
 @property (nonatomic, copy) NSString *userAgent;
 @property (nullable, nonatomic, readonly, weak) id updater;
 @property (nullable, nonatomic, readonly, weak) id <SPUUpdaterDelegate> updaterDelegate;
-@property (nullable, nonatomic) SUAppcastItem *secondaryUpdateItem;
 @property (nullable, nonatomic, readonly, weak) id <SUAppcastDriverDelegate> delegate;
 
 @end
@@ -38,7 +37,6 @@ @implementation SUAppcastDriver
 @synthesize userAgent = _userAgent;
 @synthesize updater = _updater;
 @synthesize updaterDelegate = _updaterDelegate;
-@synthesize secondaryUpdateItem = _secondaryUpdateItem;
 @synthesize delegate = _delegate;
 
 - (instancetype)initWithHost:(SUHost *)host updater:(id)updater updaterDelegate:(id <SPUUpdaterDelegate>)updaterDelegate delegate:(id <SUAppcastDriverDelegate>)delegate
@@ -124,8 +122,7 @@ - (void)appcastDidFinishLoading:(SUAppcast *)loadedAppcast inBackground:(BOOL)ba
     }
     
     if ([self itemContainsValidUpdate:item inBackground:background includesSkippedUpdates:includesSkippedUpdates]) {
-        self.secondaryUpdateItem = secondaryItem;
-        [self.delegate didFindValidUpdateWithAppcastItem:item preventsAutoupdate:[self itemPreventsAutoupdate:item]];
+        [self.delegate didFindValidUpdateWithAppcastItem:item secondaryAppcastItem:secondaryItem preventsAutoupdate:[self itemPreventsAutoupdate:item]];
     } else {
         NSComparisonResult hostToLatestAppcastItemComparisonResult = (item != nil) ? [[self versionComparator] compareVersion:[self.host version] toVersion:[item versionString]] : 0;
         [self.delegate didNotFindUpdateWithLatestAppcastItem:item hostToLatestAppcastItemComparisonResult:hostToLatestAppcastItemComparisonResult];

From 564f0c316ac672d44693c9b6d80c56a2c383cbe0 Mon Sep 17 00:00:00 2001
From: Zorg <zorgiepoo@gmail.com>
Date: Sun, 25 Apr 2021 13:57:09 -0700
Subject: [PATCH 3/4] Refactor retrieving regular & delta update items

---
 Sparkle/SUAppcastDriver.m | 45 +++++++++++++++++++++++----------------
 1 file changed, 27 insertions(+), 18 deletions(-)

diff --git a/Sparkle/SUAppcastDriver.m b/Sparkle/SUAppcastDriver.m
index 69728e454b..44101dadfa 100644
--- a/Sparkle/SUAppcastDriver.m
+++ b/Sparkle/SUAppcastDriver.m
@@ -70,6 +70,9 @@ - (void)loadAppcastFromURL:(NSURL *)appcastURL userAgent:(NSString *)userAgent h
 - (SUAppcastItem * _Nullable)preferredUpdateForRegularAppcastItem:(SUAppcastItem * _Nullable)regularItem secondaryUpdate:(SUAppcastItem * __autoreleasing _Nullable *)secondaryUpdate
 {
     if (regularItem == nil) {
+        if (secondaryUpdate != NULL) {
+            *secondaryUpdate = nil;
+        }
         return nil;
     }
     
@@ -95,37 +98,43 @@ - (void)appcastDidFinishLoading:(SUAppcast *)loadedAppcast inBackground:(BOOL)ba
     NSNumber *phasedUpdateGroup = background ? @([SUPhasedUpdateGroupInfo updateGroupForHost:self.host]) : nil;
     SUAppcast *supportedAppcast = [[self class] filterSupportedAppcast:loadedAppcast phasedUpdateGroup:phasedUpdateGroup];
     
-    SUAppcastItem *item = nil;
-    SUAppcastItem *secondaryItem = nil;
-    
     // Find the best valid update in the appcast by asking the delegate
-    if ([self.updaterDelegate respondsToSelector:@selector((bestValidUpdateInAppcast:forUpdater:))])
-    {
-        SUAppcastItem *regularItem = [self.updaterDelegate bestValidUpdateInAppcast:supportedAppcast forUpdater:(id _Nonnull)self.updater];
+    SUAppcastItem *regularItemFromDelegate;
+    if ([self.updaterDelegate respondsToSelector:@selector((bestValidUpdateInAppcast:forUpdater:))]) {
+        SUAppcastItem *candidateItem = [self.updaterDelegate bestValidUpdateInAppcast:supportedAppcast forUpdater:(id _Nonnull)self.updater];
         
-        assert(!regularItem.deltaUpdate);
-        if (regularItem.deltaUpdate) {
+        assert(!candidateItem.deltaUpdate);
+        if (candidateItem.deltaUpdate) {
             // Client would have to go out of their way to examine the .deltaUpdates to return one
             // This is very unlikely, and we need them to give us a regular update item back
             SULog(SULogLevelError, @"Error: -bestValidUpdateInAppcast:forUpdater: cannot return a delta update item");
+            regularItemFromDelegate = nil;
         } else {
-            item = [self preferredUpdateForRegularAppcastItem:regularItem secondaryUpdate:&secondaryItem];
+            regularItemFromDelegate = candidateItem;
         }
+    } else {
+        regularItemFromDelegate = nil;
     }
     
     // Take care of finding best appcast item ourselves if delegate does not
-    if (item == nil)
-    {
-        SUAppcastItem *regularItem = [[self class] bestItemFromAppcastItems:supportedAppcast.items comparator:[self versionComparator]];
-        
-        item = [self preferredUpdateForRegularAppcastItem:regularItem secondaryUpdate:&secondaryItem];
+    SUAppcastItem *regularItem;
+    if (regularItemFromDelegate == nil) {
+        regularItem = [[self class] bestItemFromAppcastItems:supportedAppcast.items comparator:[self versionComparator]];
+    } else {
+        regularItem = regularItemFromDelegate;
     }
     
-    if ([self itemContainsValidUpdate:item inBackground:background includesSkippedUpdates:includesSkippedUpdates]) {
-        [self.delegate didFindValidUpdateWithAppcastItem:item secondaryAppcastItem:secondaryItem preventsAutoupdate:[self itemPreventsAutoupdate:item]];
+    // Retrieve the preferred primary and secondary update items
+    // In the case of a delta update, the preferred primary item will be the delta update,
+    // and the secondary item will be the regular update.
+    SUAppcastItem *secondaryItem = nil;
+    SUAppcastItem *primaryItem = [self preferredUpdateForRegularAppcastItem:regularItem secondaryUpdate:&secondaryItem];
+    
+    if ([self itemContainsValidUpdate:primaryItem inBackground:background includesSkippedUpdates:includesSkippedUpdates]) {
+        [self.delegate didFindValidUpdateWithAppcastItem:primaryItem secondaryAppcastItem:secondaryItem preventsAutoupdate:[self itemPreventsAutoupdate:primaryItem]];
     } else {
-        NSComparisonResult hostToLatestAppcastItemComparisonResult = (item != nil) ? [[self versionComparator] compareVersion:[self.host version] toVersion:[item versionString]] : 0;
-        [self.delegate didNotFindUpdateWithLatestAppcastItem:item hostToLatestAppcastItemComparisonResult:hostToLatestAppcastItemComparisonResult];
+        NSComparisonResult hostToLatestAppcastItemComparisonResult = (primaryItem != nil) ? [[self versionComparator] compareVersion:[self.host version] toVersion:[primaryItem versionString]] : 0;
+        [self.delegate didNotFindUpdateWithLatestAppcastItem:primaryItem hostToLatestAppcastItemComparisonResult:hostToLatestAppcastItemComparisonResult];
     }
 }
 

From 063974c5537aba2c03aec74336b620ada0b3a131 Mon Sep 17 00:00:00 2001
From: Zorg <zorgiepoo@gmail.com>
Date: Sun, 25 Apr 2021 14:28:33 -0700
Subject: [PATCH 4/4] Simplify logic in -preferredUpdateForRegularAppcastItem
 method

---
 Sparkle/SUAppcastDriver.m | 12 ++++--------
 1 file changed, 4 insertions(+), 8 deletions(-)

diff --git a/Sparkle/SUAppcastDriver.m b/Sparkle/SUAppcastDriver.m
index 44101dadfa..64366002e4 100644
--- a/Sparkle/SUAppcastDriver.m
+++ b/Sparkle/SUAppcastDriver.m
@@ -69,14 +69,7 @@ - (void)loadAppcastFromURL:(NSURL *)appcastURL userAgent:(NSString *)userAgent h
 
 - (SUAppcastItem * _Nullable)preferredUpdateForRegularAppcastItem:(SUAppcastItem * _Nullable)regularItem secondaryUpdate:(SUAppcastItem * __autoreleasing _Nullable *)secondaryUpdate
 {
-    if (regularItem == nil) {
-        if (secondaryUpdate != NULL) {
-            *secondaryUpdate = nil;
-        }
-        return nil;
-    }
-    
-    SUAppcastItem *deltaItem = [[self class] deltaUpdateFromAppcastItem:regularItem hostVersion:self.host.version];
+    SUAppcastItem *deltaItem = (regularItem != nil) ? [[self class] deltaUpdateFromAppcastItem:regularItem hostVersion:self.host.version] : nil;
     
     if (deltaItem != nil) {
         if (secondaryUpdate != NULL) {
@@ -84,6 +77,9 @@ - (SUAppcastItem * _Nullable)preferredUpdateForRegularAppcastItem:(SUAppcastItem
         }
         return deltaItem;
     } else {
+        if (secondaryUpdate != NULL) {
+            *secondaryUpdate = nil;
+        }
         return regularItem;
     }
 }