//
//  AppInstaller.m
//  Sparkle
//
//  Created by Mayur Pawashe on 3/7/16.
//  Copyright © 2016 Sparkle Project. All rights reserved.
//

#import "AppInstaller.h"
#import "SUInstaller.h"
#import "SUUpdateValidator.h"
#import "SULog.h"
#import "SULog+NSError.h"
#import "SUHost.h"
#import "SULocalizations.h"
#import "SUStandardVersionComparator.h"
#import "SPUMessageTypes.h"
#import "SPUSecureCoding.h"
#import "SPUInstallationInputData.h"
#import "SUUnarchiver.h"
#import "SUFileManager.h"
#import "SPUInstallationInfo.h"
#import "SUAppcastItem.h"
#import "SUErrors.h"
#import "SUInstallerCommunicationProtocol.h"
#import "AgentConnection.h"
#import "SPUInstallerAgentProtocol.h"
#import "SPUInstallationType.h"
#import "SPULocalCacheDirectory.h"
#import "SPUVerifierInformation.h"
#import "SUConstants.h"
#import "SUCodeSigningVerifier.h"
#import <os/lock.h>


#include "AppKitPrevention.h"

#define FIRST_UPDATER_MESSAGE_TIMEOUT 18ull
#define RETRIEVE_PROCESS_IDENTIFIER_TIMEOUT 8ull

/**
 * Show display progress UI after a delay from starting the final part of the installation.
 * This should be long enough so that we don't show progress for very fast installations, but
 * short enough so that we don't leave the user wondering why nothing is happening.
 */
static const NSTimeInterval SUDisplayProgressTimeDelay = 0.7;

@interface AppInstaller () <NSXPCListenerDelegate, SUInstallerCommunicationProtocol, AgentConnectionDelegate>
@end

@implementation AppInstaller
{
    NSXPCListener* _xpcListener;
    // Must be synchronized with _newConnectionLock
    // Set from new connection handler, and also set/read from main thread
    NSXPCConnection *_activeConnection;
    
    id<SUInstallerCommunicationProtocol> _communicator;
    AgentConnection *_agentConnection;

    SUUpdateValidator *_updateValidator;

    NSString *_hostBundleIdentifier;
    NSString *_homeDirectory;
    NSString *_userName;
    SUHost *_host;
    NSString *_updateDirectoryPath;
    NSString *_extractionDirectory;
    NSString *_downloadName;
    NSString *_decryptionPassword;
    SUSignatures *_signatures;
    NSString *_relaunchPath;
    NSString *_installationType;
    SPUVerifierInformation *_verifierInformation;

    id<SUInstallerProtocol> _installer;

    dispatch_queue_t _installerQueue;
    
    os_unfair_lock _newConnectionLock;
    
#if SPARKLE_BUILD_PACKAGE_SUPPORT
    // Must be synchronized with _newConnectionLock
    // Set from new connection handler, read from main thread
    BOOL _connectionCodeSigningValidationSkipped;
#endif
    
    BOOL _shouldRelaunch;
    BOOL _shouldShowUI;
    
    BOOL _receivedUpdaterPong;
    
    BOOL _willCompleteInstallation;
    BOOL _receivedInstallationData;
    BOOL _finishedValidation;
    BOOL _agentInitiatedConnection;
    
    // Setting _performedStage1Installation on main thread must be synchronzied with reading it from new connection handler
    BOOL _performedStage1Installation;
    
    BOOL _performedStage2Installation;
    BOOL _performedStage3Installation;
    
    BOOL _targetTerminated;
}

- (instancetype)initWithHostBundleIdentifier:(NSString *)hostBundleIdentifier homeDirectory:(NSString *)homeDirectory userName:(NSString *)userName
{
    if (!(self = [super init])) {
        return nil;
    }
    
    _newConnectionLock = OS_UNFAIR_LOCK_INIT;
    
    _hostBundleIdentifier = [hostBundleIdentifier copy];
    
    _homeDirectory = [homeDirectory copy];
    _userName = [userName copy];
    
    _xpcListener = [[NSXPCListener alloc] initWithMachServiceName:SPUInstallerServiceNameForBundleIdentifier(hostBundleIdentifier)];
    _xpcListener.delegate = self;
    
    _agentConnection = [[AgentConnection alloc] initWithHostBundleIdentifier:hostBundleIdentifier delegate:self];
    
    return self;
}

- (BOOL)listener:(NSXPCListener *)__unused listener shouldAcceptNewConnection:(NSXPCConnection *)newConnection
{
    os_unfair_lock_lock(&_newConnectionLock);
    {
        if (_activeConnection != nil) {
            os_unfair_lock_unlock(&_newConnectionLock);
            
            SULog(SULogLevelError, @"Error: Rejecting multiple XPC connections for installer...");
            
            [newConnection invalidate];
            return NO;
        }
        
    #if SPARKLE_BUILD_PACKAGE_SUPPORT
        BOOL connectionCodeSigningValidationSkipped = NO;
    #endif
        
        // It's safe to allow any connections once stage 1 installation is complete
        // This is to allow general updaters to resume the installation.
        if (!_performedStage1Installation) {
            BOOL passesValidation;
            NSError *validationError = nil;
            SUValidateConnectionStatus status = [SUCodeSigningVerifier validateConnection:newConnection options:SUValidateConnectionOptionDefault error:&validationError];
            switch (status) {
                case SUValidateConnectionStatusSetCodeSigningRequirementSuccess:
                    passesValidation = YES;
                    break;
                case SUValidateConnectionStatusSetNoRequirementSuccess:
                    passesValidation = YES;
#if SPARKLE_BUILD_PACKAGE_SUPPORT
                    connectionCodeSigningValidationSkipped = YES;
#endif
                    break;
                case SUValidateConnectionStatusAPIFailure:
                case SUValidateConnectionStatusCodeSigningRequirementFailure:
                case SUValidateConectionNoSupportedValidationMethodFailure:
                    passesValidation = NO;
                    break;
            }
            
            if (!passesValidation) {
                os_unfair_lock_unlock(&_newConnectionLock);
                
                SULog(SULogLevelError, @"Error: Rejecting new connection for installer due to failing validation of XPC connection with status %lu and error: %@", status, validationError.localizedDescription);
                [newConnection invalidate];
                return NO;
            }
        }
        
#if SPARKLE_BUILD_PACKAGE_SUPPORT
        _connectionCodeSigningValidationSkipped = connectionCodeSigningValidationSkipped;
#endif
        _activeConnection = newConnection;
    }
    os_unfair_lock_unlock(&_newConnectionLock);
    
    newConnection.exportedInterface = [NSXPCInterface interfaceWithProtocol:@protocol(SUInstallerCommunicationProtocol)];
    newConnection.exportedObject = self;
    
    newConnection.remoteObjectInterface = [NSXPCInterface interfaceWithProtocol:@protocol(SUInstallerCommunicationProtocol)];
    
    __weak __typeof__(self) weakSelf = self;
    newConnection.interruptionHandler = ^{
        dispatch_async(dispatch_get_main_queue(), ^{
            __typeof__(self) strongSelf = weakSelf;
            if (strongSelf != nil) {
                os_unfair_lock_lock(&strongSelf->_newConnectionLock);
                NSXPCConnection *activeConnection = strongSelf->_activeConnection;
                os_unfair_lock_unlock(&strongSelf->_newConnectionLock);
                
                [activeConnection invalidate];
            }
        });
    };
    
    newConnection.invalidationHandler = ^{
        dispatch_async(dispatch_get_main_queue(), ^{
            __typeof__(self) strongSelf = weakSelf;
            if (strongSelf != nil) {
                if (strongSelf->_activeConnection != nil && !strongSelf->_willCompleteInstallation) {
                    [strongSelf cleanupAndExitWithStatus:EXIT_FAILURE error:[NSError errorWithDomain:SUSparkleErrorDomain code:SPUInstallerError userInfo:@{ NSLocalizedDescriptionKey: @"Invalidation on remote port being called, and installation is not close enough to completion!" }]];
                }
                strongSelf->_communicator = nil;
                
                os_unfair_lock_lock(&strongSelf->_newConnectionLock);
                strongSelf->_activeConnection = nil;
                os_unfair_lock_unlock(&strongSelf->_newConnectionLock);
            }
        });
    };
    
    // _communicator is used only on main thread
    dispatch_async(dispatch_get_main_queue(), ^{
        self->_communicator = newConnection.remoteObjectProxy;
        [newConnection resume];
    });
    
    return YES;
}

- (void)start
{
    [_xpcListener resume];
    [_agentConnection startListener];
    
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(FIRST_UPDATER_MESSAGE_TIMEOUT * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        if (!self->_receivedInstallationData) {
            [self cleanupAndExitWithStatus:EXIT_FAILURE error:[NSError errorWithDomain:SUSparkleErrorDomain code:SPUInstallerError userInfo:@{ NSLocalizedDescriptionKey: @"Timeout: installation data was never received" }]];
        }
        
        if (!self->_agentConnection.connected) {
            [self cleanupAndExitWithStatus:EXIT_FAILURE error:[NSError errorWithDomain:SUSparkleErrorDomain code:SPUInstallerError userInfo:@{ NSLocalizedDescriptionKey: @"Timeout: agent connection was never initiated" }]];
        }
    });
}

- (void)extractAndInstallUpdate SPU_OBJC_DIRECT
{
    [_communicator handleMessageWithIdentifier:SPUExtractionStarted data:[NSData data]];
    
    NSString *archivePath = [_updateDirectoryPath stringByAppendingPathComponent:_downloadName];
    
    id<SUUnarchiverProtocol> unarchiver = [SUUnarchiver unarchiverForPath:archivePath extractionDirectory:_extractionDirectory updatingHostBundlePath:_host.bundlePath decryptionPassword:_decryptionPassword expectingInstallationType:_installationType];
    
    NSError *prevalidationError = nil;
    BOOL success = NO;
    if (!unarchiver) {
        prevalidationError = [NSError errorWithDomain:SUSparkleErrorDomain code:SUUnarchivingError userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"No valid unarchiver was found for %@", archivePath] }];
        
        success = NO;
    } else {
        NSError *fileAttributesError = nil;
        NSDictionary<NSFileAttributeKey, id> *archiveFileAttributes = [NSFileManager.defaultManager attributesOfItemAtPath:archivePath error:&fileAttributesError];
        if (archiveFileAttributes == nil) {
            SULog(SULogLevelError, @"Failed to retrieve file attributes from archive: %@.", fileAttributesError);
        } else {
            _verifierInformation.actualContentLength = (uint64_t)(archiveFileAttributes.fileSize);
        }
        
        _updateValidator = [[SUUpdateValidator alloc] initWithDownloadPath:archivePath signatures:_signatures host:_host verifierInformation:_verifierInformation];
        
        // More uncommon archives types (.aar, .yaa) need SUVerifyUpdateBeforeExtraction
        BOOL verifyBeforeExtraction = [_host boolForInfoDictionaryKey:SUVerifyUpdateBeforeExtractionKey];
        if (!verifyBeforeExtraction && unarchiver.needsVerifyBeforeExtractionKey) {
            prevalidationError = [NSError errorWithDomain:SUSparkleErrorDomain code:SUValidationError userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Extracting %@ archives require setting %@ to YES in the old app. Please visit https://sparkle-project.org/documentation/customization/ for more information.", archivePath.pathExtension, SUVerifyUpdateBeforeExtractionKey] }];
            
            success = NO;
        } else {
            // Delta, package updates, and apps with SUVerifyUpdateBeforeExtraction will require validation before extraction
            // Otherwise normal application updates are a bit more lenient allowing developers to change one of apple dev ID or EdDSA keys after extraction
            BOOL archiveTypeMustValidateBeforeExtraction = [[unarchiver class] mustValidateBeforeExtraction];
            BOOL needsPrevalidation = verifyBeforeExtraction || archiveTypeMustValidateBeforeExtraction || ![_installationType isEqualToString:SPUInstallationTypeApplication];

            if (needsPrevalidation) {
                // EdDSA signing is required, so host must have public keys
                if (![_updateValidator validateHostHasPublicKeys:&prevalidationError]) {
                    success = NO;
                } else {
                    // Falling back on code signing for prevalidation requires SUVerifyUpdateBeforeExtraction
                    // and that update is a regular app update, and not a delta update
                    BOOL fallbackOnCodeSigning = (verifyBeforeExtraction && !archiveTypeMustValidateBeforeExtraction && [_installationType isEqualToString:SPUInstallationTypeApplication]);
                    
                    success = [_updateValidator validateDownloadPathWithFallbackOnCodeSigning:fallbackOnCodeSigning error:&prevalidationError];
                }
            } else {
                success = YES;
            }
        }
    }
    
    if (!success) {
        [self unarchiverDidFailWithError:prevalidationError];
    } else {
        [unarchiver
         unarchiveWithCompletionBlock:^(NSError * _Nullable error) {
             if (error != nil) {
                 [self unarchiverDidFailWithError:[NSError errorWithDomain:SUSparkleErrorDomain code:SUUnarchivingError userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Failed to unarchive %@", archivePath], NSUnderlyingErrorKey: (NSError * _Nonnull)error }]];
             } else {
                 [self->_communicator handleMessageWithIdentifier:SPUValidationStarted data:[NSData data]];
                 
                 NSError *validationError = nil;
                 BOOL validationSuccess = [self->_updateValidator validateWithUpdateDirectory:self->_extractionDirectory error:&validationError];
                 
                 if (!validationSuccess) {
                     [self cleanupAndExitWithStatus:EXIT_FAILURE error:[NSError errorWithDomain:SUSparkleErrorDomain code:SPUInstallerError userInfo:@{ NSLocalizedDescriptionKey: @"Update validation was a failure", NSUnderlyingErrorKey: validationError }]];
                 } else {
                     [self->_communicator handleMessageWithIdentifier:SPUInstallationStartedStage1 data:[NSData data]];
                     
                     self->_finishedValidation = YES;
                     if (self->_agentInitiatedConnection) {
                         [self retrieveProcessIdentifierAndStartInstallation];
                     }
                 }
             }
         }
         progressBlock:^(double progress) {
             if (sizeof(progress) == sizeof(uint64_t)) {
                 uint64_t progressValue = CFSwapInt64HostToLittle(*(uint64_t *)&progress);
                 NSData *data = [NSData dataWithBytes:&progressValue length:sizeof(progressValue)];
                 
                 [self->_communicator handleMessageWithIdentifier:SPUExtractedArchiveWithProgress data:data];
             }
         } waitForCleanup:NO];
    }
}

- (void)clearUpdateDirectory SPU_OBJC_DIRECT
{
    if (_updateDirectoryPath != nil) {
        NSError *theError = nil;
        if (![[[SUFileManager alloc] init] removeItemAtURL:[NSURL fileURLWithPath:_updateDirectoryPath] error:&theError]) {
            SULog(SULogLevelError, @"Couldn't remove update folder: %@.", theError);
        }
        _updateDirectoryPath = nil;
    }
}

- (void)unarchiverDidFailWithError:(NSError *)error SPU_OBJC_DIRECT
{
    SULog(SULogLevelError, @"Failed to unarchive file");
    SULogError(error);
    
    // No longer need update validator until next possible extraction (eg: if initial delta update fails)
    _updateValidator = nil;
    
    // Client could try update again with different inputs
    // Eg: one common case is if a delta update fails, client may want to fall back to regular update
    // We really only need to set updateDirectoryPath to nil since that's the field we check if we've received installation data,
    // but may as well set other fields to nil too
    [self clearUpdateDirectory];
    _downloadName = nil;
    _extractionDirectory = nil;
    _decryptionPassword = nil;
    _signatures = nil;
    _relaunchPath = nil;
    _host = nil;
    
    NSData *archivedError = SPUArchiveRootObjectSecurely(error);
    [_communicator handleMessageWithIdentifier:SPUArchiveExtractionFailed data:archivedError != nil ? archivedError : [NSData data]];
}

- (void)agentConnectionDidInitiate
{
    _agentInitiatedConnection = YES;
    if (_finishedValidation) {
        [self retrieveProcessIdentifierAndStartInstallation];
    }
}

- (void)agentConnectionDidInvalidate
{
    if (!_finishedValidation || !_agentInitiatedConnection || !_targetTerminated) {
        NSMutableDictionary *userInfo = [NSMutableDictionary dictionaryWithDictionary:@{ NSLocalizedDescriptionKey: @"Error: Agent connection invalidated before installation began" }];
        
        NSError *agentError = _agentConnection.invalidationError;
        if (agentError != nil) {
            userInfo[NSUnderlyingErrorKey] = agentError;
        }
        
        [self cleanupAndExitWithStatus:EXIT_FAILURE error:[NSError errorWithDomain:SUSparkleErrorDomain code:SPUInstallerError userInfo:userInfo]];
    }
}

- (void)retrieveProcessIdentifierAndStartInstallation SPU_OBJC_DIRECT
{
    // We use the relaunch path for the bundle to listen for termination instead of the host path
    // For a plug-in this makes a big difference; we want to wait until the app hosting the plug-in terminates
    // Otherwise for an app, the relaunch path and host path should be identical
    
    __block BOOL receivedResponse = NO;
    [_agentConnection.agent registerApplicationBundlePath:_relaunchPath reply:^(BOOL targetTerminated) {
        dispatch_async(dispatch_get_main_queue(), ^{
            receivedResponse = YES;
            
            if (!targetTerminated) {
                [self->_agentConnection.agent listenForTerminationWithCompletion:^{
                    dispatch_async(dispatch_get_main_queue(), ^{
                        self->_targetTerminated = YES;
                        
                        if (self->_performedStage1Installation) {
                            [self finishInstallationAfterHostTermination];
                        }
                    });
                }];
            } else {
                self->_targetTerminated = YES;
            }
            
            [self startInstallation];
        });
    }];
    
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(RETRIEVE_PROCESS_IDENTIFIER_TIMEOUT * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        if (!receivedResponse) {
            [self cleanupAndExitWithStatus:EXIT_FAILURE error:[NSError errorWithDomain:SUSparkleErrorDomain code:SPUInstallerError userInfo:@{ NSLocalizedDescriptionKey: @"Timeout error: failed to retrieve process identifier from agent" }]];
        }
    });
}

- (void)handleMessageWithIdentifier:(int32_t)identifier data:(NSData *)data
{
    if (identifier == SPUInstallationData && _updateDirectoryPath == nil) {
        dispatch_async(dispatch_get_main_queue(), ^{
            // Mark that we have received the installation data
            // Do not rely on eg: self->_updateDirectoryPath != nil because we may set it to nil again if an early stage fails (i.e, archive extraction)
            self->_receivedInstallationData = YES;
            
            SPUInstallationInputData *installationData = (data != nil) ? (SPUInstallationInputData *)SPUUnarchiveRootObjectSecurely(data, [SPUInstallationInputData class]) : nil;
            if (installationData == nil) {
                [self cleanupAndExitWithStatus:EXIT_FAILURE error:[NSError errorWithDomain:SUSparkleErrorDomain code:SPUInstallerError userInfo:@{ NSLocalizedDescriptionKey: @"Error: Failed to unarchive input installation data" }]];
                return;
            }
            
            NSString *installationType = installationData.installationType;
            if (!SPUValidInstallationType(installationType)) {
                [self cleanupAndExitWithStatus:EXIT_FAILURE error:[NSError errorWithDomain:SUSparkleErrorDomain code:SPUInstallerError userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Error: Received invalid installation type: %@", installationType] }]];
                return;
            }
            
            NSBundle *hostBundle = [NSBundle bundleWithPath:installationData.hostBundlePath];
            
            NSString *bundleIdentifier = hostBundle.bundleIdentifier;
            if (bundleIdentifier == nil || ![bundleIdentifier isEqualToString:self->_hostBundleIdentifier]) {
                [self cleanupAndExitWithStatus:EXIT_FAILURE error:[NSError errorWithDomain:SUSparkleErrorDomain code:SPUInstallerError userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Error: Failed to match host bundle identifiers %@ and %@", self->_hostBundleIdentifier, bundleIdentifier] }]];
                return;
            }
            
            // This will be important later
            if (installationData.relaunchPath == nil) {
                [self cleanupAndExitWithStatus:EXIT_FAILURE error:[NSError errorWithDomain:SUSparkleErrorDomain code:SPUInstallerError userInfo:@{ NSLocalizedDescriptionKey: @"Error: Failed to obtain relaunch path from installation data" }]];
                return;
            }
            
            // This installation path is specific to sparkle and the bundle identifier
            NSString *rootCacheInstallationPath = [[SPULocalCacheDirectory cachePathForBundleIdentifier:bundleIdentifier] stringByAppendingPathComponent:@"Installation"];
            
            [SPULocalCacheDirectory removeOldItemsInDirectory:rootCacheInstallationPath];
            
            NSString *cacheInstallationPath = [SPULocalCacheDirectory createUniqueDirectoryInDirectory:rootCacheInstallationPath];
            if (cacheInstallationPath == nil) {
                [self cleanupAndExitWithStatus:EXIT_FAILURE error:[NSError errorWithDomain:SUSparkleErrorDomain code:SPUInstallerError userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Error: Failed to create installation cache directory in %@", rootCacheInstallationPath] }]];
                return;
            }
            
            // Resolve the bookmark data for the downloaded update
            // See "Share file access between processes with URL bookmarks" in https://developer.apple.com/documentation/security/app_sandbox/accessing_files_from_the_macos_app_sandbox
            BOOL isStale = NO;
            NSError *bookmarkError = nil;
            NSURL *downloadURL = [NSURL URLByResolvingBookmarkData:installationData.updateURLBookmarkData options:NSURLBookmarkResolutionWithoutUI relativeToURL:nil bookmarkDataIsStale:&isStale error:&bookmarkError];
            if (downloadURL == nil) {
                [self cleanupAndExitWithStatus:EXIT_FAILURE error:[NSError errorWithDomain:SUSparkleErrorDomain code:SPUInstallerError userInfo:@{ NSLocalizedDescriptionKey: @"Error: Failed to resolve bookmark data from downloaded update", NSUnderlyingErrorKey: bookmarkError }]];
                
                return;
            }
            
            // Validate the download URL before moving it
            {
                NSArray<NSString *> *downloadURLPathComponents = downloadURL.URLByResolvingSymlinksInPath.pathComponents;
                if (downloadURLPathComponents == nil) {
                    [self cleanupAndExitWithStatus:EXIT_FAILURE error:[NSError errorWithDomain:SUSparkleErrorDomain code:SPUInstallerError userInfo:@{ NSLocalizedDescriptionKey: @"Error: Failed to retrieve path components from download URL" }]];
                    
                    return;
                }
                
                if ([downloadURLPathComponents containsObject:@".."]) {
                    [self cleanupAndExitWithStatus:EXIT_FAILURE error:[NSError errorWithDomain:SUSparkleErrorDomain code:SPUInstallerError userInfo:@{ NSLocalizedDescriptionKey: @"Error: download URL path components contains '..' which is unsafe" }]];
                    
                    return;
                }
                
                if (![downloadURLPathComponents containsObject:@SPARKLE_BUNDLE_IDENTIFIER] || ![downloadURLPathComponents containsObject:@"PersistentDownloads"]) {
                    [self cleanupAndExitWithStatus:EXIT_FAILURE error:[NSError errorWithDomain:SUSparkleErrorDomain code:SPUInstallerError userInfo:@{ NSLocalizedDescriptionKey: @"Error: download URL path components does not contain PersistentDownloads or "@SPARKLE_BUNDLE_IDENTIFIER }]];
                    
                    return;
                }
            }
            
            if (!isStale) {
                SULog(SULogLevelError, @"Error: bookmark data for update download is stale.. but still continuing.");
            }
            
            NSString *originalDownloadName = downloadURL.lastPathComponent;
            if (originalDownloadName == nil) {
                [self cleanupAndExitWithStatus:EXIT_FAILURE error:[NSError errorWithDomain:SUSparkleErrorDomain code:SPUInstallerError userInfo:@{ NSLocalizedDescriptionKey: @"Error: Failed to retrieve download name from download URL" }]];
                
                return;
            }
            
            // Randomize the download name if possible
            // This adds better security if there are any vulnerabilities in extracting/executing archives
            // which allow writing in unexpected locations. For zip/tar/dmg archives we may also extract them before
            // performing signing validation (due to key rotation).
            NSString *downloadName;
            NSString *randomizedUUIDString = [[NSUUID UUID] UUIDString];
            if (randomizedUUIDString != nil) {
                // Find the real path extension of the download name
                // We cannot use -[NSString pathExtension] because it may not give us the full path extension
                // E.g. for "foo.tar.xz" we need "tar.xz", not "xz"
                NSString *downloadPathExtension;
                NSRange pathExtensionDelimiterRange = [originalDownloadName rangeOfString:@"."];
                if (pathExtensionDelimiterRange.location == NSNotFound) {
                    downloadPathExtension = @"";
                } else {
                    downloadPathExtension = [originalDownloadName substringFromIndex:pathExtensionDelimiterRange.location + 1];
                }
                
                NSString *randomizedDownloadName = [randomizedUUIDString stringByAppendingPathExtension:downloadPathExtension];
                if (randomizedDownloadName != nil) {
                    downloadName = randomizedDownloadName;
                } else {
                    downloadName = originalDownloadName;
                }
            } else {
                downloadName = originalDownloadName;
            }
            
            // Move the download archive to somewhere where probably only we will be touching it
            // This prevents eg: if a bug exists in the updater that removes files we are trying to install
            // When this tool is ran as root, we are moving it into a directory that only root will have access to
            
            NSURL *downloadDestinationURL = [[NSURL fileURLWithPath:cacheInstallationPath] URLByAppendingPathComponent:downloadName];
            
            NSError *moveError = nil;
            if (![[[SUFileManager alloc] init] moveItemAtURL:downloadURL toURL:downloadDestinationURL error:&moveError]) {
                [self cleanupAndExitWithStatus:EXIT_FAILURE error:[NSError errorWithDomain:SUSparkleErrorDomain code:SPUInstallerError userInfo:@{ NSLocalizedDescriptionKey: @"Error: Failed to move download archive to new location", NSUnderlyingErrorKey: moveError }]];
                return;
            }
            
            // Make sure the downloaded archive we moved over is a regular file and not a symbolic link placed by an attacker
            NSError *attributesError = nil;
            NSString *downloadDestinationPath = downloadDestinationURL.path;
            if (downloadDestinationPath == nil) {
                [self cleanupAndExitWithStatus:EXIT_FAILURE error:[NSError errorWithDomain:SUSparkleErrorDomain code:SPUInstallerError userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Error: Failed to retrieve download archive path from %@", downloadDestinationURL] }]];
                
                return;
            }
            
            NSDictionary<NSString *, id> *archiveAttributes = [[NSFileManager defaultManager] attributesOfItemAtPath:downloadDestinationPath error:&attributesError];
            
            if (archiveAttributes == nil) {
                [self cleanupAndExitWithStatus:EXIT_FAILURE error:[NSError errorWithDomain:SUSparkleErrorDomain code:SPUInstallerError userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Error: Failed to retrieve download archive attributes from %@", downloadDestinationPath] }]];
                
                return;
            }
            
            if (![(NSString *)archiveAttributes[NSFileType] isEqualToString:NSFileTypeRegular]) {
                [self cleanupAndExitWithStatus:EXIT_FAILURE error:[NSError errorWithDomain:SUSparkleErrorDomain code:SPUInstallerError userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Error: Received bad archive file type: %@", archiveAttributes[NSFileType]] }]];
                return;
            }
            
            NSString *extractionDirectory = [SPULocalCacheDirectory createUniqueDirectoryInDirectory:cacheInstallationPath];
            if (extractionDirectory == nil) {
                [self cleanupAndExitWithStatus:EXIT_FAILURE error:[NSError errorWithDomain:SUSparkleErrorDomain code:SPUInstallerError userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Error: Failed to create installation extraction directory in %@", cacheInstallationPath] }]];
                
                return;
            }
            
            // Carry these properties separately rather than using the SUInstallationInputData object
            // Some of our properties may slightly differ than our input and we don't want to make the mistake of using one of those
            self->_installationType = installationType;
            self->_relaunchPath = installationData.relaunchPath;
            self->_downloadName = downloadName;
            self->_signatures = installationData.signatures;
            self->_updateDirectoryPath = cacheInstallationPath;
            self->_extractionDirectory = extractionDirectory;
            self->_decryptionPassword = installationData.decryptionPassword;
            self->_host = [[SUHost alloc] initWithBundle:hostBundle];
            self->_verifierInformation = [[SPUVerifierInformation alloc] initWithExpectedVersion:installationData.expectedVersion expectedContentLength:installationData.expectedContentLength];
            
            [self extractAndInstallUpdate];
        });
    } else if (identifier == SPUSentUpdateAppcastItemData) {
        SUAppcastItem *updateItem = (data != nil) ? (SUAppcastItem *)SPUUnarchiveRootObjectSecurely(data, [SUAppcastItem class]) : nil;
        if (updateItem != nil) {
            SPUInstallationInfo *installationInfo = [[SPUInstallationInfo alloc] initWithAppcastItem:updateItem canSilentlyInstall:[_installer canInstallSilently]];
            
            NSData *archivedData = SPUArchiveRootObjectSecurely(installationInfo);
            if (archivedData != nil) {
                [_agentConnection.agent registerInstallationInfoData:archivedData];
            }
        }
    } else if (identifier == SPUResumeInstallationToStage2 && data.length == sizeof(uint8_t) * 2) {
        // Because anyone can ask us to resume the installation, it may be wise to think about backwards compatibility here if IPC changes
        uint8_t relaunch = *((const uint8_t *)data.bytes);
        uint8_t showsUI = *((const uint8_t *)data.bytes + 1);
        
        dispatch_async(dispatch_get_main_queue(), ^{
            // This flag has an impact on interactive type installations and showing UI progress during non-interactive installations
            self->_shouldShowUI = (BOOL)showsUI;
            // Don't test if the application was alive initially, leave that to the progress agent if we decide to relaunch
            self->_shouldRelaunch = (BOOL)relaunch;
            
            if (self->_performedStage1Installation) {
                // Resume the installation if we aren't done with stage 2 yet, and remind the client we are prepared to relaunch
                dispatch_async(self->_installerQueue, ^{
                    if (!self->_performedStage2Installation) {
                        [self performStage2Installation];
                    } else if (!self->_performedStage3Installation) {
                        // If we already performed the 2nd stage, re-purpose this request to re-try sending another termination signal
                        dispatch_async(dispatch_get_main_queue(), ^{
                            // Don't check if the target is already terminated, leave that to the progress agent
                            // We could be slightly off if there were multiple instances running
                            [self->_agentConnection.agent sendTerminationSignal];
                        });
                    }
                });
            }
        });
    } else if (identifier == SPUCancelInstallation) {
        dispatch_async(dispatch_get_main_queue(), ^{
            [self cleanupAndExitWithStatus:0 error:nil];
        });
    } else if (identifier == SPUUpdaterAlivePong) {
        _receivedUpdaterPong = YES;
    }
}

- (void)startInstallation SPU_OBJC_DIRECT
{
    _willCompleteInstallation = YES;
    
    dispatch_queue_attr_t queuePriority = dispatch_queue_attr_make_with_qos_class(DISPATCH_QUEUE_SERIAL, QOS_CLASS_USER_INITIATED, 0);
    
    _installerQueue = dispatch_queue_create("org.sparkle-project.sparkle.installer", queuePriority);
    
#if SPARKLE_BUILD_PACKAGE_SUPPORT
    os_unfair_lock_lock(&_newConnectionLock);
    BOOL connectionCodeSigningValidationSkipped = self->_connectionCodeSigningValidationSkipped;
    os_unfair_lock_unlock(&_newConnectionLock);
#else
    BOOL connectionCodeSigningValidationSkipped = NO;
#endif
    
    dispatch_async(_installerQueue, ^{
        NSError *installerError = nil;
        id <SUInstallerProtocol> installer = [SUInstaller installerForHost:self->_host expectedInstallationType:self->_installationType updateDirectory:self->_extractionDirectory connectionCodeSigningValidationSkipped:connectionCodeSigningValidationSkipped homeDirectory:self->_homeDirectory userName:self->_userName error:&installerError];
        
        if (installer == nil) {
            dispatch_async(dispatch_get_main_queue(), ^{
                [self cleanupAndExitWithStatus:EXIT_FAILURE error:[NSError errorWithDomain:SUSparkleErrorDomain code:SPUInstallerError userInfo:@{ NSLocalizedDescriptionKey: @"Error: Failed to create installer instance", NSUnderlyingErrorKey: installerError }]];
            });
            return;
        }
        
        NSError *firstStageError = nil;
        if (![installer performInitialInstallation:&firstStageError]) {
            self->_installer = nil;
            
            dispatch_async(dispatch_get_main_queue(), ^{
                [self cleanupAndExitWithStatus:EXIT_FAILURE error:[NSError errorWithDomain:SUSparkleErrorDomain code:SPUInstallerError userInfo:@{ NSLocalizedDescriptionKey: @"Error: Failed to start installer", NSUnderlyingErrorKey: firstStageError }]];
            });
            return;
        }
        
        uint8_t canPerformSilentInstall = (uint8_t)[installer canInstallSilently];
        
        dispatch_async(dispatch_get_main_queue(), ^{
            self->_installer = installer;
            
            os_unfair_lock_lock(&self->_newConnectionLock);
            self->_performedStage1Installation = YES;
            os_unfair_lock_unlock(&self->_newConnectionLock);
            
            uint8_t sendInformation[] = {canPerformSilentInstall, (uint8_t)self->_targetTerminated};
            
            NSData *sendData = [NSData dataWithBytes:sendInformation length:sizeof(sendInformation)];
            
            [self->_communicator handleMessageWithIdentifier:SPUInstallationFinishedStage1 data:sendData];
            
            if (self->_targetTerminated) {
                // Stage 2 can still be run before we finish installation
                // if the updater requests for it before the app is terminated
                [self finishInstallationAfterHostTermination];
            }
        });
    });
}

- (void)performStage2Installation SPU_OBJC_DIRECT
{
    BOOL canPerformSecondStage = _shouldShowUI || [_installer canInstallSilently];
    if (canPerformSecondStage) {
        _performedStage2Installation = YES;
        
        dispatch_async(dispatch_get_main_queue(), ^{
            uint8_t targetTerminated = (uint8_t)self->_targetTerminated;
            
            NSData *sendData = [NSData dataWithBytes:&targetTerminated length:sizeof(targetTerminated)];
            [self->_communicator handleMessageWithIdentifier:SPUInstallationFinishedStage2 data:sendData];
            
            // Don't check if the target is already terminated, leave that to the progress agent
            // We could be slightly off if there were multiple instances running
            [self->_agentConnection.agent sendTerminationSignal];
        });
    } else {
        _installer = nil;
        
        dispatch_async(dispatch_get_main_queue(), ^{
            [self cleanupAndExitWithStatus:EXIT_FAILURE error:[NSError errorWithDomain:SUSparkleErrorDomain code:SPUInstallerError userInfo:@{ NSLocalizedDescriptionKey: @"Error: Failed to resume installer on stage 2 because installation cannot be installed silently" }]];
        });
    }
}

- (void)finishInstallationAfterHostTermination SPU_OBJC_DIRECT
{
    assert(self->_targetTerminated);
    
    // Show our installer progress UI tool if only after a certain amount of time passes,
    // and if our installer is silent (i.e, doesn't show progress on its own)
    __block BOOL shouldShowUIProgress = YES;
    if (self->_shouldShowUI && [self->_installer canInstallSilently]) {
        // Ask the updater if it is still alive
        // If they are, we will receive a pong response back
        // Reset if we received a pong just to be on the safe side
        self->_receivedUpdaterPong = NO;
        [self->_communicator handleMessageWithIdentifier:SPUUpdaterAlivePing data:[NSData data]];
        
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(SUDisplayProgressTimeDelay * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
            // Make sure we're still eligible for showing the installer progress
            // Also if the updater process is still alive, showing the progress should not be our duty
            // if the communicator object is nil, the updater definitely isn't alive. However, if it is not nil,
            // this does not necessarily mean the updater is alive, so we should also check if we got a recent response back from the updater
            if (shouldShowUIProgress && (!self->_receivedUpdaterPong || self->_communicator == nil)) {
                [self->_agentConnection.agent showProgress];
            }
        });
    }
        
    dispatch_async(self->_installerQueue, ^{
        if (!self->_performedStage2Installation) {
            [self performStage2Installation];
        }
        
        if (!self->_performedStage2Installation) {
            // We failed and we're going to exit shortly
            return;
        }
        
        NSError *thirdStageError = nil;
        if (![self->_installer performFinalInstallationProgressBlock:nil error:&thirdStageError]) {
            [self->_installer performCleanup];
            self->_installer = nil;
            
            dispatch_async(dispatch_get_main_queue(), ^{
                [self cleanupAndExitWithStatus:EXIT_FAILURE error:[NSError errorWithDomain:SUSparkleErrorDomain code:SPUInstallerError userInfo:@{ NSLocalizedDescriptionKey: @"Failed to finalize installation", NSUnderlyingErrorKey: thirdStageError }]];
            });
            return;
        }
        
        self->_performedStage3Installation = YES;
        
        dispatch_async(dispatch_get_main_queue(), ^{
            // Make sure to stop our displayed progress before we move onto cleanup & relaunch
            // This will also stop the agent from broadcasting the status info service, which we want to do before
            // we relaunch the app because the relaunched app could check the service upon launch..
            [self->_agentConnection.agent stopProgress];
            shouldShowUIProgress = NO;
            
            [self->_communicator handleMessageWithIdentifier:SPUInstallationFinishedStage3 data:[NSData data]];
            
            if (self->_shouldRelaunch) {
                // This will also signal to the agent that it will terminate soon
                [self->_agentConnection.agent relaunchApplication];
            }
            
            [self->_installer performCleanup];
            
            [self cleanupAndExitWithStatus:EXIT_SUCCESS error:nil];
        });
    });
}

- (void)cleanupAndExitWithStatus:(int)status error:(NSError * _Nullable)error __attribute__((noreturn))
{
    if (error != nil) {
        SULogError(error);
        
        NSData *errorData = SPUArchiveRootObjectSecurely((NSError * _Nonnull)error);
        if (errorData != nil) {
            [_communicator handleMessageWithIdentifier:SPUInstallerError data:errorData];
        }
    }
    
    // It's nice to tell the other end we're invalidating
    
    os_unfair_lock_lock(&_newConnectionLock);
    
    NSXPCConnection *activeConnection = _activeConnection;
    _activeConnection = nil;
    
    os_unfair_lock_unlock(&_newConnectionLock);
    
    [activeConnection invalidate];
    
    [_xpcListener invalidate];
    _xpcListener = nil;
    
    [_agentConnection invalidate];
    _agentConnection = nil;
    
    [self clearUpdateDirectory];
    
    exit(status);
}

@end
