From 984c3dc0e17a897d78e4c9bf2160521353520f22 Mon Sep 17 00:00:00 2001 From: Zorg Date: Wed, 22 Jul 2015 23:38:07 -0400 Subject: [PATCH 01/12] Rewrote file operations for updating an app The improvements we want to achieve: * Stability - No more corrupt / incomplete updates by ensuring atomic move operations * Efficiency - Faster updates because of reduction of file copies that need to be done * Separation from Host and Helper - No usage of SUFileManager in the host application * Cleanness - Minimized deprecated APIs (no FSPathMakeRef, FSGetCatalogInfo, FSFindFolder, etc), use modern Cocoa APIs when possible, robust error handling, strong documentation, easier to read code. Many other small notes: * SUFileManager does not have class methods. Methods across an instance share an AuthorizationRef, instead of having to authorize every time a privileged operation needs to be done * The odd logic of `while (authStat == errAuthorizationDenied)` was removed * Instead of checking if we have write access, we try to invoke a normal operation first. If that doesn't work, with the right error returned back, we try authorizing, which is what Apple recommends doing * The trash is not used as a temporary location anymore. Rather we now request for a temporary directory to be created. This is more likely to succeed too (eg: on a drive that has no trash folder) * The old method for releasing the quarantine did not work until I used the "com.apple.quarantine" key instead of the LS constant that was being used. * For the old method for releasing the quarantine, we now check if a file has the quarantine xattr bit set before removing it. Just like how the new method works. * Quarantines can now be removed with authorization * The result of calling temporaryNameForPath: was never actually used in the old code * -[NSString fileSystemRepresentation] which can throw an exception is not used. Only getFileSystemRepresentation:maxLength: is used and the error on it is always checked * The desperate/hopeless check for if the code signature for the update is valid after the installation is complete is removed Potential issues that still needs to be looked in to: * Currently the method to move a file to the trash only works on 10.8+ * Appending of version number / picking destination name of app moved to trash --- Sparkle.xcodeproj/project.pbxproj | 20 +- Sparkle/Autoupdate/Autoupdate.m | 3 +- Sparkle/SUBasicUpdateDriver.m | 25 +- Sparkle/SUFileManager.h | 100 +++++ Sparkle/SUFileManager.m | 642 ++++++++++++++++++++++++++++ Sparkle/SUPlainInstaller.m | 146 ++++++- Sparkle/SUPlainInstallerInternals.h | 24 -- Sparkle/SUPlainInstallerInternals.m | 639 --------------------------- 8 files changed, 899 insertions(+), 700 deletions(-) create mode 100644 Sparkle/SUFileManager.h create mode 100644 Sparkle/SUFileManager.m delete mode 100644 Sparkle/SUPlainInstallerInternals.h delete mode 100644 Sparkle/SUPlainInstallerInternals.m diff --git a/Sparkle.xcodeproj/project.pbxproj b/Sparkle.xcodeproj/project.pbxproj index 501ba0e39f..b1ba4ec13e 100644 --- a/Sparkle.xcodeproj/project.pbxproj +++ b/Sparkle.xcodeproj/project.pbxproj @@ -69,7 +69,6 @@ 55C14F24136EF86F00649790 /* SUPackageInstaller.m in Sources */ = {isa = PBXBuildFile; fileRef = 618FA5210DAE8E8A0026945C /* SUPackageInstaller.m */; }; 55C14F2A136EF9A900649790 /* SUWindowController.m in Sources */ = {isa = PBXBuildFile; fileRef = 61180BC90D64138900B4E0D1 /* SUWindowController.m */; }; 55C14F32136EFC2400649790 /* SystemConfiguration.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 55C14F31136EFC2400649790 /* SystemConfiguration.framework */; }; - 55C14F7E136F005000649790 /* SUPlainInstallerInternals.m in Sources */ = {isa = PBXBuildFile; fileRef = 61B5F8E509C4CE3C00B25A18 /* SUPlainInstallerInternals.m */; }; 55C14F9A136F045400649790 /* Security.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 61B5F8F609C4CEB300B25A18 /* Security.framework */; }; 55C14FC7136F05E100649790 /* Sparkle.strings in Resources */ = {isa = PBXBuildFile; fileRef = 61AAE8220A321A7F00D8810D /* Sparkle.strings */; }; 55E6F33319EC9F6C00005E76 /* SUErrors.h in Headers */ = {isa = PBXBuildFile; fileRef = 55E6F33219EC9F6C00005E76 /* SUErrors.h */; settings = {ATTRIBUTES = (Public, ); }; }; @@ -110,7 +109,6 @@ 6120721309CC5C4B007FE0F6 /* SUAutomaticUpdateAlert.m in Sources */ = {isa = PBXBuildFile; fileRef = 6120721109CC5C4B007FE0F6 /* SUAutomaticUpdateAlert.m */; }; 61299A2F09CA2DAB00B7442F /* SUDSAVerifier.h in Headers */ = {isa = PBXBuildFile; fileRef = 61299A2D09CA2DAB00B7442F /* SUDSAVerifier.h */; settings = {ATTRIBUTES = (); }; }; 61299A3009CA2DAB00B7442F /* SUDSAVerifier.m in Sources */ = {isa = PBXBuildFile; fileRef = 61299A2E09CA2DAB00B7442F /* SUDSAVerifier.m */; settings = {COMPILER_FLAGS = "-Wno-deprecated-declarations"; }; }; - 61299A4A09CA2DD000B7442F /* SUPlainInstallerInternals.h in Headers */ = {isa = PBXBuildFile; fileRef = 6129984309C9E2DA00B7442F /* SUPlainInstallerInternals.h */; settings = {ATTRIBUTES = (); }; }; 61299A5C09CA6D4500B7442F /* SUConstants.h in Headers */ = {isa = PBXBuildFile; fileRef = 61299A5B09CA6D4500B7442F /* SUConstants.h */; settings = {ATTRIBUTES = (); }; }; 61299A6009CA6EB100B7442F /* SUConstants.m in Sources */ = {isa = PBXBuildFile; fileRef = 61299A5F09CA6EB100B7442F /* SUConstants.m */; }; 61299A8D09CA790200B7442F /* SUUnarchiver.h in Headers */ = {isa = PBXBuildFile; fileRef = 61299A8B09CA790200B7442F /* SUUnarchiver.h */; settings = {ATTRIBUTES = (Private, ); }; }; @@ -141,7 +139,6 @@ 61B078CF15A5FB6100600039 /* SUCodeSigningVerifier.m in Sources */ = {isa = PBXBuildFile; fileRef = 61B078CD15A5FB6100600039 /* SUCodeSigningVerifier.m */; }; 61B5F8ED09C4CE3C00B25A18 /* SUUpdater.h in Headers */ = {isa = PBXBuildFile; fileRef = 61B5F8E309C4CE3C00B25A18 /* SUUpdater.h */; settings = {ATTRIBUTES = (Public, ); }; }; 61B5F8EE09C4CE3C00B25A18 /* SUUpdater.m in Sources */ = {isa = PBXBuildFile; fileRef = 61B5F8E409C4CE3C00B25A18 /* SUUpdater.m */; }; - 61B5F8EF09C4CE3C00B25A18 /* SUPlainInstallerInternals.m in Sources */ = {isa = PBXBuildFile; fileRef = 61B5F8E509C4CE3C00B25A18 /* SUPlainInstallerInternals.m */; }; 61B5F8F709C4CEB300B25A18 /* Security.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 61B5F8F609C4CEB300B25A18 /* Security.framework */; }; 61B5F90F09C4CF3A00B25A18 /* Sparkle.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 8DC2EF5B0486A6940098B216 /* Sparkle.framework */; }; 61B5F92E09C4CFD800B25A18 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 61B5F92A09C4CFD800B25A18 /* InfoPlist.strings */; }; @@ -174,6 +171,9 @@ 721CF1AB1AD764EB00D9AC09 /* libbz2.dylib in Frameworks */ = {isa = PBXBuildFile; fileRef = 5D06E8FB0FD68D61005AE3F6 /* libbz2.dylib */; }; 7223E7631AD1AEFF008E3161 /* sais.c in Sources */ = {isa = PBXBuildFile; fileRef = 7223E7611AD1AEFF008E3161 /* sais.c */; }; 7268AC631AD634C200C3E0C1 /* SUBinaryDeltaCreate.m in Sources */ = {isa = PBXBuildFile; fileRef = 7268AC621AD634C200C3E0C1 /* SUBinaryDeltaCreate.m */; }; + 7275F9C11B5F1F2900B1D19E /* SUFileManager.h in Headers */ = {isa = PBXBuildFile; fileRef = 7275F9BF1B5F1F2900B1D19E /* SUFileManager.h */; }; + 7275F9C21B5F1F2900B1D19E /* SUFileManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 7275F9C01B5F1F2900B1D19E /* SUFileManager.m */; }; + 7275F9C31B5F1F2900B1D19E /* SUFileManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 7275F9C01B5F1F2900B1D19E /* SUFileManager.m */; }; 72D4DAA11AD7632900B211E2 /* SUBinaryDeltaCreate.m in Sources */ = {isa = PBXBuildFile; fileRef = 7268AC621AD634C200C3E0C1 /* SUBinaryDeltaCreate.m */; }; 767B61AC1972D488004E0C3C /* SUGuidedPackageInstaller.h in Headers */ = {isa = PBXBuildFile; fileRef = 767B61AA1972D488004E0C3C /* SUGuidedPackageInstaller.h */; }; 767B61AD1972D488004E0C3C /* SUGuidedPackageInstaller.m in Sources */ = {isa = PBXBuildFile; fileRef = 767B61AB1972D488004E0C3C /* SUGuidedPackageInstaller.m */; }; @@ -461,7 +461,6 @@ 612279D90DB5470200AB99EA /* Sparkle Unit Tests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "Sparkle Unit Tests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; 612279DA0DB5470200AB99EA /* SparkleTests-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "SparkleTests-Info.plist"; sourceTree = ""; }; 61227A150DB548B800AB99EA /* SUVersionComparisonTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SUVersionComparisonTest.m; sourceTree = ""; }; - 6129984309C9E2DA00B7442F /* SUPlainInstallerInternals.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SUPlainInstallerInternals.h; sourceTree = ""; }; 61299A2D09CA2DAB00B7442F /* SUDSAVerifier.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SUDSAVerifier.h; sourceTree = ""; }; 61299A2E09CA2DAB00B7442F /* SUDSAVerifier.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SUDSAVerifier.m; sourceTree = ""; }; 61299A5B09CA6D4500B7442F /* SUConstants.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SUConstants.h; sourceTree = ""; }; @@ -513,7 +512,6 @@ 61B078CD15A5FB6100600039 /* SUCodeSigningVerifier.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SUCodeSigningVerifier.m; sourceTree = ""; }; 61B5F8E309C4CE3C00B25A18 /* SUUpdater.h */ = {isa = PBXFileReference; fileEncoding = 30; lastKnownFileType = sourcecode.c.h; path = SUUpdater.h; sourceTree = ""; }; 61B5F8E409C4CE3C00B25A18 /* SUUpdater.m */ = {isa = PBXFileReference; fileEncoding = 30; lastKnownFileType = sourcecode.c.objc; path = SUUpdater.m; sourceTree = ""; }; - 61B5F8E509C4CE3C00B25A18 /* SUPlainInstallerInternals.m */ = {isa = PBXFileReference; fileEncoding = 30; lastKnownFileType = sourcecode.c.objc; path = SUPlainInstallerInternals.m; sourceTree = ""; }; 61B5F8F609C4CEB300B25A18 /* Security.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Security.framework; path = System/Library/Frameworks/Security.framework; sourceTree = SDKROOT; }; 61B5F90209C4CEE200B25A18 /* Sparkle Test App.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Sparkle Test App.app"; sourceTree = BUILT_PRODUCTS_DIR; }; 61B5F90409C4CEE200B25A18 /* TestApplication-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "TestApplication-Info.plist"; sourceTree = ""; }; @@ -553,6 +551,8 @@ 7223E7621AD1AEFF008E3161 /* sais.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = sais.h; sourceTree = ""; }; 7268AC621AD634C200C3E0C1 /* SUBinaryDeltaCreate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SUBinaryDeltaCreate.m; sourceTree = ""; }; 7268AC641AD634E400C3E0C1 /* SUBinaryDeltaCreate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SUBinaryDeltaCreate.h; sourceTree = ""; }; + 7275F9BF1B5F1F2900B1D19E /* SUFileManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SUFileManager.h; sourceTree = ""; }; + 7275F9C01B5F1F2900B1D19E /* SUFileManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SUFileManager.m; sourceTree = ""; }; 767B61AA1972D488004E0C3C /* SUGuidedPackageInstaller.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SUGuidedPackageInstaller.h; sourceTree = ""; }; 767B61AB1972D488004E0C3C /* SUGuidedPackageInstaller.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SUGuidedPackageInstaller.m; sourceTree = ""; }; 8DC2EF5A0486A6940098B216 /* Sparkle-Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = "Sparkle-Info.plist"; sourceTree = ""; }; @@ -897,8 +897,8 @@ 618FA5210DAE8E8A0026945C /* SUPackageInstaller.m */, 618FA5030DAE8AB80026945C /* SUPlainInstaller.h */, 618FA5040DAE8AB80026945C /* SUPlainInstaller.m */, - 6129984309C9E2DA00B7442F /* SUPlainInstallerInternals.h */, - 61B5F8E509C4CE3C00B25A18 /* SUPlainInstallerInternals.m */, + 7275F9BF1B5F1F2900B1D19E /* SUFileManager.h */, + 7275F9C01B5F1F2900B1D19E /* SUFileManager.m */, ); name = Installation; sourceTree = ""; @@ -1033,11 +1033,11 @@ 767B61AC1972D488004E0C3C /* SUGuidedPackageInstaller.h in Headers */, 61EF67590E25C5B400F754E0 /* SUHost.h in Headers */, 618FA5010DAE88B40026945C /* SUInstaller.h in Headers */, + 7275F9C11B5F1F2900B1D19E /* SUFileManager.h in Headers */, 55C14F06136EF6DB00649790 /* SULog.h in Headers */, 618FA5220DAE8E8A0026945C /* SUPackageInstaller.h in Headers */, 6102FE460E077FCE00F85D09 /* SUPipedUnarchiver.h in Headers */, 618FA5050DAE8AB80026945C /* SUPlainInstaller.h in Headers */, - 61299A4A09CA2DD000B7442F /* SUPlainInstallerInternals.h in Headers */, 6101347B0DD2541A0049ACDF /* SUProbingUpdateDriver.h in Headers */, 61B93C090DD112FF00DCD2F8 /* SUScheduledUpdateDriver.h in Headers */, 61A225A40D1C4AC000430CCD /* SUStandardVersionComparator.h in Headers */, @@ -1411,13 +1411,13 @@ 55C14BD4136EEFCE00649790 /* Autoupdate.m in Sources */, F8761EB61ADC5E7A000C9034 /* SUCodeSigningVerifier.m in Sources */, 55C14F00136EF6B700649790 /* SUConstants.m in Sources */, + 7275F9C31B5F1F2900B1D19E /* SUFileManager.m in Sources */, 767B61AE1972D488004E0C3C /* SUGuidedPackageInstaller.m in Sources */, 55C14F0C136EF6EA00649790 /* SUHost.m in Sources */, 55C14F0D136EF6F200649790 /* SUInstaller.m in Sources */, 55C14F08136EF6DB00649790 /* SULog.m in Sources */, 55C14F24136EF86F00649790 /* SUPackageInstaller.m in Sources */, 55C14F21136EF84D00649790 /* SUPlainInstaller.m in Sources */, - 55C14F7E136F005000649790 /* SUPlainInstallerInternals.m in Sources */, 55C14F22136EF86000649790 /* SUStandardVersionComparator.m in Sources */, 55C14F20136EF84300649790 /* SUStatusController.m in Sources */, 55C14F23136EF86700649790 /* SUSystemProfiler.m in Sources */, @@ -1489,10 +1489,10 @@ 618FA5230DAE8E8A0026945C /* SUPackageInstaller.m in Sources */, 61D85D6D0E10B2ED00F9B4A9 /* SUPipedUnarchiver.m in Sources */, 618FA5060DAE8AB80026945C /* SUPlainInstaller.m in Sources */, - 61B5F8EF09C4CE3C00B25A18 /* SUPlainInstallerInternals.m in Sources */, 6101347C0DD2541A0049ACDF /* SUProbingUpdateDriver.m in Sources */, 61B93C0A0DD112FF00DCD2F8 /* SUScheduledUpdateDriver.m in Sources */, 61A225A50D1C4AC000430CCD /* SUStandardVersionComparator.m in Sources */, + 7275F9C21B5F1F2900B1D19E /* SUFileManager.m in Sources */, 6196CFFA09C72149000DC222 /* SUStatusController.m in Sources */, 61A2279D0D1CEE7600430CCD /* SUSystemProfiler.m in Sources */, 61B93A3D0DD02D7000DCD2F8 /* SUUIBasedUpdateDriver.m in Sources */, diff --git a/Sparkle/Autoupdate/Autoupdate.m b/Sparkle/Autoupdate/Autoupdate.m index 4d4dd13884..f7a973885c 100644 --- a/Sparkle/Autoupdate/Autoupdate.m +++ b/Sparkle/Autoupdate/Autoupdate.m @@ -3,7 +3,6 @@ #import "SUHost.h" #import "SUStandardVersionComparator.h" #import "SUStatusController.h" -#import "SUPlainInstallerInternals.h" #import "SULog.h" #include @@ -134,7 +133,7 @@ - (void)relaunch __attribute__((noreturn)) if (self.folderpath) { NSError *theError = nil; - if (![SUPlainInstaller _removeFileAtPath:[SUInstaller updateFolder] error:&theError]) + if (![[NSFileManager defaultManager] removeItemAtPath:[SUInstaller updateFolder] error:&theError]) SULog(@"Couldn't remove update folder: %@.", theError); } [[NSFileManager defaultManager] removeItemAtPath:self.selfPath error:NULL]; diff --git a/Sparkle/SUBasicUpdateDriver.m b/Sparkle/SUBasicUpdateDriver.m index 71c4151faa..6371285d70 100644 --- a/Sparkle/SUBasicUpdateDriver.m +++ b/Sparkle/SUBasicUpdateDriver.m @@ -15,8 +15,6 @@ #import "SUUnarchiver.h" #import "SUConstants.h" #import "SULog.h" -#import "SUPlainInstaller.h" -#import "SUPlainInstallerInternals.h" #import "SUBinaryDeltaCommon.h" #import "SUCodeSigningVerifier.h" #import "SUUpdater_Private.h" @@ -420,17 +418,30 @@ - (void)installWithToolAndRelaunch:(BOOL)relaunch displayingUserInterface:(BOOL) } NSBundle *sparkleBundle = [NSBundle bundleWithIdentifier:SUBundleIdentifier]; - // Copy the relauncher into a temporary directory so we can get to it after the new version's installed. // Only the paranoid survive: if there's already a stray copy of relaunch there, we would have problems. NSString *const relaunchPathToCopy = [sparkleBundle pathForResource:[[sparkleBundle infoDictionary] objectForKey:SURelaunchToolNameKey] ofType:@"app"]; if (relaunchPathToCopy != nil) { NSString *targetPath = [self.host.appCachePath stringByAppendingPathComponent:[relaunchPathToCopy lastPathComponent]]; - // Only the paranoid survive: if there's already a stray copy of relaunch there, we would have problems. + + NSFileManager *fileManager = [[NSFileManager alloc] init]; + + BOOL shouldAbortUpdate = NO; NSError *error = nil; - [[NSFileManager defaultManager] createDirectoryAtPath:[targetPath stringByDeletingLastPathComponent] withIntermediateDirectories:YES attributes:@{} error:&error]; - - if ([SUPlainInstaller copyPathWithAuthentication:relaunchPathToCopy overPath:targetPath temporaryName:nil error:&error]) { + if ([fileManager fileExistsAtPath:targetPath]) { + if (![fileManager removeItemAtPath:targetPath error:&error]) { + shouldAbortUpdate = YES; + } + } else { + if (![fileManager createDirectoryAtPath:[targetPath stringByDeletingLastPathComponent] withIntermediateDirectories:YES attributes:@{} error:&error]) { + shouldAbortUpdate = YES; + } + } + + // We only need to run our copy of the app by spawning a task + // Since we are copying the app to a directory that is write-accessible, we don't need to muck with owner/group IDs + // And since we spawn a task, we don't need to clear the quarantine bits + if (!shouldAbortUpdate && [fileManager copyItemAtPath:relaunchPathToCopy toPath:targetPath error:&error]) { self.relaunchPath = targetPath; } else { [self abortUpdateWithError:[NSError errorWithDomain:SUSparkleErrorDomain code:SURelaunchError userInfo:@{ diff --git a/Sparkle/SUFileManager.h b/Sparkle/SUFileManager.h new file mode 100644 index 0000000000..5f07bff036 --- /dev/null +++ b/Sparkle/SUFileManager.h @@ -0,0 +1,100 @@ +// +// SUFileManager.h +// Sparkle +// +// Created by Mayur Pawashe on 7/18/15. +// Copyright (c) 2015 zgcoder. All rights reserved. +// + +#import + +/** + * A class used for performing file operations that may also perform authentication if permission is denied when trying to + * perform them normally as the running user. All operations on this class may be used on thread other than the main thread. + * This class provides basic file operations and stays away from including much application-level logic. + */ +@interface SUFileManager : NSObject + +/** + * Creates a temporary directory on the same volume as a provided URL + * @param preferredName A name that may be used when creating the temporary directory. Note that in the uncommon case this name is used, the temporary directory will be created inside the directory pointed by appropriateURL + * @param appropriateURL A URL to a directory that resides on the volume that the temporary directory will be created on. In the uncommon case, the temporary directory may be created inside this directory. + * @param error If an error occurs, upon returns contains an NSError object that describes the problem. If you are not interested in possible errors, you may pass in NULL. + * @return A URL pointing to the newly created temporary directory, or nil with a populated error object if an error occurs. + * + * When moving an item from a source to a destination, it is desirable to create a temporary intermediate destination on the same volume as the destination to ensure + * that the item will be moved, and not copied, from the intermediate point to the final destination. This ensures file atomicity. + */ +- (NSURL *)makeTemporaryDirectoryWithPreferredName:(NSString *)preferredName appropriateForDirectoryURL:(NSURL *)appropriateURL error:(NSError **)error; + +/** + * Moves an item from a source to a destination + * @param sourceURL A URL pointing to the item to move. The item at this URL must exist. + * @param destinationURL A URL pointing to the destination the item will be moved at. An item must not already exist at this URL. + * @param error If an error occurs, upon returns contains an NSError object that describes the problem. If you are not interested in possible errors, you may pass in NULL. + * @return YES if the item was moved successfully, otherwise NO along with a populated error object + * + * If sourceURL and destinationURL reside on the same volume, this operation will be an atomic move operation. + * Otherwise this will be equivalent to a copy & remove which will be a nonatomic operation. + */ +- (BOOL)moveItemAtURL:(NSURL *)sourceURL toURL:(NSURL *)destinationURL error:(NSError **)error; + +/** + * Moves an item at a specified URL to the running user's trash directory + * @param url A URL pointing to the item to move to the trash. The item at this URL must exist. + * @param error If an error occurs, upon returns contains an NSError object that describes the problem. If you are not interested in possible errors, you may pass in NULL. + * @return YES if the item was moved to the trash successfully, otherwise NO along with a populated error object + * + * + * This method has to locate the trash directory and uses an intermediate temporary directory before trashing the item. + * A copy may have to be done if the url is not on the same volume as the running user's trash directory. + * If a failure occurs in the middle of this operation, the item to remove may be lost forever or stuck in a temporary location. + * + * This is not an atomic operation, nor intended to be a recoverable operation if the worst comes to worst. + */ +- (BOOL)moveItemAtURLToTrash:(NSURL *)url error:(NSError **)error; + +/** + * Removes an item at a URL + * @param url A URL pointing to the item to remove. The item at this URL must exist. + * @param error If an error occurs, upon returns contains an NSError object that describes the problem. If you are not interested in possible errors, you may pass in NULL. + * @return YES if the item was removed successfully, otherwise NO along with a populated error object + * + * This is not an atomic operation. + */ +- (BOOL)removeItemAtURL:(NSURL *)url error:(NSError **)error; + +/** + * Changes the owner and group IDs of an item at a specified target URL to match another URL + * @param targetURL A URL pointing to the target item whose owner and group IDs to alter. This will be applied recursively if the item is a directory. The item at this URL must exist. + * @param matchURL A URL pointing to the item whose owner and group IDs will be used for changing on the targetURL. The item at this URL must exist. + * @param error If an error occurs, upon returns contains an NSError object that describes the problem. If you are not interested in possible errors, you may pass in NULL. + * @return YES if the target item's owner and group IDs have changed to match the origin's ones, otherwise NO along with a populated error object + * + * If the owner and group IDs match on the root items of targetURL and matchURL, this method stops and assumes that nothing needs to be done. + * Otherwise this method recursively changes the IDs if the target is a directory. If an item in the directory is encountered that is unable to be changed, + * then this method stops and returns NO. + * + * This is not an atomic operation. + */ +- (BOOL)changeOwnerAndGroupOfItemAtRootURL:(NSURL *)targetURL toMatchURL:(NSURL *)matchURL error:(NSError **)error; + +/** + * Releases Apple's quarantine extended attribute from the item at the specified root URL + * @param rootURL A URL pointing to the item to release from Apple's quarantine. This will be applied recursively if the item is a directory. The item at this URL must exist. + * @param error If an error occurs, upon returns contains an NSError object that describes the problem. If you are not interested in possible errors, you may pass in NULL. + * @return YES if all the items at the target could be released from quarantine, otherwise NO if any items couldn't along with a populated error object + * + * This method removes quarantine attributes from an item, ideally an application, so that when the user launches a new application themselves, they + * don't have to witness the OS X dialog alerting them that they downloaded an application from the internet and asking if they want to continue. + * Note that this may not exactly mimic OS X's behavior when a user opens an application for the first time (i.e, the xattr isn't deleted), + * but this should be sufficient enough for our purposes. + * + * This method may return NO even if some items do get released from quarantine if the target URL is pointing to a directory. + * Thus if an item cannot be released from quarantine, this method still continues on to the next enumerated item. + * + * This is not an atomic operation. + */ +- (BOOL)releaseItemFromQuarantineAtRootURL:(NSURL *)rootURL error:(NSError **)error; + +@end diff --git a/Sparkle/SUFileManager.m b/Sparkle/SUFileManager.m new file mode 100644 index 0000000000..59163b8dbb --- /dev/null +++ b/Sparkle/SUFileManager.m @@ -0,0 +1,642 @@ +// +// SUFileManager.m +// Sparkle +// +// Created by Mayur Pawashe on 7/18/15. +// Copyright (c) 2015 zgcoder. All rights reserved. +// + +#import "SUFileManager.h" + +#include +#include + +#if __MAC_OS_X_VERSION_MAX_ALLOWED < 101000 /* MAC_OS_X_VERSION_10_10 */ +extern NSString *const NSURLQuarantinePropertiesKey WEAK_IMPORT_ATTRIBUTE; +#endif + +// Authorization code based on generous contribution from Allan Odgaard. Thanks, Allan! +static BOOL AuthorizationExecuteWithPrivilegesAndWait(AuthorizationRef authorization, const char *executablePath, AuthorizationFlags options, char *const *arguments) +{ + sig_t oldSigChildHandler = signal(SIGCHLD, SIG_DFL); + BOOL returnValue = YES; + +#pragma clang diagnostic push + // In the future, we may have to look at SMJobBless API to avoid deprecation. See issue #558 +#pragma clang diagnostic ignored "-Wdeprecated-declarations" + if (AuthorizationExecuteWithPrivileges(authorization, executablePath, options, arguments, NULL) == errAuthorizationSuccess) +#pragma clang diagnostic pop + { + int status; + pid_t pid = wait(&status); + if (pid == -1 || !WIFEXITED(status) || WEXITSTATUS(status) != 0) + returnValue = NO; + } + else + returnValue = NO; + + signal(SIGCHLD, oldSigChildHandler); + return returnValue; +} + +// Used to indicate if the type of NSError requires us to attempt to peform the same operation again except with authentication +// To be safe, both read and write permission denied's are included because Cocoa's error methods are not very well documented +// and at least one case is caused from lack of read permissions (-[NSURL setResourceValue:forKey:error:]) +#define NS_HAS_PERMISSION_ERROR(error) (error.code == NSFileReadNoPermissionError || error.code == NSFileWriteNoPermissionError) + +#pragma clang diagnostic push +// Use direct access because it's easier, clearer, and faster +#pragma clang diagnostic ignored "-Wdirect-ivar-access" + +@implementation SUFileManager +{ + AuthorizationRef _auth; + NSFileManager *_fileManager; +} + +- (id)init +{ + self = [super init]; + if (self != nil) { + _fileManager = [[NSFileManager alloc] init]; + } + return self; +} + +// Acquires an authorization reference which is intended to be used for future authorized file operations +- (BOOL)acquireAuthorizationWithError:(NSError *__autoreleasing *)error +{ + // No need to continue if we already acquired an authorization reference + if (_auth != NULL) { + return YES; + } + + OSStatus status = AuthorizationCreate(NULL, kAuthorizationEmptyEnvironment, kAuthorizationFlagDefaults, &_auth); + if (status != errAuthorizationSuccess) { + if (error != NULL) { + *error = [NSError errorWithDomain:SUSparkleErrorDomain code:SUAuthenticationFailure userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Failed creating authorization reference with status code %d", status] }]; + } + _auth = NULL; + return NO; + } + return YES; +} + +- (void)dealloc +{ + if (_auth != NULL) { + AuthorizationFree(_auth, kAuthorizationFlagDefaults); + } +} + +// Wrapper around getxattr() +- (ssize_t)getXAttr:(NSString *)nameString fromFile:(NSString *)file options:(int)options +{ + char path[PATH_MAX] = {0}; + if (![file getFileSystemRepresentation:path maxLength:sizeof(path)]) { + errno = 0; + return -1; + } + + const char *name = [nameString cStringUsingEncoding:NSASCIIStringEncoding]; + if (name == NULL) { + errno = 0; + return -1; + } + + return getxattr(path, name, NULL, 0, 0, options); +} + +// Wrapper around removexattr() +- (int)removeXAttr:(NSString *)name fromFile:(NSString *)file options:(int)options +{ + char path[PATH_MAX] = {0}; + if (![file getFileSystemRepresentation:path maxLength:sizeof(path)]) { + errno = 0; + return -1; + } + + const char *attr = [name cStringUsingEncoding:NSASCIIStringEncoding]; + if (attr == NULL) { + errno = 0; + return -1; + } + + return removexattr(path, attr, options); +} + +#define XATTR_UTILITY_PATH "/usr/bin/xattr" +// Recursively remove an xattr at a specified root URL with authentication +- (BOOL)removeXAttrWithAuthentication:(NSString *)name fromRootURL:(NSURL *)rootURL error:(NSError *__autoreleasing *)error +{ + if (![_fileManager fileExistsAtPath:@(XATTR_UTILITY_PATH)]) { + if (error != NULL) { + *error = [NSError errorWithDomain:NSCocoaErrorDomain code:NSFileNoSuchFileError userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"%@ does not exist", @(XATTR_UTILITY_PATH)] }]; + } + return NO; + } + + char path[PATH_MAX] = {0}; + if (![rootURL.path getFileSystemRepresentation:path maxLength:sizeof(path)]) { + if (error != NULL) { + *error = [NSError errorWithDomain:NSCocoaErrorDomain code:NSFileReadInvalidFileNameError userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"%@ is not a valid file system representation", rootURL.path] }]; + } + return NO; + } + + const char *xattrName = [name cStringUsingEncoding:NSASCIIStringEncoding]; + if (xattrName == NULL) { + if (error != NULL) { + *error = [NSError errorWithDomain:NSCocoaErrorDomain code:NSFileWriteInapplicableStringEncodingError userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"%@ is not a valid ASCII convertible string", name] }]; + } + return NO; + } + + if (![self acquireAuthorizationWithError:error]) { + return NO; + } + + BOOL success = AuthorizationExecuteWithPrivilegesAndWait(_auth, XATTR_UTILITY_PATH, kAuthorizationFlagDefaults, (char *[]){ "-s", "-r", "-d", (char *)xattrName, path, NULL }); + + if (!success && error != NULL) { + NSString *errorMessage = [NSString stringWithFormat:@"Authenticated xattr deletion for attribute %@ failed on %@", name, rootURL.path]; + *error = [NSError errorWithDomain:SUSparkleErrorDomain code:SUAuthenticationFailure userInfo:@{ NSLocalizedDescriptionKey:errorMessage }]; + } + + return success; +} + +#define APPLE_QUARANTINE_IDENTIFIER @"com.apple.quarantine" + +// Removes the directory tree rooted at |root| from the file quarantine. +// The quarantine was introduced on OS X 10.5 and is described at: +// +// http://developer.apple.com/releasenotes/Carbon/RN-LaunchServices/index.html#apple_ref/doc/uid/TP40001369-DontLinkElementID_2 +// +// If |root| is not a directory, then it alone is removed from the quarantine. +// Symbolic links, including |root| if it is a symbolic link, will not be +// traversed. +- (BOOL)releaseItemFromQuarantineAtRootURL:(NSURL *)rootURL error:(NSError * __autoreleasing *)error +{ +#if __MAC_OS_X_VERSION_MIN_REQUIRED < 101000 /* MAC_OS_X_VERSION_10_10 */ + if (!&NSURLQuarantinePropertiesKey) { + return [self releaseItemUsingOldMethodFromQuarantineAtRootURL:rootURL error:error]; + } +#endif + + BOOL success = YES; + id rootResourceValue = nil; + if ([rootURL getResourceValue:&rootResourceValue forKey:NSURLQuarantinePropertiesKey error:NULL] && rootResourceValue != nil) { + NSError *setResourceError = nil; + if (![rootURL setResourceValue:[NSNull null] forKey:NSURLQuarantinePropertiesKey error:&setResourceError]) { + if (NS_HAS_PERMISSION_ERROR(setResourceError)) { + return [self removeXAttrWithAuthentication:APPLE_QUARANTINE_IDENTIFIER fromRootURL:rootURL error:error]; + } else { + if (error != NULL) { + *error = setResourceError; + } + // Fail, but still try to release other items from quarantine + success = NO; + } + } + } + + // Only recurse if it's actually a directory. Don't recurse into a + // root-level symbolic link. + NSDictionary *rootAttributes = [_fileManager attributesOfItemAtPath:rootURL.path error:nil]; + NSString *rootType = rootAttributes[NSFileType]; + + if (rootType == NSFileTypeDirectory) { + // The NSDirectoryEnumerator will avoid recursing into any contained + // symbolic links, so no further type checks are needed. + NSDirectoryEnumerator *directoryEnumerator = [_fileManager enumeratorAtURL:rootURL includingPropertiesForKeys:nil options:(NSDirectoryEnumerationOptions)0 errorHandler:nil]; + + for (NSURL *file in directoryEnumerator) { + id fileResourceValue = nil; + if ([file getResourceValue:&fileResourceValue forKey:NSURLQuarantinePropertiesKey error:NULL] && fileResourceValue != nil) { + NSError *setResourceError = nil; + if (![file setResourceValue:[NSNull null] forKey:NSURLQuarantinePropertiesKey error:&setResourceError]) { + if (NS_HAS_PERMISSION_ERROR(setResourceError)) { + return [self removeXAttrWithAuthentication:APPLE_QUARANTINE_IDENTIFIER fromRootURL:rootURL error:error]; + } else { + // Make sure we haven't already run into an error + if (success && error != NULL) { + *error = setResourceError; + } + // Fail, but still try to release other items from quarantine + success = NO; + } + } + } + } + } + + return success; +} + +// Ordinarily, the quarantine is managed by calling LSSetItemAttribute +// to set the kLSItemQuarantineProperties attribute to a dictionary specifying +// the quarantine properties to be applied. However, it does not appear to be +// possible to remove an item from the quarantine directly through any public +// Launch Services calls. Instead, this method takes advantage of the fact +// that the quarantine is implemented in part by setting an extended attribute, +// "com.apple.quarantine", on affected files. Removing this attribute is +// sufficient to remove files from the quarantine. +- (BOOL)releaseItemUsingOldMethodFromQuarantineAtRootURL:(NSURL *)rootURL error:(NSError *__autoreleasing *)error +{ + BOOL success = YES; + NSString *root = rootURL.path; + const int removeXAttrOptions = XATTR_NOFOLLOW; + + if ([self getXAttr:APPLE_QUARANTINE_IDENTIFIER fromFile:root options:removeXAttrOptions] >= 0) { + if ([self removeXAttr:APPLE_QUARANTINE_IDENTIFIER fromFile:root options:removeXAttrOptions] != 0) { + if (errno == EACCES) { + return [self removeXAttrWithAuthentication:APPLE_QUARANTINE_IDENTIFIER fromRootURL:rootURL error:error]; + } else { + if (error != NULL) { + *error = [NSError errorWithDomain:NSPOSIXErrorDomain code:errno userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Failed to remove xattr %@ on %@", APPLE_QUARANTINE_IDENTIFIER, root] }]; + } + // Fail, but still try to release other items from quarantine + success = NO; + } + } + } + + // Only recurse if it's actually a directory. Don't recurse into a + // root-level symbolic link. + NSDictionary *rootAttributes = [_fileManager attributesOfItemAtPath:root error:nil]; + NSString *rootType = rootAttributes[NSFileType]; + + if ([rootType isEqualToString:NSFileTypeDirectory]) { + // The NSDirectoryEnumerator will avoid recursing into any contained + // symbolic links, so no further type checks are needed. + NSDirectoryEnumerator *directoryEnumerator = [_fileManager enumeratorAtPath:root]; + NSString *file = nil; + while ((file = [directoryEnumerator nextObject])) { + NSString *filePath = [root stringByAppendingPathComponent:file]; + if ([self getXAttr:APPLE_QUARANTINE_IDENTIFIER fromFile:filePath options:removeXAttrOptions] >= 0) { + if ([self removeXAttr:APPLE_QUARANTINE_IDENTIFIER fromFile:filePath options:removeXAttrOptions] != 0) { + if (errno == EACCES) { + return [self removeXAttrWithAuthentication:APPLE_QUARANTINE_IDENTIFIER fromRootURL:rootURL error:error]; + } else { + // Make sure we haven't already run into an error + if (success && error != NULL) { + *error = [NSError errorWithDomain:NSPOSIXErrorDomain code:errno userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Failed to remove xattr %@ on %@", APPLE_QUARANTINE_IDENTIFIER, filePath] }]; + } + // Fail, but still try to release other items from quarantine + success = NO; + } + } + } + } + } + + return success; +} + +- (BOOL)moveItemAtURL:(NSURL *)sourceURL toURL:(NSURL *)destinationURL error:(NSError *__autoreleasing *)error +{ + if (![_fileManager fileExistsAtPath:sourceURL.path]) { + if (error != NULL) { + *error = [NSError errorWithDomain:NSCocoaErrorDomain code:NSFileNoSuchFileError userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Source %@ does not exist", sourceURL.path] }]; + } + return NO; + } + + if ([_fileManager fileExistsAtPath:destinationURL.path]) { + if (error != NULL) { + *error = [NSError errorWithDomain:NSCocoaErrorDomain code:NSFileWriteFileExistsError userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Destination %@ already exists", destinationURL.path] }]; + } + return NO; + } + + NSError *moveError = nil; + if ([_fileManager moveItemAtURL:sourceURL toURL:destinationURL error:&moveError]) { + return YES; + } + + if (!NS_HAS_PERMISSION_ERROR(moveError)) { + if (error != NULL) { + *error = moveError; + } + return NO; + } + + if (![self acquireAuthorizationWithError:error]) { + return NO; + } + + char sourcePath[PATH_MAX] = {0}; + if (![sourceURL.path getFileSystemRepresentation:sourcePath maxLength:sizeof(sourcePath)]) { + if (error != NULL) { + *error = [NSError errorWithDomain:NSCocoaErrorDomain code:NSFileReadInvalidFileNameError userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"%@ is not a valid file system representation", sourceURL.path] }]; + } + return NO; + } + + char destinationPath[PATH_MAX] = {0}; + if (![destinationURL.path getFileSystemRepresentation:destinationPath maxLength:sizeof(destinationPath)]) { + if (error != NULL) { + *error = [NSError errorWithDomain:NSCocoaErrorDomain code:NSFileReadInvalidFileNameError userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"%@ is not a valid file system representation", destinationURL.path] }]; + } + return NO; + } + + if (!AuthorizationExecuteWithPrivilegesAndWait(_auth, "/bin/mv", kAuthorizationFlagDefaults, (char *[]){ "-f", sourcePath, destinationPath, NULL })) { + if (error != NULL) { + NSString *errorMessage = [NSString stringWithFormat:@"Authenticated file move from %@ to %@ failed.", sourceURL, destinationURL]; + *error = [NSError errorWithDomain:SUSparkleErrorDomain code:SUAuthenticationFailure userInfo:@{ NSLocalizedDescriptionKey:errorMessage }]; + } + return NO; + } + + return YES; +} + +- (BOOL)changeOwnerAndGroupOfItemAtRootURL:(NSURL *)targetURL toMatchURL:(NSURL *)matchURL error:(NSError * __autoreleasing *)error +{ + if (![_fileManager fileExistsAtPath:targetURL.path]) { + if (error != NULL) { + *error = [NSError errorWithDomain:NSCocoaErrorDomain code:NSFileNoSuchFileError userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Item at target %@ does not exist", targetURL.path] }]; + } + return NO; + } + + if (![_fileManager fileExistsAtPath:matchURL.path]) { + if (error != NULL) { + *error = [NSError errorWithDomain:NSCocoaErrorDomain code:NSFileNoSuchFileError userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Item at URL to match %@ does not exist", matchURL.path] }]; + } + return NO; + } + + NSError *matchFileAttributesError = nil; + NSDictionary *matchFileAttributes = [_fileManager attributesOfItemAtPath:matchURL.path error:&matchFileAttributesError]; + if (matchFileAttributes == nil) { + if (error != NULL) { + *error = matchFileAttributesError; + } + return NO; + } + + NSError *targetFileAttributesError = nil; + NSDictionary *targetFileAttributes = [_fileManager attributesOfItemAtPath:targetURL.path error:&targetFileAttributesError]; + if (targetFileAttributes == nil) { + if (error != NULL) { + *error = targetFileAttributesError; + } + return NO; + } + + NSNumber *ownerID = matchFileAttributes[NSFileOwnerAccountID]; + if (ownerID == nil) { + // shouldn't be possible to error here, but just in case + if (error != NULL) { + *error = [NSError errorWithDomain:NSCocoaErrorDomain code:NSFileReadNoPermissionError userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"owner ID could not be read from %@", matchURL.path] }]; + } + return NO; + } + + NSNumber *groupID = matchFileAttributes[NSFileGroupOwnerAccountID]; + if (groupID == nil) { + // shouldn't be possible to error here, but just in case + if (error != NULL) { + *error = [NSError errorWithDomain:NSCocoaErrorDomain code:NSFileReadNoPermissionError userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"group ID could not be read from %@", matchURL.path] }]; + } + return NO; + } + + if ([ownerID isEqualToNumber:targetFileAttributes[NSFileOwnerAccountID]] && [groupID isEqualToNumber:targetFileAttributes[NSFileGroupOwnerAccountID]]) { + // Assume they're the same even if we don't check every file recursively + // Speeds up the common case + return YES; + } + + NSDirectoryEnumerator *directoryEnumerator = [_fileManager enumeratorAtURL:targetURL includingPropertiesForKeys:nil options:(NSDirectoryEnumerationOptions)0 errorHandler:nil]; + BOOL needsAuth = NO; + for (NSURL *url in directoryEnumerator) { + char path[PATH_MAX] = {0}; + if (![url.path getFileSystemRepresentation:path maxLength:sizeof(path)]) { + if (error != NULL) { + *error = [NSError errorWithDomain:NSCocoaErrorDomain code:NSFileReadInvalidFileNameError userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"%@ is not a valid file system representation", url.path] }]; + } + return NO; + } + + if (chown(path, ownerID.unsignedIntValue, groupID.unsignedIntValue) != 0) { + if (errno == EPERM) { + needsAuth = YES; + break; + } else { + if (error != NULL) { + *error = [NSError errorWithDomain:NSPOSIXErrorDomain code:errno userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Failed to chown %@ with owner ID %u and group ID %u", url.path, ownerID.unsignedIntValue, groupID.unsignedIntValue] }]; + } + return NO; + } + } + } + + if (!needsAuth) { + return YES; + } + + char targetPath[PATH_MAX] = {0}; + if (![targetURL.path getFileSystemRepresentation:targetPath maxLength:sizeof(targetPath)]) { + if (error != NULL) { + *error = [NSError errorWithDomain:NSCocoaErrorDomain code:NSFileReadInvalidFileNameError userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"%@ is not a valid file system representation", targetURL.path] }]; + } + return NO; + } + + NSString *formattedUserAndGroupIDs = [NSString stringWithFormat:@"%u:%u", ownerID.unsignedIntValue, groupID.unsignedIntValue]; + const char *userAndGroup = [formattedUserAndGroupIDs cStringUsingEncoding:NSASCIIStringEncoding]; + if (userAndGroup == NULL) { + if (error != NULL) { + *error = [NSError errorWithDomain:NSCocoaErrorDomain code:NSFormattingError userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Owner ID %u and Group ID %u could not be formatted", ownerID.unsignedIntValue, groupID.unsignedIntValue] }]; + } + return NO; + } + + if (![self acquireAuthorizationWithError:error]) { + return NO; + } + + BOOL success = AuthorizationExecuteWithPrivilegesAndWait(_auth, "/usr/sbin/chown", kAuthorizationFlagDefaults, (char *[]){ "-R", (char *)userAndGroup, targetPath, NULL }); + if (!success && error != NULL) { + NSString *errorMessage = [NSString stringWithFormat:@"Failed to chown -R \"%@\" \"%@\" with authentication", formattedUserAndGroupIDs, targetURL.path]; + *error = [NSError errorWithDomain:SUSparkleErrorDomain code:SUAuthenticationFailure userInfo:@{ NSLocalizedDescriptionKey: errorMessage }]; + } + + return success; +} + +// Creates a directory at the item pointed by url +// An item cannot already exist at the url, but the parent must be a directory that exists +- (BOOL)makeDirectoryAtURL:(NSURL *)url error:(NSError * __autoreleasing *)error +{ + if ([_fileManager fileExistsAtPath:url.path]) { + if (error != NULL) { + *error = [NSError errorWithDomain:NSCocoaErrorDomain code:NSFileWriteFileExistsError userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Item at %@ already exists", url.path] }]; + } + return NO; + } + + NSURL *parentURL = [url URLByDeletingLastPathComponent]; + BOOL isParentADirectory = NO; + if (![_fileManager fileExistsAtPath:parentURL.path isDirectory:&isParentADirectory] || !isParentADirectory) { + if (error != NULL) { + *error = [NSError errorWithDomain:NSCocoaErrorDomain code:NSFileNoSuchFileError userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Directory at %@ does not exist", parentURL.path] }]; + } + return NO; + } + + NSError *createDirectoryError = nil; + if ([_fileManager createDirectoryAtURL:url withIntermediateDirectories:NO attributes:nil error:&createDirectoryError]) { + return YES; + } + + if (!NS_HAS_PERMISSION_ERROR(createDirectoryError)) { + if (error != NULL) { + *error = createDirectoryError; + } + return NO; + } + + char path[PATH_MAX] = {0}; + if (![url.path getFileSystemRepresentation:path maxLength:sizeof(path)]) { + if (error != NULL) { + *error = [NSError errorWithDomain:NSCocoaErrorDomain code:NSFileReadInvalidFileNameError userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"%@ is not a valid file system representation", url.path] }]; + } + return NO; + } + + if (![self acquireAuthorizationWithError:error]) { + return NO; + } + + BOOL success = AuthorizationExecuteWithPrivilegesAndWait(_auth, "/bin/mkdir", kAuthorizationFlagDefaults, (char *[]){ path, NULL }); + if (!success && error != NULL) { + NSString *errorMessage = [NSString stringWithFormat:@"Failed to make directory %@ with authentication", url.path]; + *error = [NSError errorWithDomain:SUSparkleErrorDomain code:SUAuthenticationFailure userInfo:@{ NSLocalizedDescriptionKey: errorMessage }]; + } + return success; +} + +- (NSURL *)makeTemporaryDirectoryWithPreferredName:(NSString *)preferredName appropriateForDirectoryURL:(NSURL *)directoryURL error:(NSError * __autoreleasing *)error +{ + NSError *tempError = nil; + NSURL *tempURL = [_fileManager URLForDirectory:NSItemReplacementDirectory inDomain:NSUserDomainMask appropriateForURL:directoryURL create:YES error:&tempError]; + + if (tempURL != nil) { + return tempURL; + } + + // It is pretty unlikely in my testing we will get here, but just in case we do, we should create a directory inside + // the directory pointed by directoryURL, using the preferredName + + NSURL *desiredURL = [directoryURL URLByAppendingPathComponent:preferredName]; + NSUInteger tagIndex = 1; + while ([_fileManager fileExistsAtPath:desiredURL.path] && tagIndex <= 9999) { + desiredURL = [directoryURL URLByAppendingPathComponent:[preferredName stringByAppendingFormat:@" (%lu)", (unsigned long)++tagIndex]]; + } + + return [self makeDirectoryAtURL:desiredURL error:error] ? desiredURL : nil; +} + +- (BOOL)removeItemAtURL:(NSURL *)url error:(NSError * __autoreleasing *)error +{ + if (![_fileManager fileExistsAtPath:url.path]) { + if (error != NULL) { + *error = [NSError errorWithDomain:NSCocoaErrorDomain code:NSFileNoSuchFileError userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Item at %@ does not exist", url.path] }]; + } + return NO; + } + + NSError *removeError = nil; + if ([_fileManager removeItemAtURL:url error:&removeError]) { + return YES; + } + + if (!NS_HAS_PERMISSION_ERROR(removeError)) { + if (error != NULL) { + *error = removeError; + } + return NO; + } + + if (![self acquireAuthorizationWithError:error]) { + return NO; + } + + char path[PATH_MAX] = {0}; + if (![url.path getFileSystemRepresentation:path maxLength:sizeof(path)]) { + if (error != NULL) { + *error = [NSError errorWithDomain:NSCocoaErrorDomain code:NSFileReadInvalidFileNameError userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"%@ is not a valid file system representation", url.path] }]; + } + return NO; + } + + BOOL success = AuthorizationExecuteWithPrivilegesAndWait(_auth, "/bin/rm", kAuthorizationFlagDefaults, (char *[]){ "-rf", path, NULL }); + if (!success && error != NULL) { + *error = [NSError errorWithDomain:SUSparkleErrorDomain code:SUAuthenticationFailure userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Failed to rm -rf \"%@\" with authentication", url.path] }]; + } + return success; +} + +// TODO: Fix or address that this method only runs on 10.8 or later +- (BOOL)moveItemAtURLToTrash:(NSURL *)url error:(NSError *__autoreleasing *)error +{ + if (![_fileManager fileExistsAtPath:url.path]) { + if (error != NULL) { + *error = [NSError errorWithDomain:NSCocoaErrorDomain code:NSFileNoSuchFileError userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Item at %@ does not exist", url.path] }]; + } + return NO; + } + + // TODO: address NSTrashDirectory being only available in 10.8 or later + NSURL *trashURL = [[_fileManager URLsForDirectory:NSTrashDirectory inDomains:NSUserDomainMask] firstObject]; + if (trashURL == nil) { + if (error != NULL) { + *error = [NSError errorWithDomain:NSCocoaErrorDomain code:NSFileNoSuchFileError userInfo:@{ NSLocalizedDescriptionKey: @"User's Trash directory was not found" }]; + } + return NO; + } + + // In the rare worst case scenario, our temporary directory will be labeled with "Incomplete" and be in the user's trash directory, + // indicating that whatever inside of there is not yet completely moved. + // Regardless, we want the item to be in our Volume before we try moving it to the trash + NSString *preferredName = [url.lastPathComponent.stringByDeletingPathExtension stringByAppendingString:@" (Incomplete)"]; + NSURL *tempDirectory = [self makeTemporaryDirectoryWithPreferredName:preferredName appropriateForDirectoryURL:trashURL error:error]; + if (tempDirectory == nil) { + return NO; + } + + NSURL *tempItemURL = [tempDirectory URLByAppendingPathComponent:url.lastPathComponent]; + if (![self moveItemAtURL:url toURL:tempItemURL error:error]) { + // If we can't move the item at url, just remove it completely; chances are it's not going to be missed + [self removeItemAtURL:url error:NULL]; + [self removeItemAtURL:tempDirectory error:NULL]; + return NO; + } + + if (![self changeOwnerAndGroupOfItemAtRootURL:tempItemURL toMatchURL:trashURL error:error]) { + // Removing the item inside of the temp directory is better than trying to move the item to the trash with incorrect ownership + [self removeItemAtURL:tempDirectory error:NULL]; + return NO; + } + + // If we get here, we should be able to trash the item normally without authentication + // TODO: address -[NSFileManager trashItemAtURL: resultingItemURL: error:] being 10.8+ only + NSError *trashError = nil; + BOOL success = [_fileManager trashItemAtURL:tempItemURL resultingItemURL:NULL error:&trashError]; + if (!success && error != NULL) { + *error = trashError; + } + + [self removeItemAtURL:tempDirectory error:NULL]; + + return success; +} + +@end + +#pragma clang diagnostic pop diff --git a/Sparkle/SUPlainInstaller.m b/Sparkle/SUPlainInstaller.m index cea72914b1..4ae02f7537 100644 --- a/Sparkle/SUPlainInstaller.m +++ b/Sparkle/SUPlainInstaller.m @@ -7,13 +7,136 @@ // #import "SUPlainInstaller.h" -#import "SUPlainInstallerInternals.h" +#import "SUFileManager.h" #import "SUCodeSigningVerifier.h" #import "SUConstants.h" #import "SUHost.h" +#import "SULog.h" @implementation SUPlainInstaller ++ (BOOL)performInstallationToURL:(NSURL *)installationURL withOldURL:(NSURL *)oldURL newURL:(NSURL *)newURL error:(NSError * __autoreleasing *)error +{ + SUFileManager *fileManager = [[SUFileManager alloc] init]; + + // Create a temporary directory for our new app that resides on our destination's volume + NSURL *tempNewDirectoryURL = [fileManager makeTemporaryDirectoryWithPreferredName:[installationURL.lastPathComponent.stringByDeletingPathExtension stringByAppendingString:@" (Incomplete)"] appropriateForDirectoryURL:installationURL.URLByDeletingLastPathComponent error:error]; + if (tempNewDirectoryURL == nil) { + SULog(@"Failed to make new temp directory"); + return NO; + } + + // Move the new app to our temporary directory + NSURL *newTempURL = [tempNewDirectoryURL URLByAppendingPathComponent:newURL.lastPathComponent]; + if (![fileManager moveItemAtURL:newURL toURL:newTempURL error:error]) { + SULog(@"Failed to move the new app from %@ to its temp directory at %@", newURL.path, newTempURL.path); + [fileManager removeItemAtURL:tempNewDirectoryURL error:NULL]; + return NO; + } + + // Release our new app from quarantine and fix its owner and group IDs while it's at our temporary destination + // We must leave moving the app to its destination as the final step in installing it, so that + // it's not possible our new app can be left in an incomplete state at the final destination + + NSError *quarantineError = nil; + if (![fileManager releaseItemFromQuarantineAtRootURL:newTempURL error:&quarantineError]) { + // Not big enough of a deal to fail the entire installation + SULog(@"Failed to release quarantine at %@ with error %@", newTempURL.path, quarantineError); + } + + if (![fileManager changeOwnerAndGroupOfItemAtRootURL:newTempURL toMatchURL:oldURL error:error]) { + // But this is big enough of a deal to fail + SULog(@"Failed to change owner and group of new app at %@ to match old app at %@", newTempURL.path, oldURL.path); + [fileManager removeItemAtURL:tempNewDirectoryURL error:NULL]; + return NO; + } + + // If the installation and old URL differ, the sooner we can install our new app + // because we won't have to move the old app out of the way first + // increasing our chances of having it installed in case something goes wrong + if (![installationURL isEqual:oldURL]) { + // Move the new app to its final destination + if (![fileManager moveItemAtURL:newTempURL toURL:installationURL error:error]) { + SULog(@"Failed to move new app at %@ to final destination %@ (with installationURL != oldURL)", newTempURL.path, installationURL.path); + [fileManager removeItemAtURL:tempNewDirectoryURL error:NULL]; + return NO; + } + + // Cleanup: move the old app to the trash + // We will first have to move the app to a temporary location that the user may not care about + // This is necessary because the operation could fail mid-way through + // Nothing past here will be a fatal error because we already installed the new app + + // Create a temporary directory for our old app that resides on its volume + NSError *makeOldTempDirectoryError = nil; + NSURL *tempOldDirectoryURL = [fileManager makeTemporaryDirectoryWithPreferredName:oldURL.lastPathComponent.stringByDeletingPathExtension appropriateForDirectoryURL:oldURL.URLByDeletingLastPathComponent error:&makeOldTempDirectoryError]; + if (tempOldDirectoryURL == nil) { + SULog(@"Failed to create temporary directory for old app at %@ after finishing installation. This is not a fatal error. Error: %@", oldURL.path, makeOldTempDirectoryError); + } else { + // Move the old app to the temporary directory + NSURL *oldTempURL = [tempOldDirectoryURL URLByAppendingPathComponent:oldURL.lastPathComponent]; + NSError *moveOldAppError = nil; + if (![fileManager moveItemAtURL:oldURL toURL:oldTempURL error:&moveOldAppError]) { + SULog(@"Failed to move the old app at %@ to a temporary location at %@. This is not a fatal error. Error: %@", oldURL.path, oldTempURL.path, moveOldAppError); + } else { + // Finally try to trash our old app + NSError *trashError = nil; + if (![fileManager moveItemAtURLToTrash:oldTempURL error:&trashError]) { + SULog(@"Failed to move %@ to trash. This is not a fatal error. Error: %@", oldURL, trashError); + } + } + + [fileManager removeItemAtURL:tempOldDirectoryURL error:NULL]; + } + } else { + // Create a temporary directory for our old app that resides on its volume + NSURL *tempOldDirectoryURL = [fileManager makeTemporaryDirectoryWithPreferredName:oldURL.lastPathComponent.stringByDeletingPathExtension appropriateForDirectoryURL:oldURL.URLByDeletingLastPathComponent error:error]; + if (tempOldDirectoryURL == nil) { + SULog(@"Failed to create temporary directory for old app at %@", oldURL.path); + [fileManager removeItemAtURL:tempNewDirectoryURL error:NULL]; + return NO; + } + + // Move the old app to the temporary directory + NSURL *oldTempURL = [tempOldDirectoryURL URLByAppendingPathComponent:oldURL.lastPathComponent]; + if (![fileManager moveItemAtURL:oldURL toURL:oldTempURL error:error]) { + SULog(@"Failed to move the old app at %@ to a temporary location at %@", oldURL.path, oldTempURL.path); + + // Just forget about our updated app on failure + [fileManager removeItemAtURL:tempNewDirectoryURL error:NULL]; + [fileManager removeItemAtURL:tempOldDirectoryURL error:NULL]; + + return NO; + } + + // Move the new app to its final destination + if (![fileManager moveItemAtURL:newTempURL toURL:installationURL error:error]) { + SULog(@"Failed to move new app at %@ to final destination %@", newTempURL.path, installationURL.path); + + // Forget about our updated app on failure + [fileManager removeItemAtURL:tempNewDirectoryURL error:NULL]; + + // Attempt to restore our old app back the way it was on failure + [fileManager moveItemAtURL:oldTempURL toURL:oldURL error:NULL]; + [fileManager removeItemAtURL:tempOldDirectoryURL error:NULL]; + + return NO; + } + + // Cleanup: move the old app to the trash + NSError *trashError = nil; + if (![fileManager moveItemAtURLToTrash:oldTempURL error:&trashError]) { + SULog(@"Failed to move %@ to trash with error %@", oldTempURL, trashError); + } + + [fileManager removeItemAtURL:tempOldDirectoryURL error:NULL]; + } + + [fileManager removeItemAtURL:tempNewDirectoryURL error:NULL]; + + return YES; +} + + (void)performInstallationToPath:(NSString *)installationPath fromPath:(NSString *)path host:(SUHost *)host versionComparator:(id)comparator completionHandler:(void (^)(NSError *))completionHandler { SUParameterAssert(host); @@ -30,24 +153,11 @@ + (void)performInstallationToPath:(NSString *)installationPath fromPath:(NSStrin dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ NSError *error = nil; - NSString *oldPath = [host bundlePath]; - NSString *tempName = [self temporaryNameForPath:[host installationPath]]; - - BOOL result = [self copyPathWithAuthentication:path overPath:installationPath temporaryName:tempName error:&error]; + NSURL *oldURL = [NSURL fileURLWithPath:[host bundlePath]]; + NSURL *newURL = [NSURL fileURLWithPath:path]; + NSURL *installationURL = [NSURL fileURLWithPath:installationPath]; - if (result) { - if ([SUCodeSigningVerifier applicationAtPathIsCodeSigned:installationPath]) { - result = [SUCodeSigningVerifier codeSignatureIsValidAtPath:installationPath error:&error]; - } - } - - if (result) { - BOOL haveOld = [[NSFileManager defaultManager] fileExistsAtPath:oldPath]; - BOOL differentFromNew = ![oldPath isEqualToString:installationPath]; - if (haveOld && differentFromNew) { - [self _movePathToTrash:oldPath]; // On success, trash old copy if there's still one due to renaming. - } - } + BOOL result = [self performInstallationToURL:installationURL withOldURL:oldURL newURL:newURL error:&error]; dispatch_async(dispatch_get_main_queue(), ^{ [self finishInstallationToPath:installationPath withResult:result error:error completionHandler:completionHandler]; diff --git a/Sparkle/SUPlainInstallerInternals.h b/Sparkle/SUPlainInstallerInternals.h deleted file mode 100644 index f05cae9167..0000000000 --- a/Sparkle/SUPlainInstallerInternals.h +++ /dev/null @@ -1,24 +0,0 @@ -// -// SUPlainInstallerInternals.m -// Sparkle -// -// Created by Andy Matuschak on 3/9/06. -// Copyright 2006 Andy Matuschak. All rights reserved. -// - -#ifndef SUPLAININSTALLERINTERNALS_H -#define SUPLAININSTALLERINTERNALS_H - -#import - -#import "SUPlainInstaller.h" - -@interface SUPlainInstaller (Internals) -+ (NSString *)temporaryNameForPath:(NSString *)path; -+ (BOOL)copyPathWithAuthentication:(NSString *)src overPath:(NSString *)dst temporaryName:(NSString *)tmp error:(NSError **)error; -+ (void)_movePathToTrash:(NSString *)path; -+ (BOOL)_removeFileAtPath:(NSString *)path error:(NSError **)error; -+ (BOOL)_removeFileAtPathWithForcedAuthentication:(NSString *)src error:(NSError **)error; -@end - -#endif diff --git a/Sparkle/SUPlainInstallerInternals.m b/Sparkle/SUPlainInstallerInternals.m deleted file mode 100644 index 75ae5a5cdf..0000000000 --- a/Sparkle/SUPlainInstallerInternals.m +++ /dev/null @@ -1,639 +0,0 @@ -// -// SUPlainInstallerInternals.m -// Sparkle -// -// Created by Andy Matuschak on 3/9/06. -// Copyright 2006 Andy Matuschak. All rights reserved. -// - -#import "SUUpdater.h" - -#import "SUAppcast.h" -#import "SUAppcastItem.h" -#import "SUVersionComparisonProtocol.h" -#import "SUPlainInstallerInternals.h" -#import "SUConstants.h" -#import "SULog.h" - -#include -#include -#include -#include -#include -#include -#include - -#if __MAC_OS_X_VERSION_MAX_ALLOWED < 101000 -extern NSString *const NSURLQuarantinePropertiesKey WEAK_IMPORT_ATTRIBUTE; -#endif - -static inline void PerformOnMainThreadSync(dispatch_block_t theBlock) -{ - if ([NSThread isMainThread]) { - theBlock(); - } else { - dispatch_sync(dispatch_get_main_queue(), theBlock); - } -} - -@interface SUPlainInstaller (MMExtendedAttributes) -// Removes the directory tree rooted at |root| from the file quarantine. -// The quarantine was introduced on OS X 10.5 and is described at: -// -// http://developer.apple.com/releasenotes/Carbon/RN-LaunchServices/index.html -//#apple_ref/doc/uid/TP40001369-DontLinkElementID_2 -// -// If |root| is not a directory, then it alone is removed from the quarantine. -// Symbolic links, including |root| if it is a symbolic link, will not be -// traversed. -// -// Ordinarily, the quarantine is managed by calling LSSetItemAttribute -// to set the kLSItemQuarantineProperties attribute to a dictionary specifying -// the quarantine properties to be applied. However, it does not appear to be -// possible to remove an item from the quarantine directly through any public -// Launch Services calls. Instead, this method takes advantage of the fact -// that the quarantine is implemented in part by setting an extended attribute, -// "com.apple.quarantine", on affected files. Removing this attribute is -// sufficient to remove files from the quarantine. -+ (void)releaseFromQuarantine:(NSString *)root; -@end - -// Authorization code based on generous contribution from Allan Odgaard. Thanks, Allan! -#pragma clang diagnostic push -#pragma clang diagnostic ignored "-Wdeprecated-declarations" // this is terrible; will fix later probably -static BOOL AuthorizationExecuteWithPrivilegesAndWait(AuthorizationRef authorization, const char *executablePath, AuthorizationFlags options, char *const *arguments) -{ - // *** MUST BE SAFE TO CALL ON NON-MAIN THREAD! - - sig_t oldSigChildHandler = signal(SIGCHLD, SIG_DFL); - BOOL returnValue = YES; - - if (AuthorizationExecuteWithPrivileges(authorization, executablePath, options, arguments, NULL) == errAuthorizationSuccess) - { - int status; - pid_t pid = wait(&status); - if (pid == -1 || !WIFEXITED(status) || WEXITSTATUS(status) != 0) - returnValue = NO; - } - else - returnValue = NO; - - signal(SIGCHLD, oldSigChildHandler); - return returnValue; -} -#pragma clang diagnostic pop - -@implementation SUPlainInstaller (Internals) - -+ (NSString *)temporaryNameForPath:(NSString *)path -{ - // Let's try to read the version number so the filename will be more meaningful. - NSString *postFix; - NSString *version; - if ((version = [[NSBundle bundleWithPath:path] objectForInfoDictionaryKey:(__bridge NSString *)kCFBundleVersionKey]) && ![version isEqualToString:@""]) - { - NSMutableCharacterSet *validCharacters = [NSMutableCharacterSet alphanumericCharacterSet]; - [validCharacters formUnionWithCharacterSet:[NSCharacterSet characterSetWithCharactersInString:@".-()"]]; - postFix = [version stringByTrimmingCharactersInSet:[validCharacters invertedSet]]; - } - else - postFix = @"old"; - NSString *prefix = [[path stringByDeletingPathExtension] stringByAppendingFormat:@" (%@)", postFix]; - NSString *tempDir = [prefix stringByAppendingPathExtension:[path pathExtension]]; - // Now let's make sure we get a unique path. - unsigned int cnt = 2; - while ([[NSFileManager defaultManager] fileExistsAtPath:tempDir] && cnt <= 999) - tempDir = [NSString stringWithFormat:@"%@ %u.%@", prefix, cnt++, [path pathExtension]]; - return [tempDir lastPathComponent]; -} - -+ (NSString *)_temporaryCopyNameForPath:(NSString *)path didFindTrash:(BOOL *)outDidFindTrash -{ - // *** MUST BE SAFE TO CALL ON NON-MAIN THREAD! - NSString *tempDir = nil; - - UInt8 trashPath[MAXPATHLEN + 1] = { 0 }; - FSRef trashRef, pathRef; - FSVolumeRefNum vSrcRefNum = kFSInvalidVolumeRefNum; - FSCatalogInfo catInfo; - memset(&catInfo, 0, sizeof(catInfo)); - OSStatus err = FSPathMakeRef((const UInt8 *)[path fileSystemRepresentation], &pathRef, NULL); - if( err == noErr ) - { - err = FSGetCatalogInfo(&pathRef, kFSCatInfoVolume, &catInfo, NULL, NULL, NULL); - vSrcRefNum = catInfo.volume; - } - if (err == noErr) - err = FSFindFolder(vSrcRefNum, kTrashFolderType, kCreateFolder, &trashRef); - if (err == noErr) - err = FSGetCatalogInfo(&trashRef, kFSCatInfoVolume, &catInfo, NULL, NULL, NULL); - if (err == noErr && vSrcRefNum != catInfo.volume) - err = nsvErr; // Couldn't find a trash folder on same volume as given path. Docs say this may happen in the future. - if (err == noErr) - err = FSRefMakePath(&trashRef, trashPath, MAXPATHLEN); - if (err == noErr) - tempDir = [[NSFileManager defaultManager] stringWithFileSystemRepresentation:(char *)trashPath length:strlen((char *)trashPath)]; - if (outDidFindTrash) - *outDidFindTrash = (tempDir != nil); - if (!tempDir) - tempDir = [path stringByDeletingLastPathComponent]; - - // Let's try to read the version number so the filename will be more meaningful - NSString *prefix; - if ([[[NSBundle bundleWithIdentifier:SUBundleIdentifier] infoDictionary][SUAppendVersionNumberKey] boolValue]) { - NSString *postFix = nil; - NSString *version = nil; - if ((version = [[NSBundle bundleWithPath: path] objectForInfoDictionaryKey:(__bridge NSString *)kCFBundleVersionKey]) && ![version isEqualToString:@""]) - { - NSMutableCharacterSet *validCharacters = [NSMutableCharacterSet alphanumericCharacterSet]; - [validCharacters formUnionWithCharacterSet:[NSCharacterSet characterSetWithCharactersInString:@".-()"]]; - postFix = [version stringByTrimmingCharactersInSet:[validCharacters invertedSet]]; - } - else { - postFix = @"old"; - } - prefix = [NSString stringWithFormat:@"%@ (%@)", [[path lastPathComponent] stringByDeletingPathExtension], postFix]; - } else { - prefix = [[path lastPathComponent] stringByDeletingPathExtension]; - } - NSString *tempName = [prefix stringByAppendingPathExtension:[path pathExtension]]; - tempDir = [tempDir stringByAppendingPathComponent:tempName]; - - // Now let's make sure we get a unique path. - int cnt = 2; - while ([[NSFileManager defaultManager] fileExistsAtPath:tempDir] && cnt <= 9999) { - tempDir = [[tempDir stringByDeletingLastPathComponent] stringByAppendingPathComponent:[NSString stringWithFormat:@"%@ %d.%@", prefix, cnt++, [path pathExtension]]]; - } - - return tempDir; -} - -+ (BOOL)_copyPathWithForcedAuthentication:(NSString *)src toPath:(NSString *)dst temporaryPath:(NSString *)tmp error:(NSError *__autoreleasing *)error -{ - // *** MUST BE SAFE TO CALL ON NON-MAIN THREAD! - - char srcPath[PATH_MAX] = {0}; - [src getFileSystemRepresentation:srcPath maxLength:sizeof(srcPath)]; - - char tmpPath[PATH_MAX] = {0}; - [tmp getFileSystemRepresentation:tmpPath maxLength:sizeof(tmpPath)]; - - char dstPath[PATH_MAX] = {0}; - [dst getFileSystemRepresentation:dstPath maxLength:sizeof(dstPath)]; - - struct stat dstSB; - if (stat(dstPath, &dstSB) != 0) // Doesn't exist yet, try containing folder. - { - const char *dstDirPath = [[dst stringByDeletingLastPathComponent] fileSystemRepresentation]; - if( stat(dstDirPath, &dstSB) != 0 ) - { - NSString *errorMessage = [NSString stringWithFormat:@"Stat on %@ during authenticated file copy failed.", dst]; - if (error != NULL) - *error = [NSError errorWithDomain:SUSparkleErrorDomain code:SUFileCopyFailure userInfo:@{ NSLocalizedDescriptionKey: errorMessage }]; - return NO; - } - } - - AuthorizationRef auth = NULL; - OSStatus authStat = errAuthorizationDenied; - while (authStat == errAuthorizationDenied) { - authStat = AuthorizationCreate(NULL, - kAuthorizationEmptyEnvironment, - kAuthorizationFlagDefaults, - &auth); - } - - BOOL res = NO; - if (authStat == errAuthorizationSuccess) { - res = YES; - - char uidgid[42]; - snprintf(uidgid, sizeof(uidgid), "%u:%u", - dstSB.st_uid, dstSB.st_gid); - - // If the currently-running application is trusted, the new - // version should be trusted as well. Remove it from the - // quarantine to avoid a delay at launch, and to avoid - // presenting the user with a confusing trust dialog. - // - // This needs to be done after the application is moved to its - // new home with "mv" in case it's moved across filesystems: if - // that happens, "mv" actually performs a copy and may result - // in the application being quarantined. It also needs to be - // done before "chown" changes ownership, because the ownership - // change will almost certainly make it impossible to change - // attributes to release the files from the quarantine. - if (res) - { - SULog(@"releaseFromQuarantine"); - PerformOnMainThreadSync(^{ - [self releaseFromQuarantine:src]; - }); - } - - if (res) // Set permissions while it's still in source, so we have it with working and correct perms when it arrives at destination. - { - char *coParams[] = { "-R", uidgid, srcPath, NULL }; - res = AuthorizationExecuteWithPrivilegesAndWait(auth, "/usr/sbin/chown", kAuthorizationFlagDefaults, coParams); - if (!res) - SULog(@"chown -R %@ %@ failed.", @(uidgid), @(srcPath)); - } - - BOOL haveDst = [[NSFileManager defaultManager] fileExistsAtPath:dst]; - if (res && haveDst) // If there's something at our tmp path (previous failed update or whatever) delete that first. - { - char *rmParams[] = { "-rf", tmpPath, NULL }; - res = AuthorizationExecuteWithPrivilegesAndWait(auth, "/bin/rm", kAuthorizationFlagDefaults, rmParams); - if (!res) - SULog(@"rm failed"); - } - - if (res && haveDst) // Move old exe to tmp path. - { - char *mvParams[] = { "-f", dstPath, tmpPath, NULL }; - res = AuthorizationExecuteWithPrivilegesAndWait(auth, "/bin/mv", kAuthorizationFlagDefaults, mvParams); - if (!res) - SULog(@"mv 1 failed"); - } - - if (res) // Move new exe to old exe's path. - { - char *mvParams2[] = { "-f", srcPath, dstPath, NULL }; - res = AuthorizationExecuteWithPrivilegesAndWait(auth, "/bin/mv", kAuthorizationFlagDefaults, mvParams2); - if (!res) - SULog(@"mv 2 failed"); - } - - // if( res && haveDst /*&& !foundTrash*/ ) // If we managed to put the old exe in the trash, leave it there for the user to delete or recover. - // { // ... Otherwise we better delete it, wouldn't want dozens of old versions lying around next to the new one. - // const char* rmParams2[] = { "-rf", tmpPath, NULL }; - // res = AuthorizationExecuteWithPrivilegesAndWait( auth, "/bin/rm", kAuthorizationFlagDefaults, rmParams2 ); - // } - - AuthorizationFree(auth, 0); - - // If the currently-running application is trusted, the new - // version should be trusted as well. Remove it from the - // quarantine to avoid a delay at launch, and to avoid - // presenting the user with a confusing trust dialog. - // - // This needs to be done after the application is moved to its - // new home with "mv" in case it's moved across filesystems: if - // that happens, "mv" actually performs a copy and may result - // in the application being quarantined. - if (res) - { - SULog(@"releaseFromQuarantine after installing"); - PerformOnMainThreadSync(^{ - [self releaseFromQuarantine:dst]; - }); - } - - if (!res) - { - // Something went wrong somewhere along the way, but we're not sure exactly where. - NSString *errorMessage = [NSString stringWithFormat:@"Authenticated file copy from %@ to %@ failed.", src, dst]; - if (error != nil) - *error = [NSError errorWithDomain:SUSparkleErrorDomain code:SUAuthenticationFailure userInfo:@{ NSLocalizedDescriptionKey: errorMessage }]; - } - } - else - { - if (error != nil) - *error = [NSError errorWithDomain:SUSparkleErrorDomain code:SUAuthenticationFailure userInfo:@{ NSLocalizedDescriptionKey: @"Couldn't get permission to authenticate." }]; - } - return res; -} - -+ (BOOL)_movePathWithForcedAuthentication:(NSString *)src toPath:(NSString *)dst error:(NSError *__autoreleasing *)error -{ - // *** MUST BE SAFE TO CALL ON NON-MAIN THREAD! - - char srcPath[PATH_MAX] = {0}; - [src getFileSystemRepresentation:srcPath maxLength:sizeof(srcPath)]; - - char dstPath[PATH_MAX] = {0}; - [dst getFileSystemRepresentation:dstPath maxLength:sizeof(dstPath)]; - - char dstContainerPath[PATH_MAX] = {0}; - [dst.stringByDeletingLastPathComponent getFileSystemRepresentation:dstContainerPath maxLength:sizeof(dstContainerPath)]; - - struct stat dstSB; - stat(dstContainerPath, &dstSB); - - AuthorizationRef auth = NULL; - OSStatus authStat = errAuthorizationDenied; - while( authStat == errAuthorizationDenied ) - { - authStat = AuthorizationCreate(NULL, - kAuthorizationEmptyEnvironment, - kAuthorizationFlagDefaults, - &auth); - } - - BOOL res = NO; - if (authStat == errAuthorizationSuccess) - { - res = YES; - - char uidgid[42]; - snprintf(uidgid, sizeof(uidgid), "%d:%d", - dstSB.st_uid, dstSB.st_gid); - - if (res) // Set permissions while it's still in source, so we have it with working and correct perms when it arrives at destination. - { - char *coParams[] = { "-R", uidgid, srcPath, NULL }; - res = AuthorizationExecuteWithPrivilegesAndWait(auth, "/usr/sbin/chown", kAuthorizationFlagDefaults, coParams); - if (!res) - SULog(@"Can't set permissions"); - } - - BOOL haveDst = [[NSFileManager defaultManager] fileExistsAtPath:dst]; - if (res && haveDst) // If there's something at our tmp path (previous failed update or whatever) delete that first. - { - char *rmParams[] = { "-rf", dstPath, NULL }; - res = AuthorizationExecuteWithPrivilegesAndWait(auth, "/bin/rm", kAuthorizationFlagDefaults, rmParams); - if (!res) - SULog(@"Can't remove destination file"); - } - - if (res) // Move!. - { - char *mvParams[] = { "-f", srcPath, dstPath, NULL }; - res = AuthorizationExecuteWithPrivilegesAndWait(auth, "/bin/mv", kAuthorizationFlagDefaults, mvParams); - if (!res) - SULog(@"Can't move source file"); - } - - AuthorizationFree(auth, 0); - - if (!res) - { - // Something went wrong somewhere along the way, but we're not sure exactly where. - NSString *errorMessage = [NSString stringWithFormat:@"Authenticated file move from %@ to %@ failed.", src, dst]; - if (error != NULL) - *error = [NSError errorWithDomain:SUSparkleErrorDomain code:SUAuthenticationFailure userInfo:@{ NSLocalizedDescriptionKey: errorMessage }]; - } - } - else - { - if (error != NULL) - *error = [NSError errorWithDomain:SUSparkleErrorDomain code:SUAuthenticationFailure userInfo:@{ NSLocalizedDescriptionKey: @"Couldn't get permission to authenticate." }]; - } - return res; -} - - -+ (BOOL)_removeFileAtPathWithForcedAuthentication:(NSString *)src error:(NSError *__autoreleasing *)error -{ - // *** MUST BE SAFE TO CALL ON NON-MAIN THREAD! - - char srcPath[PATH_MAX] = {0}; - [src getFileSystemRepresentation:srcPath maxLength:sizeof(srcPath)]; - - AuthorizationRef auth = NULL; - OSStatus authStat = errAuthorizationDenied; - while( authStat == errAuthorizationDenied ) - { - authStat = AuthorizationCreate(NULL, - kAuthorizationEmptyEnvironment, - kAuthorizationFlagDefaults, - &auth); - } - - BOOL res = NO; - if (authStat == errAuthorizationSuccess) - { - res = YES; - - if (res) // If there's something at our tmp path (previous failed update or whatever) delete that first. - { - char *rmParams[] = { "-rf", srcPath, NULL }; - res = AuthorizationExecuteWithPrivilegesAndWait(auth, "/bin/rm", kAuthorizationFlagDefaults, rmParams); - if (!res) - SULog(@"Can't remove destination file"); - } - - AuthorizationFree(auth, 0); - - if (!res) - { - // Something went wrong somewhere along the way, but we're not sure exactly where. - NSString *errorMessage = [NSString stringWithFormat:@"Authenticated file remove from %@ failed.", src]; - if (error != NULL) - *error = [NSError errorWithDomain:SUSparkleErrorDomain code:SUAuthenticationFailure userInfo:@{ NSLocalizedDescriptionKey: errorMessage }]; - } - } - else - { - if (error != NULL) - *error = [NSError errorWithDomain:SUSparkleErrorDomain code:SUAuthenticationFailure userInfo:@{ NSLocalizedDescriptionKey: @"Couldn't get permission to authenticate." }]; - } - return res; -} - -+ (BOOL)_removeFileAtPath:(NSString *)path error:(NSError *__autoreleasing *)error -{ - BOOL success = YES; - if( ![[NSFileManager defaultManager] removeItemAtPath: path error: NULL] ) - { - success = [self _removeFileAtPathWithForcedAuthentication:path error:error]; - } - - return success; -} - -+ (void)_movePathToTrash:(NSString *)path -{ - //SULog(@"Moving %@ to the trash.", path); - NSInteger tag = 0; - if (![[NSWorkspace sharedWorkspace] performFileOperation:NSWorkspaceRecycleOperation source:[path stringByDeletingLastPathComponent] destination:@"" files:@[[path lastPathComponent]] tag:&tag]) - { - BOOL didFindTrash = NO; - NSString *trashPath = [self _temporaryCopyNameForPath:path didFindTrash:&didFindTrash]; - if( didFindTrash ) - { - NSError *err = nil; - if (![self _movePathWithForcedAuthentication:path toPath:trashPath error:&err]) { - SULog(@"Error: couldn't move %@ to the trash (%@). %@", path, trashPath, err); - } - } - else { - SULog(@"Error: couldn't move %@ to the trash. This is often a sign of a permissions error.", path); - } - } -} - -+ (BOOL)copyPathWithAuthentication:(NSString *)src overPath:(NSString *)dst temporaryName:(NSString *)__unused tmp error:(NSError *__autoreleasing *)error -{ - FSRef srcRef, dstRef, dstDirRef, tmpDirRef; - OSStatus err; - BOOL hadFileAtDest = NO, didFindTrash = NO; - NSString *tmpPath = [self _temporaryCopyNameForPath:dst didFindTrash:&didFindTrash]; - - // Make FSRef for destination: - err = FSPathMakeRefWithOptions((const UInt8 *)[dst fileSystemRepresentation], kFSPathMakeRefDoNotFollowLeafSymlink, &dstRef, NULL); - hadFileAtDest = (err == noErr); // There is a file at the destination, move it aside. If we normalized the name, we might not get here, so don't error. - if( hadFileAtDest ) - { - if (0 != access([dst fileSystemRepresentation], W_OK) || 0 != access([[dst stringByDeletingLastPathComponent] fileSystemRepresentation], W_OK)) - { - return [self _copyPathWithForcedAuthentication:src toPath:dst temporaryPath:tmpPath error:error]; - } - } - else - { - if (0 != access([[dst stringByDeletingLastPathComponent] fileSystemRepresentation], W_OK) - || 0 != access([[[dst stringByDeletingLastPathComponent] stringByDeletingLastPathComponent] fileSystemRepresentation], W_OK)) - { - return [self _copyPathWithForcedAuthentication:src toPath:dst temporaryPath:tmpPath error:error]; - } - } - - if( hadFileAtDest ) - { - err = FSPathMakeRef((const UInt8 *)[[tmpPath stringByDeletingLastPathComponent] fileSystemRepresentation], &tmpDirRef, NULL); - if (err != noErr) - FSPathMakeRef((const UInt8 *)[[dst stringByDeletingLastPathComponent] fileSystemRepresentation], &tmpDirRef, NULL); - } - - err = FSPathMakeRef((const UInt8 *)[[dst stringByDeletingLastPathComponent] fileSystemRepresentation], &dstDirRef, NULL); - - if (err == noErr && hadFileAtDest) - { - NSFileManager *manager = [[NSFileManager alloc] init]; - BOOL success = [manager moveItemAtPath:dst toPath:tmpPath error:error]; - if (!success && hadFileAtDest) - { - if (error != NULL) - *error = [NSError errorWithDomain:SUSparkleErrorDomain code:SUFileCopyFailure userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Couldn't move %@ to %@.", dst, tmpPath] }]; - return NO; - } - } - - err = FSPathMakeRef((const UInt8 *)[src fileSystemRepresentation], &srcRef, NULL); - if (err == noErr) - { - NSFileManager *manager = [[NSFileManager alloc] init]; - BOOL success = [manager copyItemAtPath:src toPath:dst error:error]; - if (!success) - { - // We better move the old version back to its old location - if (hadFileAtDest) { - success = [manager moveItemAtPath:tmpPath toPath:dst error:error]; - } - if (!success && error != NULL) - *error = [NSError errorWithDomain:SUSparkleErrorDomain code:SUFileCopyFailure userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Couldn't move %@ to %@.", dst, tmpPath] }]; - return NO; - - } - } - - // If the currently-running application is trusted, the new - // version should be trusted as well. Remove it from the - // quarantine to avoid a delay at launch, and to avoid - // presenting the user with a confusing trust dialog. - // - // This needs to be done after the application is moved to its - // new home in case it's moved across filesystems: if that - // happens, the move is actually a copy, and it may result - // in the application being quarantined. - PerformOnMainThreadSync(^{ - [self releaseFromQuarantine:dst]; - }); - - return YES; -} - -@end - -#include -#include -#include - -@implementation SUPlainInstaller (MMExtendedAttributes) - -+ (int)removeXAttr:(NSString *)name - fromFile:(NSString *)file - options:(int)options -{ - // *** MUST BE SAFE TO CALL ON NON-MAIN THREAD! - - const char *path = NULL; - const char *attr = [name cStringUsingEncoding:NSASCIIStringEncoding]; - @try { - path = [file fileSystemRepresentation]; - } - @catch (id) { - // -[NSString fileSystemRepresentation] throws an exception if it's - // unable to convert the string to something suitable. Map that to - // EDOM, "argument out of domain", which sort of conveys that there - // was a conversion failure. - errno = EDOM; - return -1; - } - - return removexattr(path, attr, options); -} - -+ (void)releaseFromQuarantine:(NSString *)root -{ - // *** MUST BE SAFE TO CALL ON NON-MAIN THREAD! - - NSFileManager *manager = [NSFileManager defaultManager]; -#if __MAC_OS_X_VERSION_MIN_REQUIRED < 101000 - if (!&NSURLQuarantinePropertiesKey) { - NSString *const quarantineAttribute = (__bridge NSString *)kLSItemQuarantineProperties; - const int removeXAttrOptions = XATTR_NOFOLLOW; - - [self removeXAttr:quarantineAttribute - fromFile:root - options:removeXAttrOptions]; - - // Only recurse if it's actually a directory. Don't recurse into a - // root-level symbolic link. - NSDictionary *rootAttributes = [manager attributesOfItemAtPath:root error:nil]; - NSString *rootType = rootAttributes[NSFileType]; - - if (rootType == NSFileTypeDirectory) { - // The NSDirectoryEnumerator will avoid recursing into any contained - // symbolic links, so no further type checks are needed. - NSDirectoryEnumerator *directoryEnumerator = [manager enumeratorAtPath:root]; - NSString *file = nil; - while ((file = [directoryEnumerator nextObject])) { - [self removeXAttr:quarantineAttribute - fromFile:[root stringByAppendingPathComponent:file] - options:removeXAttrOptions]; - } - } - return; - } -#endif - NSURL *rootURL = [NSURL fileURLWithPath:root]; - id rootResourceValue = nil; - [rootURL getResourceValue:&rootResourceValue forKey:NSURLQuarantinePropertiesKey error:NULL]; - if (rootResourceValue) { - [rootURL setResourceValue:[NSNull null] forKey:NSURLQuarantinePropertiesKey error:NULL]; - } - - // Only recurse if it's actually a directory. Don't recurse into a - // root-level symbolic link. - NSDictionary *rootAttributes = [manager attributesOfItemAtPath:root error:nil]; - NSString *rootType = rootAttributes[NSFileType]; - - if (rootType == NSFileTypeDirectory) { - // The NSDirectoryEnumerator will avoid recursing into any contained - // symbolic links, so no further type checks are needed. - NSDirectoryEnumerator *directoryEnumerator = [manager enumeratorAtURL:rootURL includingPropertiesForKeys:nil options:(NSDirectoryEnumerationOptions)0 errorHandler:nil]; - - for (NSURL *file in directoryEnumerator) { - id fileResourceValue = nil; - [file getResourceValue:&fileResourceValue forKey:NSURLQuarantinePropertiesKey error:NULL]; - if (fileResourceValue) { - [file setResourceValue:[NSNull null] forKey:NSURLQuarantinePropertiesKey error:NULL]; - } - } - } -} - -@end From 9c8fb9918432940c8de05b5b33539a3d4c31ee10 Mon Sep 17 00:00:00 2001 From: Zorg Date: Fri, 24 Jul 2015 18:47:50 -0400 Subject: [PATCH 02/12] Move deciding to abort an update into a procedure --- Sparkle/SUBasicUpdateDriver.m | 38 ++++++++++++++++++++++++----------- 1 file changed, 26 insertions(+), 12 deletions(-) diff --git a/Sparkle/SUBasicUpdateDriver.m b/Sparkle/SUBasicUpdateDriver.m index 6371285d70..8f2a6c8ce2 100644 --- a/Sparkle/SUBasicUpdateDriver.m +++ b/Sparkle/SUBasicUpdateDriver.m @@ -377,6 +377,31 @@ - (void)installWithToolAndRelaunch:(BOOL)relaunch [self installWithToolAndRelaunch:relaunch displayingUserInterface:relaunch]; } +// Creates intermediate directories up until targetPath if they don't already exist, +// and removes the directory at targetPath if one already exists there +- (BOOL)preparePathForRelaunchTool:(NSString *)targetPath error:(NSError * __autoreleasing *)error +{ + NSFileManager *fileManager = [[NSFileManager alloc] init]; + if ([fileManager fileExistsAtPath:targetPath]) { + NSError *removeError = nil; + if (![fileManager removeItemAtPath:targetPath error:&removeError]) { + if (error != NULL) { + *error = removeError; + } + return NO; + } + } else { + NSError *createDirectoryError = nil; + if (![fileManager createDirectoryAtPath:[targetPath stringByDeletingLastPathComponent] withIntermediateDirectories:YES attributes:@{} error:&createDirectoryError]) { + if (error != NULL) { + *error = createDirectoryError; + } + return NO; + } + } + return YES; +} + - (void)installWithToolAndRelaunch:(BOOL)relaunch displayingUserInterface:(BOOL)showUI { assert(self.updateItem); @@ -425,23 +450,12 @@ - (void)installWithToolAndRelaunch:(BOOL)relaunch displayingUserInterface:(BOOL) NSString *targetPath = [self.host.appCachePath stringByAppendingPathComponent:[relaunchPathToCopy lastPathComponent]]; NSFileManager *fileManager = [[NSFileManager alloc] init]; - - BOOL shouldAbortUpdate = NO; NSError *error = nil; - if ([fileManager fileExistsAtPath:targetPath]) { - if (![fileManager removeItemAtPath:targetPath error:&error]) { - shouldAbortUpdate = YES; - } - } else { - if (![fileManager createDirectoryAtPath:[targetPath stringByDeletingLastPathComponent] withIntermediateDirectories:YES attributes:@{} error:&error]) { - shouldAbortUpdate = YES; - } - } // We only need to run our copy of the app by spawning a task // Since we are copying the app to a directory that is write-accessible, we don't need to muck with owner/group IDs // And since we spawn a task, we don't need to clear the quarantine bits - if (!shouldAbortUpdate && [fileManager copyItemAtPath:relaunchPathToCopy toPath:targetPath error:&error]) { + if ([self preparePathForRelaunchTool:targetPath error:&error] && [fileManager copyItemAtPath:relaunchPathToCopy toPath:targetPath error:&error]) { self.relaunchPath = targetPath; } else { [self abortUpdateWithError:[NSError errorWithDomain:SUSparkleErrorDomain code:SURelaunchError userInfo:@{ From 19876c5a56ce30df601433a7da0440168720c604 Mon Sep 17 00:00:00 2001 From: Zorg Date: Fri, 24 Jul 2015 23:45:04 -0400 Subject: [PATCH 03/12] -moveItemAtURLToTrash:error: now works on 10.7 --- Sparkle/SUFileManager.m | 41 +++++++++++++++++++++++++++++++++-------- 1 file changed, 33 insertions(+), 8 deletions(-) diff --git a/Sparkle/SUFileManager.m b/Sparkle/SUFileManager.m index 59163b8dbb..ae9a8f3903 100644 --- a/Sparkle/SUFileManager.m +++ b/Sparkle/SUFileManager.m @@ -582,7 +582,6 @@ - (BOOL)removeItemAtURL:(NSURL *)url error:(NSError * __autoreleasing *)error return success; } -// TODO: Fix or address that this method only runs on 10.8 or later - (BOOL)moveItemAtURLToTrash:(NSURL *)url error:(NSError *__autoreleasing *)error { if (![_fileManager fileExistsAtPath:url.path]) { @@ -592,8 +591,22 @@ - (BOOL)moveItemAtURLToTrash:(NSURL *)url error:(NSError *__autoreleasing *)erro return NO; } - // TODO: address NSTrashDirectory being only available in 10.8 or later - NSURL *trashURL = [[_fileManager URLsForDirectory:NSTrashDirectory inDomains:NSUserDomainMask] firstObject]; + NSURL *trashURL = nil; + BOOL canUseNewTrashAPI = YES; +#if __MAC_OS_X_VERSION_MIN_REQUIRED < MAC_OS_X_VERSION_10_8 + canUseNewTrashAPI = [_fileManager respondsToSelector:@selector(trashItemAtURL:resultingItemURL:error:)]; + if (!canUseNewTrashAPI) { + FSRef trashRef; + if (FSFindFolder(kUserDomain, kTrashFolderType, kDontCreateFolder, &trashRef) == noErr) { + trashURL = CFBridgingRelease(CFURLCreateFromFSRef(kCFAllocatorDefault, &trashRef)); + } + } +#endif + + if (canUseNewTrashAPI) { + trashURL = [[_fileManager URLsForDirectory:NSTrashDirectory inDomains:NSUserDomainMask] firstObject]; + } + if (trashURL == nil) { if (error != NULL) { *error = [NSError errorWithDomain:NSCocoaErrorDomain code:NSFileNoSuchFileError userInfo:@{ NSLocalizedDescriptionKey: @"User's Trash directory was not found" }]; @@ -625,11 +638,23 @@ - (BOOL)moveItemAtURLToTrash:(NSURL *)url error:(NSError *__autoreleasing *)erro } // If we get here, we should be able to trash the item normally without authentication - // TODO: address -[NSFileManager trashItemAtURL: resultingItemURL: error:] being 10.8+ only - NSError *trashError = nil; - BOOL success = [_fileManager trashItemAtURL:tempItemURL resultingItemURL:NULL error:&trashError]; - if (!success && error != NULL) { - *error = trashError; + + BOOL success = NO; +#if __MAC_OS_X_VERSION_MIN_REQUIRED < MAC_OS_X_VERSION_10_8 + if (!canUseNewTrashAPI) { + success = [[NSWorkspace sharedWorkspace] performFileOperation:NSWorkspaceRecycleOperation source:tempItemURL.URLByDeletingLastPathComponent.path destination:@"" files:@[tempItemURL.lastPathComponent] tag:NULL]; + if (!success && error != NULL) { + *error = [NSError errorWithDomain:NSCocoaErrorDomain code:NSFileNoSuchFileError userInfo:@{ NSLocalizedDescriptionKey: @"Failed to move file into the trash" }]; + } + } +#endif + + if (canUseNewTrashAPI) { + NSError *trashError = nil; + success = [_fileManager trashItemAtURL:tempItemURL resultingItemURL:NULL error:&trashError]; + if (!success && error != NULL) { + *error = trashError; + } } [self removeItemAtURL:tempDirectory error:NULL]; From d4b7a696af8aa21d9e8ddc1e6dfbf42da32ddf92 Mon Sep 17 00:00:00 2001 From: Zorg Date: Sat, 25 Jul 2015 12:13:16 -0400 Subject: [PATCH 04/12] Append bundle version for older app when moving it --- Sparkle/SUPlainInstaller.m | 49 ++++++++++++++++++++++++++++++-------- 1 file changed, 39 insertions(+), 10 deletions(-) diff --git a/Sparkle/SUPlainInstaller.m b/Sparkle/SUPlainInstaller.m index 4ae02f7537..03b8afd649 100644 --- a/Sparkle/SUPlainInstaller.m +++ b/Sparkle/SUPlainInstaller.m @@ -15,7 +15,23 @@ @implementation SUPlainInstaller -+ (BOOL)performInstallationToURL:(NSURL *)installationURL withOldURL:(NSURL *)oldURL newURL:(NSURL *)newURL error:(NSError * __autoreleasing *)error +// Returns the bundle version from the specified host that is appropriate to use as a filename, or nil if we're unable to retrieve one ++ (NSString *)bundleVersionAppropriateForFilenameFromHost:(SUHost *)host +{ + NSString *bundleVersion = [host objectForInfoDictionaryKey:(NSString *)kCFBundleVersionKey]; + NSString *trimmedVersion = @""; + + if (bundleVersion != nil) { + NSMutableCharacterSet *validCharacters = [NSMutableCharacterSet alphanumericCharacterSet]; + [validCharacters formUnionWithCharacterSet:[NSCharacterSet characterSetWithCharactersInString:@".-()"]]; + + trimmedVersion = [bundleVersion stringByTrimmingCharactersInSet:[validCharacters invertedSet]]; + } + + return trimmedVersion.length > 0 ? trimmedVersion : nil; +} + ++ (BOOL)performInstallationToURL:(NSURL *)installationURL fromUpdateAtURL:(NSURL *)newURL withHost:(SUHost *)host error:(NSError * __autoreleasing *)error { SUFileManager *fileManager = [[SUFileManager alloc] init]; @@ -44,6 +60,7 @@ + (BOOL)performInstallationToURL:(NSURL *)installationURL withOldURL:(NSURL *)ol SULog(@"Failed to release quarantine at %@ with error %@", newTempURL.path, quarantineError); } + NSURL *oldURL = [NSURL fileURLWithPath:host.bundlePath]; if (![fileManager changeOwnerAndGroupOfItemAtRootURL:newTempURL toMatchURL:oldURL error:error]) { // But this is big enough of a deal to fail SULog(@"Failed to change owner and group of new app at %@ to match old app at %@", newTempURL.path, oldURL.path); @@ -51,6 +68,22 @@ + (BOOL)performInstallationToURL:(NSURL *)installationURL withOldURL:(NSURL *)ol return NO; } + // Decide on a destination name we should use for the older app when we move it around the file system + // TODO: will need to merge finding sparkle bundle this with newer code + NSBundle *sparkleBundle = [NSBundle bundleWithIdentifier:SUBundleIdentifier]; + BOOL appendVersion = [[sparkleBundle infoDictionary][SUAppendVersionNumberKey] boolValue]; + + NSString *oldDestinationName = nil; + if (appendVersion) { + NSString *oldBundleVersion = [self bundleVersionAppropriateForFilenameFromHost:host]; + + oldDestinationName = [oldURL.lastPathComponent.stringByDeletingPathExtension stringByAppendingFormat:@" (%@)", oldBundleVersion != nil ? oldBundleVersion : @"old"]; + } else { + oldDestinationName = oldURL.lastPathComponent.stringByDeletingPathExtension; + } + + NSString *oldDestinationNameWithPathExtension = [oldDestinationName stringByAppendingPathExtension:oldURL.pathExtension]; + // If the installation and old URL differ, the sooner we can install our new app // because we won't have to move the old app out of the way first // increasing our chances of having it installed in case something goes wrong @@ -69,12 +102,12 @@ + (BOOL)performInstallationToURL:(NSURL *)installationURL withOldURL:(NSURL *)ol // Create a temporary directory for our old app that resides on its volume NSError *makeOldTempDirectoryError = nil; - NSURL *tempOldDirectoryURL = [fileManager makeTemporaryDirectoryWithPreferredName:oldURL.lastPathComponent.stringByDeletingPathExtension appropriateForDirectoryURL:oldURL.URLByDeletingLastPathComponent error:&makeOldTempDirectoryError]; + NSURL *tempOldDirectoryURL = [fileManager makeTemporaryDirectoryWithPreferredName:oldDestinationName appropriateForDirectoryURL:oldURL.URLByDeletingLastPathComponent error:&makeOldTempDirectoryError]; if (tempOldDirectoryURL == nil) { SULog(@"Failed to create temporary directory for old app at %@ after finishing installation. This is not a fatal error. Error: %@", oldURL.path, makeOldTempDirectoryError); } else { // Move the old app to the temporary directory - NSURL *oldTempURL = [tempOldDirectoryURL URLByAppendingPathComponent:oldURL.lastPathComponent]; + NSURL *oldTempURL = [tempOldDirectoryURL URLByAppendingPathComponent:oldDestinationNameWithPathExtension]; NSError *moveOldAppError = nil; if (![fileManager moveItemAtURL:oldURL toURL:oldTempURL error:&moveOldAppError]) { SULog(@"Failed to move the old app at %@ to a temporary location at %@. This is not a fatal error. Error: %@", oldURL.path, oldTempURL.path, moveOldAppError); @@ -90,7 +123,7 @@ + (BOOL)performInstallationToURL:(NSURL *)installationURL withOldURL:(NSURL *)ol } } else { // Create a temporary directory for our old app that resides on its volume - NSURL *tempOldDirectoryURL = [fileManager makeTemporaryDirectoryWithPreferredName:oldURL.lastPathComponent.stringByDeletingPathExtension appropriateForDirectoryURL:oldURL.URLByDeletingLastPathComponent error:error]; + NSURL *tempOldDirectoryURL = [fileManager makeTemporaryDirectoryWithPreferredName:oldDestinationName appropriateForDirectoryURL:oldURL.URLByDeletingLastPathComponent error:error]; if (tempOldDirectoryURL == nil) { SULog(@"Failed to create temporary directory for old app at %@", oldURL.path); [fileManager removeItemAtURL:tempNewDirectoryURL error:NULL]; @@ -98,7 +131,7 @@ + (BOOL)performInstallationToURL:(NSURL *)installationURL withOldURL:(NSURL *)ol } // Move the old app to the temporary directory - NSURL *oldTempURL = [tempOldDirectoryURL URLByAppendingPathComponent:oldURL.lastPathComponent]; + NSURL *oldTempURL = [tempOldDirectoryURL URLByAppendingPathComponent:oldDestinationNameWithPathExtension]; if (![fileManager moveItemAtURL:oldURL toURL:oldTempURL error:error]) { SULog(@"Failed to move the old app at %@ to a temporary location at %@", oldURL.path, oldTempURL.path); @@ -153,11 +186,7 @@ + (void)performInstallationToPath:(NSString *)installationPath fromPath:(NSStrin dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ NSError *error = nil; - NSURL *oldURL = [NSURL fileURLWithPath:[host bundlePath]]; - NSURL *newURL = [NSURL fileURLWithPath:path]; - NSURL *installationURL = [NSURL fileURLWithPath:installationPath]; - - BOOL result = [self performInstallationToURL:installationURL withOldURL:oldURL newURL:newURL error:&error]; + BOOL result = [self performInstallationToURL:[NSURL fileURLWithPath:installationPath] fromUpdateAtURL:[NSURL fileURLWithPath:path] withHost:host error:&error]; dispatch_async(dispatch_get_main_queue(), ^{ [self finishInstallationToPath:installationPath withResult:result error:error completionHandler:completionHandler]; From dbae37bb5b8d65bc8aca4abf10533bd61e4779ef Mon Sep 17 00:00:00 2001 From: Zorg Date: Sat, 25 Jul 2015 12:57:01 -0400 Subject: [PATCH 05/12] Make errors/directory names more user friendly Since it is possible for the user to see these strings, we should not make them seem as 'gibberish' or unfriendly. More detailed error information is logged to the console. --- Sparkle/SUFileManager.m | 66 +++++++++++++++++++------------------- Sparkle/SUPlainInstaller.m | 2 +- 2 files changed, 34 insertions(+), 34 deletions(-) diff --git a/Sparkle/SUFileManager.m b/Sparkle/SUFileManager.m index ae9a8f3903..ddf507bae1 100644 --- a/Sparkle/SUFileManager.m +++ b/Sparkle/SUFileManager.m @@ -74,7 +74,7 @@ - (BOOL)acquireAuthorizationWithError:(NSError *__autoreleasing *)error OSStatus status = AuthorizationCreate(NULL, kAuthorizationEmptyEnvironment, kAuthorizationFlagDefaults, &_auth); if (status != errAuthorizationSuccess) { if (error != NULL) { - *error = [NSError errorWithDomain:SUSparkleErrorDomain code:SUAuthenticationFailure userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Failed creating authorization reference with status code %d", status] }]; + *error = [NSError errorWithDomain:SUSparkleErrorDomain code:SUAuthenticationFailure userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Failed creating authorization reference with status code %d.", status] }]; } _auth = NULL; return NO; @@ -131,7 +131,7 @@ - (BOOL)removeXAttrWithAuthentication:(NSString *)name fromRootURL:(NSURL *)root { if (![_fileManager fileExistsAtPath:@(XATTR_UTILITY_PATH)]) { if (error != NULL) { - *error = [NSError errorWithDomain:NSCocoaErrorDomain code:NSFileNoSuchFileError userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"%@ does not exist", @(XATTR_UTILITY_PATH)] }]; + *error = [NSError errorWithDomain:NSCocoaErrorDomain code:NSFileNoSuchFileError userInfo:@{ NSLocalizedDescriptionKey: @"xattr utility does not exist on this system." }]; } return NO; } @@ -139,7 +139,7 @@ - (BOOL)removeXAttrWithAuthentication:(NSString *)name fromRootURL:(NSURL *)root char path[PATH_MAX] = {0}; if (![rootURL.path getFileSystemRepresentation:path maxLength:sizeof(path)]) { if (error != NULL) { - *error = [NSError errorWithDomain:NSCocoaErrorDomain code:NSFileReadInvalidFileNameError userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"%@ is not a valid file system representation", rootURL.path] }]; + *error = [NSError errorWithDomain:NSCocoaErrorDomain code:NSFileReadInvalidFileNameError userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"File to remove (%@) cannot be represented as a valid file name.", rootURL.path.lastPathComponent] }]; } return NO; } @@ -147,7 +147,7 @@ - (BOOL)removeXAttrWithAuthentication:(NSString *)name fromRootURL:(NSURL *)root const char *xattrName = [name cStringUsingEncoding:NSASCIIStringEncoding]; if (xattrName == NULL) { if (error != NULL) { - *error = [NSError errorWithDomain:NSCocoaErrorDomain code:NSFileWriteInapplicableStringEncodingError userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"%@ is not a valid ASCII convertible string", name] }]; + *error = [NSError errorWithDomain:NSCocoaErrorDomain code:NSFileWriteInapplicableStringEncodingError userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Extended attribute %@ is not a valid ASCII convertible string.", name] }]; } return NO; } @@ -159,7 +159,7 @@ - (BOOL)removeXAttrWithAuthentication:(NSString *)name fromRootURL:(NSURL *)root BOOL success = AuthorizationExecuteWithPrivilegesAndWait(_auth, XATTR_UTILITY_PATH, kAuthorizationFlagDefaults, (char *[]){ "-s", "-r", "-d", (char *)xattrName, path, NULL }); if (!success && error != NULL) { - NSString *errorMessage = [NSString stringWithFormat:@"Authenticated xattr deletion for attribute %@ failed on %@", name, rootURL.path]; + NSString *errorMessage = [NSString stringWithFormat:@"Authenticated extended attribute deletion for %@ failed on %@.", name, rootURL.path.lastPathComponent]; *error = [NSError errorWithDomain:SUSparkleErrorDomain code:SUAuthenticationFailure userInfo:@{ NSLocalizedDescriptionKey:errorMessage }]; } @@ -254,7 +254,7 @@ - (BOOL)releaseItemUsingOldMethodFromQuarantineAtRootURL:(NSURL *)rootURL error: return [self removeXAttrWithAuthentication:APPLE_QUARANTINE_IDENTIFIER fromRootURL:rootURL error:error]; } else { if (error != NULL) { - *error = [NSError errorWithDomain:NSPOSIXErrorDomain code:errno userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Failed to remove xattr %@ on %@", APPLE_QUARANTINE_IDENTIFIER, root] }]; + *error = [NSError errorWithDomain:NSPOSIXErrorDomain code:errno userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Failed to remove file quarantine on %@.", root.lastPathComponent] }]; } // Fail, but still try to release other items from quarantine success = NO; @@ -281,7 +281,7 @@ - (BOOL)releaseItemUsingOldMethodFromQuarantineAtRootURL:(NSURL *)rootURL error: } else { // Make sure we haven't already run into an error if (success && error != NULL) { - *error = [NSError errorWithDomain:NSPOSIXErrorDomain code:errno userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Failed to remove xattr %@ on %@", APPLE_QUARANTINE_IDENTIFIER, filePath] }]; + *error = [NSError errorWithDomain:NSPOSIXErrorDomain code:errno userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Failed to remove file quarantine on %@.", filePath.lastPathComponent] }]; } // Fail, but still try to release other items from quarantine success = NO; @@ -298,14 +298,14 @@ - (BOOL)moveItemAtURL:(NSURL *)sourceURL toURL:(NSURL *)destinationURL error:(NS { if (![_fileManager fileExistsAtPath:sourceURL.path]) { if (error != NULL) { - *error = [NSError errorWithDomain:NSCocoaErrorDomain code:NSFileNoSuchFileError userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Source %@ does not exist", sourceURL.path] }]; + *error = [NSError errorWithDomain:NSCocoaErrorDomain code:NSFileNoSuchFileError userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Source file to move (%@) does not exist.", sourceURL.path.lastPathComponent] }]; } return NO; } if ([_fileManager fileExistsAtPath:destinationURL.path]) { if (error != NULL) { - *error = [NSError errorWithDomain:NSCocoaErrorDomain code:NSFileWriteFileExistsError userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Destination %@ already exists", destinationURL.path] }]; + *error = [NSError errorWithDomain:NSCocoaErrorDomain code:NSFileWriteFileExistsError userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Destination file to move (%@) already exists.", destinationURL.path.lastPathComponent] }]; } return NO; } @@ -329,7 +329,7 @@ - (BOOL)moveItemAtURL:(NSURL *)sourceURL toURL:(NSURL *)destinationURL error:(NS char sourcePath[PATH_MAX] = {0}; if (![sourceURL.path getFileSystemRepresentation:sourcePath maxLength:sizeof(sourcePath)]) { if (error != NULL) { - *error = [NSError errorWithDomain:NSCocoaErrorDomain code:NSFileReadInvalidFileNameError userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"%@ is not a valid file system representation", sourceURL.path] }]; + *error = [NSError errorWithDomain:NSCocoaErrorDomain code:NSFileReadInvalidFileNameError userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"File to move (%@) cannot be represented as a valid file name.", sourceURL.path.lastPathComponent] }]; } return NO; } @@ -337,14 +337,14 @@ - (BOOL)moveItemAtURL:(NSURL *)sourceURL toURL:(NSURL *)destinationURL error:(NS char destinationPath[PATH_MAX] = {0}; if (![destinationURL.path getFileSystemRepresentation:destinationPath maxLength:sizeof(destinationPath)]) { if (error != NULL) { - *error = [NSError errorWithDomain:NSCocoaErrorDomain code:NSFileReadInvalidFileNameError userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"%@ is not a valid file system representation", destinationURL.path] }]; + *error = [NSError errorWithDomain:NSCocoaErrorDomain code:NSFileReadInvalidFileNameError userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Destination (%@) cannot be represented as a valid file name.", destinationURL.path.lastPathComponent] }]; } return NO; } if (!AuthorizationExecuteWithPrivilegesAndWait(_auth, "/bin/mv", kAuthorizationFlagDefaults, (char *[]){ "-f", sourcePath, destinationPath, NULL })) { if (error != NULL) { - NSString *errorMessage = [NSString stringWithFormat:@"Authenticated file move from %@ to %@ failed.", sourceURL, destinationURL]; + NSString *errorMessage = [NSString stringWithFormat:@"Failed to perform authenticated file move for %@.", sourceURL.lastPathComponent]; *error = [NSError errorWithDomain:SUSparkleErrorDomain code:SUAuthenticationFailure userInfo:@{ NSLocalizedDescriptionKey:errorMessage }]; } return NO; @@ -357,14 +357,14 @@ - (BOOL)changeOwnerAndGroupOfItemAtRootURL:(NSURL *)targetURL toMatchURL:(NSURL { if (![_fileManager fileExistsAtPath:targetURL.path]) { if (error != NULL) { - *error = [NSError errorWithDomain:NSCocoaErrorDomain code:NSFileNoSuchFileError userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Item at target %@ does not exist", targetURL.path] }]; + *error = [NSError errorWithDomain:NSCocoaErrorDomain code:NSFileNoSuchFileError userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Failed to change owner & group IDs because %@ does not exist.", targetURL.path.lastPathComponent] }]; } return NO; } if (![_fileManager fileExistsAtPath:matchURL.path]) { if (error != NULL) { - *error = [NSError errorWithDomain:NSCocoaErrorDomain code:NSFileNoSuchFileError userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Item at URL to match %@ does not exist", matchURL.path] }]; + *error = [NSError errorWithDomain:NSCocoaErrorDomain code:NSFileNoSuchFileError userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Failed to match owner & group IDs because %@ does not exist.", matchURL.path.lastPathComponent] }]; } return NO; } @@ -391,7 +391,7 @@ - (BOOL)changeOwnerAndGroupOfItemAtRootURL:(NSURL *)targetURL toMatchURL:(NSURL if (ownerID == nil) { // shouldn't be possible to error here, but just in case if (error != NULL) { - *error = [NSError errorWithDomain:NSCocoaErrorDomain code:NSFileReadNoPermissionError userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"owner ID could not be read from %@", matchURL.path] }]; + *error = [NSError errorWithDomain:NSCocoaErrorDomain code:NSFileReadNoPermissionError userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Owner ID could not be read from %@.", matchURL.path.lastPathComponent] }]; } return NO; } @@ -400,7 +400,7 @@ - (BOOL)changeOwnerAndGroupOfItemAtRootURL:(NSURL *)targetURL toMatchURL:(NSURL if (groupID == nil) { // shouldn't be possible to error here, but just in case if (error != NULL) { - *error = [NSError errorWithDomain:NSCocoaErrorDomain code:NSFileReadNoPermissionError userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"group ID could not be read from %@", matchURL.path] }]; + *error = [NSError errorWithDomain:NSCocoaErrorDomain code:NSFileReadNoPermissionError userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Group ID could not be read from %@.", matchURL.path.lastPathComponent] }]; } return NO; } @@ -417,7 +417,7 @@ - (BOOL)changeOwnerAndGroupOfItemAtRootURL:(NSURL *)targetURL toMatchURL:(NSURL char path[PATH_MAX] = {0}; if (![url.path getFileSystemRepresentation:path maxLength:sizeof(path)]) { if (error != NULL) { - *error = [NSError errorWithDomain:NSCocoaErrorDomain code:NSFileReadInvalidFileNameError userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"%@ is not a valid file system representation", url.path] }]; + *error = [NSError errorWithDomain:NSCocoaErrorDomain code:NSFileReadInvalidFileNameError userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"File to change owner & group (%@) cannot be represented as a valid file name.", url.path.lastPathComponent] }]; } return NO; } @@ -428,7 +428,7 @@ - (BOOL)changeOwnerAndGroupOfItemAtRootURL:(NSURL *)targetURL toMatchURL:(NSURL break; } else { if (error != NULL) { - *error = [NSError errorWithDomain:NSPOSIXErrorDomain code:errno userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Failed to chown %@ with owner ID %u and group ID %u", url.path, ownerID.unsignedIntValue, groupID.unsignedIntValue] }]; + *error = [NSError errorWithDomain:NSPOSIXErrorDomain code:errno userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Failed to change owner & group for %@ with owner ID %u and group ID %u.", url.path.lastPathComponent, ownerID.unsignedIntValue, groupID.unsignedIntValue] }]; } return NO; } @@ -442,7 +442,7 @@ - (BOOL)changeOwnerAndGroupOfItemAtRootURL:(NSURL *)targetURL toMatchURL:(NSURL char targetPath[PATH_MAX] = {0}; if (![targetURL.path getFileSystemRepresentation:targetPath maxLength:sizeof(targetPath)]) { if (error != NULL) { - *error = [NSError errorWithDomain:NSCocoaErrorDomain code:NSFileReadInvalidFileNameError userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"%@ is not a valid file system representation", targetURL.path] }]; + *error = [NSError errorWithDomain:NSCocoaErrorDomain code:NSFileReadInvalidFileNameError userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Target file (%@) cannot be represented as a valid file name.", targetURL.path.lastPathComponent] }]; } return NO; } @@ -451,7 +451,7 @@ - (BOOL)changeOwnerAndGroupOfItemAtRootURL:(NSURL *)targetURL toMatchURL:(NSURL const char *userAndGroup = [formattedUserAndGroupIDs cStringUsingEncoding:NSASCIIStringEncoding]; if (userAndGroup == NULL) { if (error != NULL) { - *error = [NSError errorWithDomain:NSCocoaErrorDomain code:NSFormattingError userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Owner ID %u and Group ID %u could not be formatted", ownerID.unsignedIntValue, groupID.unsignedIntValue] }]; + *error = [NSError errorWithDomain:NSCocoaErrorDomain code:NSFormattingError userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Owner ID %u and Group ID %u could not be formatted.", ownerID.unsignedIntValue, groupID.unsignedIntValue] }]; } return NO; } @@ -462,7 +462,7 @@ - (BOOL)changeOwnerAndGroupOfItemAtRootURL:(NSURL *)targetURL toMatchURL:(NSURL BOOL success = AuthorizationExecuteWithPrivilegesAndWait(_auth, "/usr/sbin/chown", kAuthorizationFlagDefaults, (char *[]){ "-R", (char *)userAndGroup, targetPath, NULL }); if (!success && error != NULL) { - NSString *errorMessage = [NSString stringWithFormat:@"Failed to chown -R \"%@\" \"%@\" with authentication", formattedUserAndGroupIDs, targetURL.path]; + NSString *errorMessage = [NSString stringWithFormat:@"Failed to change owner:group %@ on %@ with authentication.", formattedUserAndGroupIDs, targetURL.path.lastPathComponent]; *error = [NSError errorWithDomain:SUSparkleErrorDomain code:SUAuthenticationFailure userInfo:@{ NSLocalizedDescriptionKey: errorMessage }]; } @@ -475,7 +475,7 @@ - (BOOL)makeDirectoryAtURL:(NSURL *)url error:(NSError * __autoreleasing *)error { if ([_fileManager fileExistsAtPath:url.path]) { if (error != NULL) { - *error = [NSError errorWithDomain:NSCocoaErrorDomain code:NSFileWriteFileExistsError userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Item at %@ already exists", url.path] }]; + *error = [NSError errorWithDomain:NSCocoaErrorDomain code:NSFileWriteFileExistsError userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Failed to create directory because file %@ already exists.", url.path.lastPathComponent] }]; } return NO; } @@ -484,7 +484,7 @@ - (BOOL)makeDirectoryAtURL:(NSURL *)url error:(NSError * __autoreleasing *)error BOOL isParentADirectory = NO; if (![_fileManager fileExistsAtPath:parentURL.path isDirectory:&isParentADirectory] || !isParentADirectory) { if (error != NULL) { - *error = [NSError errorWithDomain:NSCocoaErrorDomain code:NSFileNoSuchFileError userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Directory at %@ does not exist", parentURL.path] }]; + *error = [NSError errorWithDomain:NSCocoaErrorDomain code:NSFileNoSuchFileError userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Failed to create directory because parent directory %@ does not exist.", parentURL.path.lastPathComponent] }]; } return NO; } @@ -504,7 +504,7 @@ - (BOOL)makeDirectoryAtURL:(NSURL *)url error:(NSError * __autoreleasing *)error char path[PATH_MAX] = {0}; if (![url.path getFileSystemRepresentation:path maxLength:sizeof(path)]) { if (error != NULL) { - *error = [NSError errorWithDomain:NSCocoaErrorDomain code:NSFileReadInvalidFileNameError userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"%@ is not a valid file system representation", url.path] }]; + *error = [NSError errorWithDomain:NSCocoaErrorDomain code:NSFileReadInvalidFileNameError userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Directory to create (%@) cannot be represented as a valid file name.", url.path.lastPathComponent] }]; } return NO; } @@ -515,7 +515,7 @@ - (BOOL)makeDirectoryAtURL:(NSURL *)url error:(NSError * __autoreleasing *)error BOOL success = AuthorizationExecuteWithPrivilegesAndWait(_auth, "/bin/mkdir", kAuthorizationFlagDefaults, (char *[]){ path, NULL }); if (!success && error != NULL) { - NSString *errorMessage = [NSString stringWithFormat:@"Failed to make directory %@ with authentication", url.path]; + NSString *errorMessage = [NSString stringWithFormat:@"Failed to make directory %@ with authentication.", url.path.lastPathComponent]; *error = [NSError errorWithDomain:SUSparkleErrorDomain code:SUAuthenticationFailure userInfo:@{ NSLocalizedDescriptionKey: errorMessage }]; } return success; @@ -546,7 +546,7 @@ - (BOOL)removeItemAtURL:(NSURL *)url error:(NSError * __autoreleasing *)error { if (![_fileManager fileExistsAtPath:url.path]) { if (error != NULL) { - *error = [NSError errorWithDomain:NSCocoaErrorDomain code:NSFileNoSuchFileError userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Item at %@ does not exist", url.path] }]; + *error = [NSError errorWithDomain:NSCocoaErrorDomain code:NSFileNoSuchFileError userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Failed to remove file %@ because it does not exist.", url.path.lastPathComponent] }]; } return NO; } @@ -570,14 +570,14 @@ - (BOOL)removeItemAtURL:(NSURL *)url error:(NSError * __autoreleasing *)error char path[PATH_MAX] = {0}; if (![url.path getFileSystemRepresentation:path maxLength:sizeof(path)]) { if (error != NULL) { - *error = [NSError errorWithDomain:NSCocoaErrorDomain code:NSFileReadInvalidFileNameError userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"%@ is not a valid file system representation", url.path] }]; + *error = [NSError errorWithDomain:NSCocoaErrorDomain code:NSFileReadInvalidFileNameError userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"File to remove (%@) cannot be represented as a valid file name.", url.path.lastPathComponent] }]; } return NO; } BOOL success = AuthorizationExecuteWithPrivilegesAndWait(_auth, "/bin/rm", kAuthorizationFlagDefaults, (char *[]){ "-rf", path, NULL }); if (!success && error != NULL) { - *error = [NSError errorWithDomain:SUSparkleErrorDomain code:SUAuthenticationFailure userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Failed to rm -rf \"%@\" with authentication", url.path] }]; + *error = [NSError errorWithDomain:SUSparkleErrorDomain code:SUAuthenticationFailure userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Failed to remove %@ with authentication.", url.path.lastPathComponent] }]; } return success; } @@ -586,7 +586,7 @@ - (BOOL)moveItemAtURLToTrash:(NSURL *)url error:(NSError *__autoreleasing *)erro { if (![_fileManager fileExistsAtPath:url.path]) { if (error != NULL) { - *error = [NSError errorWithDomain:NSCocoaErrorDomain code:NSFileNoSuchFileError userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Item at %@ does not exist", url.path] }]; + *error = [NSError errorWithDomain:NSCocoaErrorDomain code:NSFileNoSuchFileError userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Failed to move %@ to the trash because the file does not exist.", url.path.lastPathComponent] }]; } return NO; } @@ -609,15 +609,15 @@ - (BOOL)moveItemAtURLToTrash:(NSURL *)url error:(NSError *__autoreleasing *)erro if (trashURL == nil) { if (error != NULL) { - *error = [NSError errorWithDomain:NSCocoaErrorDomain code:NSFileNoSuchFileError userInfo:@{ NSLocalizedDescriptionKey: @"User's Trash directory was not found" }]; + *error = [NSError errorWithDomain:NSCocoaErrorDomain code:NSFileNoSuchFileError userInfo:@{ NSLocalizedDescriptionKey: @"Failed to locate the user's trash folder." }]; } return NO; } - // In the rare worst case scenario, our temporary directory will be labeled with "Incomplete" and be in the user's trash directory, + // In the rare worst case scenario, our temporary directory will be labeled incomplete and be in the user's trash directory, // indicating that whatever inside of there is not yet completely moved. // Regardless, we want the item to be in our Volume before we try moving it to the trash - NSString *preferredName = [url.lastPathComponent.stringByDeletingPathExtension stringByAppendingString:@" (Incomplete)"]; + NSString *preferredName = [url.lastPathComponent.stringByDeletingPathExtension stringByAppendingString:@" (Incomplete Files)"]; NSURL *tempDirectory = [self makeTemporaryDirectoryWithPreferredName:preferredName appropriateForDirectoryURL:trashURL error:error]; if (tempDirectory == nil) { return NO; @@ -644,7 +644,7 @@ - (BOOL)moveItemAtURLToTrash:(NSURL *)url error:(NSError *__autoreleasing *)erro if (!canUseNewTrashAPI) { success = [[NSWorkspace sharedWorkspace] performFileOperation:NSWorkspaceRecycleOperation source:tempItemURL.URLByDeletingLastPathComponent.path destination:@"" files:@[tempItemURL.lastPathComponent] tag:NULL]; if (!success && error != NULL) { - *error = [NSError errorWithDomain:NSCocoaErrorDomain code:NSFileNoSuchFileError userInfo:@{ NSLocalizedDescriptionKey: @"Failed to move file into the trash" }]; + *error = [NSError errorWithDomain:NSCocoaErrorDomain code:NSFileNoSuchFileError userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Failed to move file %@ into the trash.", tempItemURL.lastPathComponent] }]; } } #endif diff --git a/Sparkle/SUPlainInstaller.m b/Sparkle/SUPlainInstaller.m index 03b8afd649..da02f67476 100644 --- a/Sparkle/SUPlainInstaller.m +++ b/Sparkle/SUPlainInstaller.m @@ -36,7 +36,7 @@ + (BOOL)performInstallationToURL:(NSURL *)installationURL fromUpdateAtURL:(NSURL SUFileManager *fileManager = [[SUFileManager alloc] init]; // Create a temporary directory for our new app that resides on our destination's volume - NSURL *tempNewDirectoryURL = [fileManager makeTemporaryDirectoryWithPreferredName:[installationURL.lastPathComponent.stringByDeletingPathExtension stringByAppendingString:@" (Incomplete)"] appropriateForDirectoryURL:installationURL.URLByDeletingLastPathComponent error:error]; + NSURL *tempNewDirectoryURL = [fileManager makeTemporaryDirectoryWithPreferredName:[installationURL.lastPathComponent.stringByDeletingPathExtension stringByAppendingString:@" (Incomplete Update)"] appropriateForDirectoryURL:installationURL.URLByDeletingLastPathComponent error:error]; if (tempNewDirectoryURL == nil) { SULog(@"Failed to make new temp directory"); return NO; From fd7628e658ef3f7045fdae88cd702bf2b8a296cb Mon Sep 17 00:00:00 2001 From: Zorg Date: Sat, 15 Aug 2015 17:39:06 -0400 Subject: [PATCH 06/12] Don't follow symlinks for checking file existence --- Sparkle/SUFileManager.m | 40 +++++++++++++++++++++++++++++++--------- 1 file changed, 31 insertions(+), 9 deletions(-) diff --git a/Sparkle/SUFileManager.m b/Sparkle/SUFileManager.m index ddf507bae1..c2f7d27042 100644 --- a/Sparkle/SUFileManager.m +++ b/Sparkle/SUFileManager.m @@ -89,6 +89,27 @@ - (void)dealloc } } +// -[NSFileManager attributesOfItemAtPath:error:] won't follow symbolic links + +- (BOOL)itemExistsAtURL:(NSURL *)fileURL +{ + return [_fileManager attributesOfItemAtPath:fileURL.path error:NULL] != nil; +} + +- (BOOL)itemExistsAtURL:(NSURL *)fileURL isDirectory:(BOOL *)isDirectory +{ + NSDictionary *attributes = [_fileManager attributesOfItemAtPath:fileURL.path error:NULL]; + if (attributes == nil) { + return NO; + } + + if (isDirectory != NULL) { + *isDirectory = [attributes[NSFileType] isEqualToString:NSFileTypeDirectory]; + } + + return YES; +} + // Wrapper around getxattr() - (ssize_t)getXAttr:(NSString *)nameString fromFile:(NSString *)file options:(int)options { @@ -129,6 +150,7 @@ - (int)removeXAttr:(NSString *)name fromFile:(NSString *)file options:(int)optio // Recursively remove an xattr at a specified root URL with authentication - (BOOL)removeXAttrWithAuthentication:(NSString *)name fromRootURL:(NSURL *)rootURL error:(NSError *__autoreleasing *)error { + // Because this is a system utility, it's fine to follow the symbolic link if one exists if (![_fileManager fileExistsAtPath:@(XATTR_UTILITY_PATH)]) { if (error != NULL) { *error = [NSError errorWithDomain:NSCocoaErrorDomain code:NSFileNoSuchFileError userInfo:@{ NSLocalizedDescriptionKey: @"xattr utility does not exist on this system." }]; @@ -296,14 +318,14 @@ - (BOOL)releaseItemUsingOldMethodFromQuarantineAtRootURL:(NSURL *)rootURL error: - (BOOL)moveItemAtURL:(NSURL *)sourceURL toURL:(NSURL *)destinationURL error:(NSError *__autoreleasing *)error { - if (![_fileManager fileExistsAtPath:sourceURL.path]) { + if (![self itemExistsAtURL:sourceURL]) { if (error != NULL) { *error = [NSError errorWithDomain:NSCocoaErrorDomain code:NSFileNoSuchFileError userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Source file to move (%@) does not exist.", sourceURL.path.lastPathComponent] }]; } return NO; } - if ([_fileManager fileExistsAtPath:destinationURL.path]) { + if ([self itemExistsAtURL:destinationURL]) { if (error != NULL) { *error = [NSError errorWithDomain:NSCocoaErrorDomain code:NSFileWriteFileExistsError userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Destination file to move (%@) already exists.", destinationURL.path.lastPathComponent] }]; } @@ -355,14 +377,14 @@ - (BOOL)moveItemAtURL:(NSURL *)sourceURL toURL:(NSURL *)destinationURL error:(NS - (BOOL)changeOwnerAndGroupOfItemAtRootURL:(NSURL *)targetURL toMatchURL:(NSURL *)matchURL error:(NSError * __autoreleasing *)error { - if (![_fileManager fileExistsAtPath:targetURL.path]) { + if (![self itemExistsAtURL:targetURL]) { if (error != NULL) { *error = [NSError errorWithDomain:NSCocoaErrorDomain code:NSFileNoSuchFileError userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Failed to change owner & group IDs because %@ does not exist.", targetURL.path.lastPathComponent] }]; } return NO; } - if (![_fileManager fileExistsAtPath:matchURL.path]) { + if (![self itemExistsAtURL:matchURL]) { if (error != NULL) { *error = [NSError errorWithDomain:NSCocoaErrorDomain code:NSFileNoSuchFileError userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Failed to match owner & group IDs because %@ does not exist.", matchURL.path.lastPathComponent] }]; } @@ -473,7 +495,7 @@ - (BOOL)changeOwnerAndGroupOfItemAtRootURL:(NSURL *)targetURL toMatchURL:(NSURL // An item cannot already exist at the url, but the parent must be a directory that exists - (BOOL)makeDirectoryAtURL:(NSURL *)url error:(NSError * __autoreleasing *)error { - if ([_fileManager fileExistsAtPath:url.path]) { + if ([self itemExistsAtURL:url]) { if (error != NULL) { *error = [NSError errorWithDomain:NSCocoaErrorDomain code:NSFileWriteFileExistsError userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Failed to create directory because file %@ already exists.", url.path.lastPathComponent] }]; } @@ -482,7 +504,7 @@ - (BOOL)makeDirectoryAtURL:(NSURL *)url error:(NSError * __autoreleasing *)error NSURL *parentURL = [url URLByDeletingLastPathComponent]; BOOL isParentADirectory = NO; - if (![_fileManager fileExistsAtPath:parentURL.path isDirectory:&isParentADirectory] || !isParentADirectory) { + if (![self itemExistsAtURL:parentURL isDirectory:&isParentADirectory] || !isParentADirectory) { if (error != NULL) { *error = [NSError errorWithDomain:NSCocoaErrorDomain code:NSFileNoSuchFileError userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Failed to create directory because parent directory %@ does not exist.", parentURL.path.lastPathComponent] }]; } @@ -535,7 +557,7 @@ - (NSURL *)makeTemporaryDirectoryWithPreferredName:(NSString *)preferredName app NSURL *desiredURL = [directoryURL URLByAppendingPathComponent:preferredName]; NSUInteger tagIndex = 1; - while ([_fileManager fileExistsAtPath:desiredURL.path] && tagIndex <= 9999) { + while ([self itemExistsAtURL:desiredURL] && tagIndex <= 9999) { desiredURL = [directoryURL URLByAppendingPathComponent:[preferredName stringByAppendingFormat:@" (%lu)", (unsigned long)++tagIndex]]; } @@ -544,7 +566,7 @@ - (NSURL *)makeTemporaryDirectoryWithPreferredName:(NSString *)preferredName app - (BOOL)removeItemAtURL:(NSURL *)url error:(NSError * __autoreleasing *)error { - if (![_fileManager fileExistsAtPath:url.path]) { + if (![self itemExistsAtURL:url]) { if (error != NULL) { *error = [NSError errorWithDomain:NSCocoaErrorDomain code:NSFileNoSuchFileError userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Failed to remove file %@ because it does not exist.", url.path.lastPathComponent] }]; } @@ -584,7 +606,7 @@ - (BOOL)removeItemAtURL:(NSURL *)url error:(NSError * __autoreleasing *)error - (BOOL)moveItemAtURLToTrash:(NSURL *)url error:(NSError *__autoreleasing *)error { - if (![_fileManager fileExistsAtPath:url.path]) { + if (![self itemExistsAtURL:url]) { if (error != NULL) { *error = [NSError errorWithDomain:NSCocoaErrorDomain code:NSFileNoSuchFileError userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Failed to move %@ to the trash because the file does not exist.", url.path.lastPathComponent] }]; } From ea959cbd873d0b4e906e8f2c377f82b1794b9946 Mon Sep 17 00:00:00 2001 From: Zorg Date: Sat, 15 Aug 2015 18:05:20 -0400 Subject: [PATCH 07/12] Release quarantine for Autoupdate.app I don't think this is actually needed, but better be safe than sorry. --- Sparkle/SUBasicUpdateDriver.m | 8 +++++++- Sparkle/SUFileManager.h | 5 +++++ Sparkle/SUFileManager.m | 22 ++++++++++++++++------ 3 files changed, 28 insertions(+), 7 deletions(-) diff --git a/Sparkle/SUBasicUpdateDriver.m b/Sparkle/SUBasicUpdateDriver.m index 85d5dd91b9..4e19024f67 100644 --- a/Sparkle/SUBasicUpdateDriver.m +++ b/Sparkle/SUBasicUpdateDriver.m @@ -18,6 +18,7 @@ #import "SUBinaryDeltaCommon.h" #import "SUCodeSigningVerifier.h" #import "SUUpdater_Private.h" +#import "SUFileManager.h" @interface SUBasicUpdateDriver () @@ -456,8 +457,13 @@ - (void)installWithToolAndRelaunch:(BOOL)relaunch displayingUserInterface:(BOOL) // We only need to run our copy of the app by spawning a task // Since we are copying the app to a directory that is write-accessible, we don't need to muck with owner/group IDs - // And since we spawn a task, we don't need to clear the quarantine bits if ([self preparePathForRelaunchTool:targetPath error:&error] && [fileManager copyItemAtPath:relaunchPathToCopy toPath:targetPath error:&error]) { + // We probably don't need to release the quarantine, but we'll do it just in case it's necessary. + // Perhaps in a sandboxed environment this matters more. Note that this may not be a fatal error. + NSError *quarantineError = nil; + if (![[[SUFileManager alloc] init] releaseItemFromQuarantineWithoutAuthenticationAtRootURL:[NSURL fileURLWithPath:targetPath] error:&quarantineError]) { + SULog(@"Failed to release quarantine on %@ with error %@", targetPath, quarantineError); + } self.relaunchPath = targetPath; } else { [self abortUpdateWithError:[NSError errorWithDomain:SUSparkleErrorDomain code:SURelaunchError userInfo:@{ diff --git a/Sparkle/SUFileManager.h b/Sparkle/SUFileManager.h index 5f07bff036..f5cba54aec 100644 --- a/Sparkle/SUFileManager.h +++ b/Sparkle/SUFileManager.h @@ -97,4 +97,9 @@ */ - (BOOL)releaseItemFromQuarantineAtRootURL:(NSURL *)rootURL error:(NSError **)error; +/** + * Behaves as -releaseItemFromQuarantineAtRootURL:error: does, except without the capability of authenticating when permission is denied + */ +- (BOOL)releaseItemFromQuarantineWithoutAuthenticationAtRootURL:(NSURL *)rootURL error:(NSError **)error; + @end diff --git a/Sparkle/SUFileManager.m b/Sparkle/SUFileManager.m index c2f7d27042..5c43289b79 100644 --- a/Sparkle/SUFileManager.m +++ b/Sparkle/SUFileManager.m @@ -198,11 +198,11 @@ - (BOOL)removeXAttrWithAuthentication:(NSString *)name fromRootURL:(NSURL *)root // If |root| is not a directory, then it alone is removed from the quarantine. // Symbolic links, including |root| if it is a symbolic link, will not be // traversed. -- (BOOL)releaseItemFromQuarantineAtRootURL:(NSURL *)rootURL error:(NSError * __autoreleasing *)error +- (BOOL)releaseItemFromQuarantineAtRootURL:(NSURL *)rootURL allowingAuthentication:(BOOL)allowsAuthentication error:(NSError * __autoreleasing *)error { #if __MAC_OS_X_VERSION_MIN_REQUIRED < 101000 /* MAC_OS_X_VERSION_10_10 */ if (!&NSURLQuarantinePropertiesKey) { - return [self releaseItemUsingOldMethodFromQuarantineAtRootURL:rootURL error:error]; + return [self releaseItemUsingOldMethodFromQuarantineAtRootURL:rootURL allowingAuthentication:allowsAuthentication error:error]; } #endif @@ -211,7 +211,7 @@ - (BOOL)releaseItemFromQuarantineAtRootURL:(NSURL *)rootURL error:(NSError * __a if ([rootURL getResourceValue:&rootResourceValue forKey:NSURLQuarantinePropertiesKey error:NULL] && rootResourceValue != nil) { NSError *setResourceError = nil; if (![rootURL setResourceValue:[NSNull null] forKey:NSURLQuarantinePropertiesKey error:&setResourceError]) { - if (NS_HAS_PERMISSION_ERROR(setResourceError)) { + if (allowsAuthentication && NS_HAS_PERMISSION_ERROR(setResourceError)) { return [self removeXAttrWithAuthentication:APPLE_QUARANTINE_IDENTIFIER fromRootURL:rootURL error:error]; } else { if (error != NULL) { @@ -238,7 +238,7 @@ - (BOOL)releaseItemFromQuarantineAtRootURL:(NSURL *)rootURL error:(NSError * __a if ([file getResourceValue:&fileResourceValue forKey:NSURLQuarantinePropertiesKey error:NULL] && fileResourceValue != nil) { NSError *setResourceError = nil; if (![file setResourceValue:[NSNull null] forKey:NSURLQuarantinePropertiesKey error:&setResourceError]) { - if (NS_HAS_PERMISSION_ERROR(setResourceError)) { + if (allowsAuthentication && NS_HAS_PERMISSION_ERROR(setResourceError)) { return [self removeXAttrWithAuthentication:APPLE_QUARANTINE_IDENTIFIER fromRootURL:rootURL error:error]; } else { // Make sure we haven't already run into an error @@ -264,7 +264,7 @@ - (BOOL)releaseItemFromQuarantineAtRootURL:(NSURL *)rootURL error:(NSError * __a // that the quarantine is implemented in part by setting an extended attribute, // "com.apple.quarantine", on affected files. Removing this attribute is // sufficient to remove files from the quarantine. -- (BOOL)releaseItemUsingOldMethodFromQuarantineAtRootURL:(NSURL *)rootURL error:(NSError *__autoreleasing *)error +- (BOOL)releaseItemUsingOldMethodFromQuarantineAtRootURL:(NSURL *)rootURL allowingAuthentication:(BOOL)allowsAuthentication error:(NSError *__autoreleasing *)error { BOOL success = YES; NSString *root = rootURL.path; @@ -298,7 +298,7 @@ - (BOOL)releaseItemUsingOldMethodFromQuarantineAtRootURL:(NSURL *)rootURL error: NSString *filePath = [root stringByAppendingPathComponent:file]; if ([self getXAttr:APPLE_QUARANTINE_IDENTIFIER fromFile:filePath options:removeXAttrOptions] >= 0) { if ([self removeXAttr:APPLE_QUARANTINE_IDENTIFIER fromFile:filePath options:removeXAttrOptions] != 0) { - if (errno == EACCES) { + if (allowsAuthentication && errno == EACCES) { return [self removeXAttrWithAuthentication:APPLE_QUARANTINE_IDENTIFIER fromRootURL:rootURL error:error]; } else { // Make sure we haven't already run into an error @@ -316,6 +316,16 @@ - (BOOL)releaseItemUsingOldMethodFromQuarantineAtRootURL:(NSURL *)rootURL error: return success; } +- (BOOL)releaseItemFromQuarantineAtRootURL:(NSURL *)rootURL error:(NSError * __autoreleasing *)error +{ + return [self releaseItemFromQuarantineAtRootURL:rootURL allowingAuthentication:YES error:error]; +} + +- (BOOL)releaseItemFromQuarantineWithoutAuthenticationAtRootURL:(NSURL *)rootURL error:(NSError * __autoreleasing *)error +{ + return [self releaseItemFromQuarantineAtRootURL:rootURL allowingAuthentication:NO error:error]; +} + - (BOOL)moveItemAtURL:(NSURL *)sourceURL toURL:(NSURL *)destinationURL error:(NSError *__autoreleasing *)error { if (![self itemExistsAtURL:sourceURL]) { From 758ed1dc3885458314be18c5a75a09d98cf60e86 Mon Sep 17 00:00:00 2001 From: Zorg Date: Sat, 15 Aug 2015 22:56:47 -0400 Subject: [PATCH 08/12] Remove installation URL != old URL case By not taking account the rare case, we simplify the code by having less duplication. The tradeoff is that the rare case will be slightly less efficient. --- Sparkle/SUPlainInstaller.m | 109 ++++++++++++------------------------- 1 file changed, 35 insertions(+), 74 deletions(-) diff --git a/Sparkle/SUPlainInstaller.m b/Sparkle/SUPlainInstaller.m index 1fa1a8ddd8..b70be52080 100644 --- a/Sparkle/SUPlainInstaller.m +++ b/Sparkle/SUPlainInstaller.m @@ -80,87 +80,48 @@ + (BOOL)performInstallationToURL:(NSURL *)installationURL fromUpdateAtURL:(NSURL NSString *oldDestinationNameWithPathExtension = [oldDestinationName stringByAppendingPathExtension:oldURL.pathExtension]; - // If the installation and old URL differ, the sooner we can install our new app - // because we won't have to move the old app out of the way first - // increasing our chances of having it installed in case something goes wrong - if (![installationURL isEqual:oldURL]) { - // Move the new app to its final destination - if (![fileManager moveItemAtURL:newTempURL toURL:installationURL error:error]) { - SULog(@"Failed to move new app at %@ to final destination %@ (with installationURL != oldURL)", newTempURL.path, installationURL.path); - [fileManager removeItemAtURL:tempNewDirectoryURL error:NULL]; - return NO; - } - - // Cleanup: move the old app to the trash - // We will first have to move the app to a temporary location that the user may not care about - // This is necessary because the operation could fail mid-way through - // Nothing past here will be a fatal error because we already installed the new app - - // Create a temporary directory for our old app that resides on its volume - NSError *makeOldTempDirectoryError = nil; - NSURL *tempOldDirectoryURL = [fileManager makeTemporaryDirectoryWithPreferredName:oldDestinationName appropriateForDirectoryURL:oldURL.URLByDeletingLastPathComponent error:&makeOldTempDirectoryError]; - if (tempOldDirectoryURL == nil) { - SULog(@"Failed to create temporary directory for old app at %@ after finishing installation. This is not a fatal error. Error: %@", oldURL.path, makeOldTempDirectoryError); - } else { - // Move the old app to the temporary directory - NSURL *oldTempURL = [tempOldDirectoryURL URLByAppendingPathComponent:oldDestinationNameWithPathExtension]; - NSError *moveOldAppError = nil; - if (![fileManager moveItemAtURL:oldURL toURL:oldTempURL error:&moveOldAppError]) { - SULog(@"Failed to move the old app at %@ to a temporary location at %@. This is not a fatal error. Error: %@", oldURL.path, oldTempURL.path, moveOldAppError); - } else { - // Finally try to trash our old app - NSError *trashError = nil; - if (![fileManager moveItemAtURLToTrash:oldTempURL error:&trashError]) { - SULog(@"Failed to move %@ to trash. This is not a fatal error. Error: %@", oldURL, trashError); - } - } - - [fileManager removeItemAtURL:tempOldDirectoryURL error:NULL]; - } - } else { - // Create a temporary directory for our old app that resides on its volume - NSURL *tempOldDirectoryURL = [fileManager makeTemporaryDirectoryWithPreferredName:oldDestinationName appropriateForDirectoryURL:oldURL.URLByDeletingLastPathComponent error:error]; - if (tempOldDirectoryURL == nil) { - SULog(@"Failed to create temporary directory for old app at %@", oldURL.path); - [fileManager removeItemAtURL:tempNewDirectoryURL error:NULL]; - return NO; - } + // Create a temporary directory for our old app that resides on its volume + NSURL *tempOldDirectoryURL = [fileManager makeTemporaryDirectoryWithPreferredName:oldDestinationName appropriateForDirectoryURL:oldURL.URLByDeletingLastPathComponent error:error]; + if (tempOldDirectoryURL == nil) { + SULog(@"Failed to create temporary directory for old app at %@", oldURL.path); + [fileManager removeItemAtURL:tempNewDirectoryURL error:NULL]; + return NO; + } + + // Move the old app to the temporary directory + NSURL *oldTempURL = [tempOldDirectoryURL URLByAppendingPathComponent:oldDestinationNameWithPathExtension]; + if (![fileManager moveItemAtURL:oldURL toURL:oldTempURL error:error]) { + SULog(@"Failed to move the old app at %@ to a temporary location at %@", oldURL.path, oldTempURL.path); - // Move the old app to the temporary directory - NSURL *oldTempURL = [tempOldDirectoryURL URLByAppendingPathComponent:oldDestinationNameWithPathExtension]; - if (![fileManager moveItemAtURL:oldURL toURL:oldTempURL error:error]) { - SULog(@"Failed to move the old app at %@ to a temporary location at %@", oldURL.path, oldTempURL.path); - - // Just forget about our updated app on failure - [fileManager removeItemAtURL:tempNewDirectoryURL error:NULL]; - [fileManager removeItemAtURL:tempOldDirectoryURL error:NULL]; - - return NO; - } + // Just forget about our updated app on failure + [fileManager removeItemAtURL:tempNewDirectoryURL error:NULL]; + [fileManager removeItemAtURL:tempOldDirectoryURL error:NULL]; - // Move the new app to its final destination - if (![fileManager moveItemAtURL:newTempURL toURL:installationURL error:error]) { - SULog(@"Failed to move new app at %@ to final destination %@", newTempURL.path, installationURL.path); - - // Forget about our updated app on failure - [fileManager removeItemAtURL:tempNewDirectoryURL error:NULL]; - - // Attempt to restore our old app back the way it was on failure - [fileManager moveItemAtURL:oldTempURL toURL:oldURL error:NULL]; - [fileManager removeItemAtURL:tempOldDirectoryURL error:NULL]; - - return NO; - } + return NO; + } + + // Move the new app to its final destination + if (![fileManager moveItemAtURL:newTempURL toURL:installationURL error:error]) { + SULog(@"Failed to move new app at %@ to final destination %@", newTempURL.path, installationURL.path); - // Cleanup: move the old app to the trash - NSError *trashError = nil; - if (![fileManager moveItemAtURLToTrash:oldTempURL error:&trashError]) { - SULog(@"Failed to move %@ to trash with error %@", oldTempURL, trashError); - } + // Forget about our updated app on failure + [fileManager removeItemAtURL:tempNewDirectoryURL error:NULL]; + // Attempt to restore our old app back the way it was on failure + [fileManager moveItemAtURL:oldTempURL toURL:oldURL error:NULL]; [fileManager removeItemAtURL:tempOldDirectoryURL error:NULL]; + + return NO; } + // Cleanup: move the old app to the trash + NSError *trashError = nil; + if (![fileManager moveItemAtURLToTrash:oldTempURL error:&trashError]) { + SULog(@"Failed to move %@ to trash with error %@", oldTempURL, trashError); + } + + [fileManager removeItemAtURL:tempOldDirectoryURL error:NULL]; + [fileManager removeItemAtURL:tempNewDirectoryURL error:NULL]; return YES; From 5b469d702e27fe055deac58f86196e6462a0a005 Mon Sep 17 00:00:00 2001 From: Zorg Date: Sun, 16 Aug 2015 01:38:41 -0400 Subject: [PATCH 09/12] Reduce code duplication for releasing quarantine --- Sparkle/SUFileManager.m | 117 +++++++++++++++++++++++----------------- 1 file changed, 68 insertions(+), 49 deletions(-) diff --git a/Sparkle/SUFileManager.m b/Sparkle/SUFileManager.m index 5c43289b79..4dd66c35e1 100644 --- a/Sparkle/SUFileManager.m +++ b/Sparkle/SUFileManager.m @@ -206,21 +206,40 @@ - (BOOL)releaseItemFromQuarantineAtRootURL:(NSURL *)rootURL allowingAuthenticati } #endif - BOOL success = YES; - id rootResourceValue = nil; - if ([rootURL getResourceValue:&rootResourceValue forKey:NSURLQuarantinePropertiesKey error:NULL] && rootResourceValue != nil) { - NSError *setResourceError = nil; - if (![rootURL setResourceValue:[NSNull null] forKey:NSURLQuarantinePropertiesKey error:&setResourceError]) { - if (allowsAuthentication && NS_HAS_PERMISSION_ERROR(setResourceError)) { - return [self removeXAttrWithAuthentication:APPLE_QUARANTINE_IDENTIFIER fromRootURL:rootURL error:error]; - } else { - if (error != NULL) { - *error = setResourceError; + __block BOOL success = YES; + + BOOL (^releasingQuarantineRequiredAuthentication)(NSURL *, BOOL *) = ^(NSURL *fileURL, BOOL *didReleaseQuarantine){ + BOOL removedQuarantine = NO; + BOOL attemptedAuthentication = NO; + + id resourceValue = nil; + if ([fileURL getResourceValue:&resourceValue forKey:NSURLQuarantinePropertiesKey error:NULL] && resourceValue != nil) { + NSError *setResourceError = nil; + if (![fileURL setResourceValue:[NSNull null] forKey:NSURLQuarantinePropertiesKey error:&setResourceError]) { + if (allowsAuthentication && NS_HAS_PERMISSION_ERROR(setResourceError)) { + attemptedAuthentication = YES; + return [self removeXAttrWithAuthentication:APPLE_QUARANTINE_IDENTIFIER fromRootURL:rootURL error:error]; + } else { + // Make sure we haven't already run into an error + if (success && error != NULL) { + *error = setResourceError; + } + // Fail, but still try to release other items from quarantine + success = NO; } - // Fail, but still try to release other items from quarantine - success = NO; } } + + if (didReleaseQuarantine != NULL) { + *didReleaseQuarantine = removedQuarantine; + } + + return attemptedAuthentication; + }; + + BOOL releasedRootQuarantine = NO; + if (releasingQuarantineRequiredAuthentication(rootURL, &releasedRootQuarantine)) { + return releasedRootQuarantine; } // Only recurse if it's actually a directory. Don't recurse into a @@ -234,21 +253,9 @@ - (BOOL)releaseItemFromQuarantineAtRootURL:(NSURL *)rootURL allowingAuthenticati NSDirectoryEnumerator *directoryEnumerator = [_fileManager enumeratorAtURL:rootURL includingPropertiesForKeys:nil options:(NSDirectoryEnumerationOptions)0 errorHandler:nil]; for (NSURL *file in directoryEnumerator) { - id fileResourceValue = nil; - if ([file getResourceValue:&fileResourceValue forKey:NSURLQuarantinePropertiesKey error:NULL] && fileResourceValue != nil) { - NSError *setResourceError = nil; - if (![file setResourceValue:[NSNull null] forKey:NSURLQuarantinePropertiesKey error:&setResourceError]) { - if (allowsAuthentication && NS_HAS_PERMISSION_ERROR(setResourceError)) { - return [self removeXAttrWithAuthentication:APPLE_QUARANTINE_IDENTIFIER fromRootURL:rootURL error:error]; - } else { - // Make sure we haven't already run into an error - if (success && error != NULL) { - *error = setResourceError; - } - // Fail, but still try to release other items from quarantine - success = NO; - } - } + BOOL releasedQuarantine = NO; + if (releasingQuarantineRequiredAuthentication(file, &releasedQuarantine)) { + return releasedQuarantine; } } } @@ -264,24 +271,45 @@ - (BOOL)releaseItemFromQuarantineAtRootURL:(NSURL *)rootURL allowingAuthenticati // that the quarantine is implemented in part by setting an extended attribute, // "com.apple.quarantine", on affected files. Removing this attribute is // sufficient to remove files from the quarantine. + - (BOOL)releaseItemUsingOldMethodFromQuarantineAtRootURL:(NSURL *)rootURL allowingAuthentication:(BOOL)allowsAuthentication error:(NSError *__autoreleasing *)error { - BOOL success = YES; + __block BOOL success = YES; NSString *root = rootURL.path; const int removeXAttrOptions = XATTR_NOFOLLOW; - if ([self getXAttr:APPLE_QUARANTINE_IDENTIFIER fromFile:root options:removeXAttrOptions] >= 0) { - if ([self removeXAttr:APPLE_QUARANTINE_IDENTIFIER fromFile:root options:removeXAttrOptions] != 0) { - if (errno == EACCES) { - return [self removeXAttrWithAuthentication:APPLE_QUARANTINE_IDENTIFIER fromRootURL:rootURL error:error]; + BOOL (^releasingQuarantineRequiredAuthentication)(NSString *, BOOL *) = ^(NSString *path, BOOL *didReleaseQuarantine){ + BOOL removedQuarantine = NO; + BOOL attemptedAuthentication = NO; + + if ([self getXAttr:APPLE_QUARANTINE_IDENTIFIER fromFile:path options:removeXAttrOptions] >= 0) { + if ([self removeXAttr:APPLE_QUARANTINE_IDENTIFIER fromFile:path options:removeXAttrOptions] == 0) { + removedQuarantine = YES; } else { - if (error != NULL) { - *error = [NSError errorWithDomain:NSPOSIXErrorDomain code:errno userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Failed to remove file quarantine on %@.", root.lastPathComponent] }]; + if (allowsAuthentication && errno == EACCES) { + attemptedAuthentication = YES; + removedQuarantine = [self removeXAttrWithAuthentication:APPLE_QUARANTINE_IDENTIFIER fromRootURL:rootURL error:error]; + } else { + // Make sure we haven't already run into an error + if (success && error != NULL) { + *error = [NSError errorWithDomain:NSPOSIXErrorDomain code:errno userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Failed to remove file quarantine on %@.", path.lastPathComponent] }]; + } + // Fail, but still try to release other items from quarantine + success = NO; } - // Fail, but still try to release other items from quarantine - success = NO; } } + + if (didReleaseQuarantine != NULL) { + *didReleaseQuarantine = removedQuarantine; + } + + return attemptedAuthentication; + }; + + BOOL releasedRootQuarantine = NO; + if (releasingQuarantineRequiredAuthentication(root, &releasedRootQuarantine)) { + return releasedRootQuarantine; } // Only recurse if it's actually a directory. Don't recurse into a @@ -296,19 +324,10 @@ - (BOOL)releaseItemUsingOldMethodFromQuarantineAtRootURL:(NSURL *)rootURL allowi NSString *file = nil; while ((file = [directoryEnumerator nextObject])) { NSString *filePath = [root stringByAppendingPathComponent:file]; - if ([self getXAttr:APPLE_QUARANTINE_IDENTIFIER fromFile:filePath options:removeXAttrOptions] >= 0) { - if ([self removeXAttr:APPLE_QUARANTINE_IDENTIFIER fromFile:filePath options:removeXAttrOptions] != 0) { - if (allowsAuthentication && errno == EACCES) { - return [self removeXAttrWithAuthentication:APPLE_QUARANTINE_IDENTIFIER fromRootURL:rootURL error:error]; - } else { - // Make sure we haven't already run into an error - if (success && error != NULL) { - *error = [NSError errorWithDomain:NSPOSIXErrorDomain code:errno userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Failed to remove file quarantine on %@.", filePath.lastPathComponent] }]; - } - // Fail, but still try to release other items from quarantine - success = NO; - } - } + + BOOL releasedQuarantine = NO; + if (releasingQuarantineRequiredAuthentication(filePath, &releasedQuarantine)) { + return releasedQuarantine; } } } From ba73e52f092ad1e133572a92540901ce61ffe837 Mon Sep 17 00:00:00 2001 From: Zorg Date: Sun, 16 Aug 2015 10:47:37 -0400 Subject: [PATCH 10/12] Reduce more duplication for releasing quarantine --- Sparkle/SUFileManager.m | 126 +++++++++++++++++----------------------- 1 file changed, 52 insertions(+), 74 deletions(-) diff --git a/Sparkle/SUFileManager.m b/Sparkle/SUFileManager.m index 4dd66c35e1..1eddc11041 100644 --- a/Sparkle/SUFileManager.m +++ b/Sparkle/SUFileManager.m @@ -188,6 +188,36 @@ - (BOOL)removeXAttrWithAuthentication:(NSString *)name fromRootURL:(NSURL *)root return success; } +- (BOOL)releaseItemFromQuarantineAtRootURL:(NSURL *)rootURL usingMethod:(BOOL (^)(NSURL *, BOOL *, BOOL *))releasingQuarantineRequiredAuthentication +{ + BOOL success = YES; + + BOOL releasedRootQuarantine = NO; + if (releasingQuarantineRequiredAuthentication(rootURL, &releasedRootQuarantine, &success)) { + return releasedRootQuarantine; + } + + // Only recurse if it's actually a directory. Don't recurse into a + // root-level symbolic link. + NSDictionary *rootAttributes = [_fileManager attributesOfItemAtPath:rootURL.path error:nil]; + NSString *rootType = rootAttributes[NSFileType]; + + if ([rootType isEqualToString:NSFileTypeDirectory]) { + // The NSDirectoryEnumerator will avoid recursing into any contained + // symbolic links, so no further type checks are needed. + NSDirectoryEnumerator *directoryEnumerator = [_fileManager enumeratorAtURL:rootURL includingPropertiesForKeys:nil options:(NSDirectoryEnumerationOptions)0 errorHandler:nil]; + + for (NSURL *file in directoryEnumerator) { + BOOL releasedQuarantine = NO; + if (releasingQuarantineRequiredAuthentication(file, &releasedQuarantine, &success)) { + return releasedQuarantine; + } + } + } + + return success; +} + #define APPLE_QUARANTINE_IDENTIFIER @"com.apple.quarantine" // Removes the directory tree rooted at |root| from the file quarantine. @@ -206,9 +236,7 @@ - (BOOL)releaseItemFromQuarantineAtRootURL:(NSURL *)rootURL allowingAuthenticati } #endif - __block BOOL success = YES; - - BOOL (^releasingQuarantineRequiredAuthentication)(NSURL *, BOOL *) = ^(NSURL *fileURL, BOOL *didReleaseQuarantine){ + return [self releaseItemFromQuarantineAtRootURL:rootURL usingMethod:^BOOL(NSURL *fileURL, BOOL *didReleaseQuarantine, BOOL *success) { BOOL removedQuarantine = NO; BOOL attemptedAuthentication = NO; @@ -218,14 +246,16 @@ - (BOOL)releaseItemFromQuarantineAtRootURL:(NSURL *)rootURL allowingAuthenticati if (![fileURL setResourceValue:[NSNull null] forKey:NSURLQuarantinePropertiesKey error:&setResourceError]) { if (allowsAuthentication && NS_HAS_PERMISSION_ERROR(setResourceError)) { attemptedAuthentication = YES; - return [self removeXAttrWithAuthentication:APPLE_QUARANTINE_IDENTIFIER fromRootURL:rootURL error:error]; + removedQuarantine = [self removeXAttrWithAuthentication:APPLE_QUARANTINE_IDENTIFIER fromRootURL:rootURL error:error]; } else { - // Make sure we haven't already run into an error - if (success && error != NULL) { - *error = setResourceError; + if (success != NULL) { + // Make sure we haven't already run into an error + if (*success && error != NULL) { + *error = setResourceError; + } + // Fail, but still try to release other items from quarantine + *success = NO; } - // Fail, but still try to release other items from quarantine - success = NO; } } } @@ -235,32 +265,7 @@ - (BOOL)releaseItemFromQuarantineAtRootURL:(NSURL *)rootURL allowingAuthenticati } return attemptedAuthentication; - }; - - BOOL releasedRootQuarantine = NO; - if (releasingQuarantineRequiredAuthentication(rootURL, &releasedRootQuarantine)) { - return releasedRootQuarantine; - } - - // Only recurse if it's actually a directory. Don't recurse into a - // root-level symbolic link. - NSDictionary *rootAttributes = [_fileManager attributesOfItemAtPath:rootURL.path error:nil]; - NSString *rootType = rootAttributes[NSFileType]; - - if (rootType == NSFileTypeDirectory) { - // The NSDirectoryEnumerator will avoid recursing into any contained - // symbolic links, so no further type checks are needed. - NSDirectoryEnumerator *directoryEnumerator = [_fileManager enumeratorAtURL:rootURL includingPropertiesForKeys:nil options:(NSDirectoryEnumerationOptions)0 errorHandler:nil]; - - for (NSURL *file in directoryEnumerator) { - BOOL releasedQuarantine = NO; - if (releasingQuarantineRequiredAuthentication(file, &releasedQuarantine)) { - return releasedQuarantine; - } - } - } - - return success; + }]; } // Ordinarily, the quarantine is managed by calling LSSetItemAttribute @@ -274,28 +279,28 @@ - (BOOL)releaseItemFromQuarantineAtRootURL:(NSURL *)rootURL allowingAuthenticati - (BOOL)releaseItemUsingOldMethodFromQuarantineAtRootURL:(NSURL *)rootURL allowingAuthentication:(BOOL)allowsAuthentication error:(NSError *__autoreleasing *)error { - __block BOOL success = YES; - NSString *root = rootURL.path; - const int removeXAttrOptions = XATTR_NOFOLLOW; + static const int removeXAttrOptions = XATTR_NOFOLLOW; - BOOL (^releasingQuarantineRequiredAuthentication)(NSString *, BOOL *) = ^(NSString *path, BOOL *didReleaseQuarantine){ + return [self releaseItemFromQuarantineAtRootURL:rootURL usingMethod:^BOOL(NSURL *fileURL, BOOL *didReleaseQuarantine, BOOL *success) { BOOL removedQuarantine = NO; BOOL attemptedAuthentication = NO; - if ([self getXAttr:APPLE_QUARANTINE_IDENTIFIER fromFile:path options:removeXAttrOptions] >= 0) { - if ([self removeXAttr:APPLE_QUARANTINE_IDENTIFIER fromFile:path options:removeXAttrOptions] == 0) { + if ([self getXAttr:APPLE_QUARANTINE_IDENTIFIER fromFile:fileURL.path options:removeXAttrOptions] >= 0) { + if ([self removeXAttr:APPLE_QUARANTINE_IDENTIFIER fromFile:fileURL.path options:removeXAttrOptions] == 0) { removedQuarantine = YES; } else { if (allowsAuthentication && errno == EACCES) { attemptedAuthentication = YES; removedQuarantine = [self removeXAttrWithAuthentication:APPLE_QUARANTINE_IDENTIFIER fromRootURL:rootURL error:error]; } else { - // Make sure we haven't already run into an error - if (success && error != NULL) { - *error = [NSError errorWithDomain:NSPOSIXErrorDomain code:errno userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Failed to remove file quarantine on %@.", path.lastPathComponent] }]; + if (success != NULL) { + // Make sure we haven't already run into an error + if (*success && error != NULL) { + *error = [NSError errorWithDomain:NSPOSIXErrorDomain code:errno userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Failed to remove file quarantine on %@.", fileURL.lastPathComponent] }]; + } + // Fail, but still try to release other items from quarantine + *success = NO; } - // Fail, but still try to release other items from quarantine - success = NO; } } } @@ -305,34 +310,7 @@ - (BOOL)releaseItemUsingOldMethodFromQuarantineAtRootURL:(NSURL *)rootURL allowi } return attemptedAuthentication; - }; - - BOOL releasedRootQuarantine = NO; - if (releasingQuarantineRequiredAuthentication(root, &releasedRootQuarantine)) { - return releasedRootQuarantine; - } - - // Only recurse if it's actually a directory. Don't recurse into a - // root-level symbolic link. - NSDictionary *rootAttributes = [_fileManager attributesOfItemAtPath:root error:nil]; - NSString *rootType = rootAttributes[NSFileType]; - - if ([rootType isEqualToString:NSFileTypeDirectory]) { - // The NSDirectoryEnumerator will avoid recursing into any contained - // symbolic links, so no further type checks are needed. - NSDirectoryEnumerator *directoryEnumerator = [_fileManager enumeratorAtPath:root]; - NSString *file = nil; - while ((file = [directoryEnumerator nextObject])) { - NSString *filePath = [root stringByAppendingPathComponent:file]; - - BOOL releasedQuarantine = NO; - if (releasingQuarantineRequiredAuthentication(filePath, &releasedQuarantine)) { - return releasedQuarantine; - } - } - } - - return success; + }]; } - (BOOL)releaseItemFromQuarantineAtRootURL:(NSURL *)rootURL error:(NSError * __autoreleasing *)error From 182652a231116136d5ecf0bf43e4b6f156f05696 Mon Sep 17 00:00:00 2001 From: Zorg Date: Sun, 16 Aug 2015 11:01:04 -0400 Subject: [PATCH 11/12] Error if file doesn't exist when releasing quarantine --- Sparkle/SUFileManager.m | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/Sparkle/SUFileManager.m b/Sparkle/SUFileManager.m index 1eddc11041..69d8b2e2dc 100644 --- a/Sparkle/SUFileManager.m +++ b/Sparkle/SUFileManager.m @@ -188,8 +188,15 @@ - (BOOL)removeXAttrWithAuthentication:(NSString *)name fromRootURL:(NSURL *)root return success; } -- (BOOL)releaseItemFromQuarantineAtRootURL:(NSURL *)rootURL usingMethod:(BOOL (^)(NSURL *, BOOL *, BOOL *))releasingQuarantineRequiredAuthentication +- (BOOL)releaseItemFromQuarantineAtRootURL:(NSURL *)rootURL usingMethod:(BOOL (^)(NSURL *, BOOL *, BOOL *))releasingQuarantineRequiredAuthentication error:(NSError * __autoreleasing *)error { + if (![self itemExistsAtURL:rootURL]) { + if (error != NULL) { + *error = [NSError errorWithDomain:NSCocoaErrorDomain code:NSFileNoSuchFileError userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Failed to remove quarantine because %@ does not exist.", rootURL.path.lastPathComponent] }]; + } + return NO; + } + BOOL success = YES; BOOL releasedRootQuarantine = NO; @@ -265,7 +272,7 @@ - (BOOL)releaseItemFromQuarantineAtRootURL:(NSURL *)rootURL allowingAuthenticati } return attemptedAuthentication; - }]; + } error:error]; } // Ordinarily, the quarantine is managed by calling LSSetItemAttribute @@ -310,7 +317,7 @@ - (BOOL)releaseItemUsingOldMethodFromQuarantineAtRootURL:(NSURL *)rootURL allowi } return attemptedAuthentication; - }]; + } error:error]; } - (BOOL)releaseItemFromQuarantineAtRootURL:(NSURL *)rootURL error:(NSError * __autoreleasing *)error From 2782b426d92407513c5ff1fd4b5a25c53480396f Mon Sep 17 00:00:00 2001 From: Zorg Date: Sun, 16 Aug 2015 12:08:24 -0400 Subject: [PATCH 12/12] Reduce even more releasing quarantine duplication --- Sparkle/SUFileManager.m | 131 +++++++++++++++++++++------------------- 1 file changed, 68 insertions(+), 63 deletions(-) diff --git a/Sparkle/SUFileManager.m b/Sparkle/SUFileManager.m index 69d8b2e2dc..7bf38b336a 100644 --- a/Sparkle/SUFileManager.m +++ b/Sparkle/SUFileManager.m @@ -188,7 +188,9 @@ - (BOOL)removeXAttrWithAuthentication:(NSString *)name fromRootURL:(NSURL *)root return success; } -- (BOOL)releaseItemFromQuarantineAtRootURL:(NSURL *)rootURL usingMethod:(BOOL (^)(NSURL *, BOOL *, BOOL *))releasingQuarantineRequiredAuthentication error:(NSError * __autoreleasing *)error +#define APPLE_QUARANTINE_IDENTIFIER @"com.apple.quarantine" + +- (BOOL)releaseItemFromQuarantineAtRootURL:(NSURL *)rootURL allowingAuthentication:(BOOL)allowsAuthentication withQuarantineRetrieval:(BOOL (^)(NSURL *))quarantineRetrieval quarantineRemoval:(BOOL (^)(NSURL *, NSError * __autoreleasing *))quarantineRemoval isAccessError:(BOOL (^)(NSError *))isAccessError error:(NSError * __autoreleasing *)error { if (![self itemExistsAtURL:rootURL]) { if (error != NULL) { @@ -197,6 +199,38 @@ - (BOOL)releaseItemFromQuarantineAtRootURL:(NSURL *)rootURL usingMethod:(BOOL (^ return NO; } + BOOL (^releasingQuarantineRequiredAuthentication)(NSURL *, BOOL *, BOOL *) = ^(NSURL *fileURL, BOOL *didReleaseQuarantine, BOOL *success) { + BOOL removedQuarantine = NO; + BOOL attemptedAuthentication = NO; + + if (quarantineRetrieval(fileURL)) { + NSError *removalError = nil; + if (quarantineRemoval(fileURL, &removalError)) { + removedQuarantine = YES; + } else { + if (allowsAuthentication && isAccessError(removalError)) { + removedQuarantine = [self removeXAttrWithAuthentication:APPLE_QUARANTINE_IDENTIFIER fromRootURL:rootURL error:error]; + attemptedAuthentication = YES; + } else { + if (success != NULL) { + // Make sure we haven't already run into an error + if (*success && error != NULL) { + *error = removalError; + } + // Fail, but still try to release other items from quarantine + *success = NO; + } + } + } + } + + if (didReleaseQuarantine != NULL) { + *didReleaseQuarantine = removedQuarantine; + } + + return attemptedAuthentication; + }; + BOOL success = YES; BOOL releasedRootQuarantine = NO; @@ -225,8 +259,6 @@ - (BOOL)releaseItemFromQuarantineAtRootURL:(NSURL *)rootURL usingMethod:(BOOL (^ return success; } -#define APPLE_QUARANTINE_IDENTIFIER @"com.apple.quarantine" - // Removes the directory tree rooted at |root| from the file quarantine. // The quarantine was introduced on OS X 10.5 and is described at: // @@ -243,36 +275,21 @@ - (BOOL)releaseItemFromQuarantineAtRootURL:(NSURL *)rootURL allowingAuthenticati } #endif - return [self releaseItemFromQuarantineAtRootURL:rootURL usingMethod:^BOOL(NSURL *fileURL, BOOL *didReleaseQuarantine, BOOL *success) { - BOOL removedQuarantine = NO; - BOOL attemptedAuthentication = NO; - - id resourceValue = nil; - if ([fileURL getResourceValue:&resourceValue forKey:NSURLQuarantinePropertiesKey error:NULL] && resourceValue != nil) { - NSError *setResourceError = nil; - if (![fileURL setResourceValue:[NSNull null] forKey:NSURLQuarantinePropertiesKey error:&setResourceError]) { - if (allowsAuthentication && NS_HAS_PERMISSION_ERROR(setResourceError)) { - attemptedAuthentication = YES; - removedQuarantine = [self removeXAttrWithAuthentication:APPLE_QUARANTINE_IDENTIFIER fromRootURL:rootURL error:error]; - } else { - if (success != NULL) { - // Make sure we haven't already run into an error - if (*success && error != NULL) { - *error = setResourceError; - } - // Fail, but still try to release other items from quarantine - *success = NO; - } - } - } - } - - if (didReleaseQuarantine != NULL) { - *didReleaseQuarantine = removedQuarantine; - } - - return attemptedAuthentication; - } error:error]; + return + [self + releaseItemFromQuarantineAtRootURL:rootURL + allowingAuthentication:allowsAuthentication + withQuarantineRetrieval:^BOOL(NSURL *fileURL) { + id resourceValue = nil; + return ([fileURL getResourceValue:&resourceValue forKey:NSURLQuarantinePropertiesKey error:NULL] && resourceValue != nil); + } + quarantineRemoval:^BOOL(NSURL *fileURL, NSError *__autoreleasing *removalError) { + return [fileURL setResourceValue:[NSNull null] forKey:NSURLQuarantinePropertiesKey error:removalError]; + } + isAccessError:^BOOL(NSError *removalError) { + return NS_HAS_PERMISSION_ERROR(removalError); + } + error:error]; } // Ordinarily, the quarantine is managed by calling LSSetItemAttribute @@ -288,36 +305,24 @@ - (BOOL)releaseItemUsingOldMethodFromQuarantineAtRootURL:(NSURL *)rootURL allowi { static const int removeXAttrOptions = XATTR_NOFOLLOW; - return [self releaseItemFromQuarantineAtRootURL:rootURL usingMethod:^BOOL(NSURL *fileURL, BOOL *didReleaseQuarantine, BOOL *success) { - BOOL removedQuarantine = NO; - BOOL attemptedAuthentication = NO; - - if ([self getXAttr:APPLE_QUARANTINE_IDENTIFIER fromFile:fileURL.path options:removeXAttrOptions] >= 0) { - if ([self removeXAttr:APPLE_QUARANTINE_IDENTIFIER fromFile:fileURL.path options:removeXAttrOptions] == 0) { - removedQuarantine = YES; - } else { - if (allowsAuthentication && errno == EACCES) { - attemptedAuthentication = YES; - removedQuarantine = [self removeXAttrWithAuthentication:APPLE_QUARANTINE_IDENTIFIER fromRootURL:rootURL error:error]; - } else { - if (success != NULL) { - // Make sure we haven't already run into an error - if (*success && error != NULL) { - *error = [NSError errorWithDomain:NSPOSIXErrorDomain code:errno userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Failed to remove file quarantine on %@.", fileURL.lastPathComponent] }]; - } - // Fail, but still try to release other items from quarantine - *success = NO; - } - } - } - } - - if (didReleaseQuarantine != NULL) { - *didReleaseQuarantine = removedQuarantine; - } - - return attemptedAuthentication; - } error:error]; + return + [self + releaseItemFromQuarantineAtRootURL:rootURL + allowingAuthentication:allowsAuthentication + withQuarantineRetrieval:^BOOL(NSURL *fileURL) { + return ([self getXAttr:APPLE_QUARANTINE_IDENTIFIER fromFile:fileURL.path options:removeXAttrOptions] >= 0); + } + quarantineRemoval:^BOOL(NSURL *fileURL, NSError * __autoreleasing *removalError) { + BOOL removedQuarantine = ([self removeXAttr:APPLE_QUARANTINE_IDENTIFIER fromFile:fileURL.path options:removeXAttrOptions] == 0); + if (!removedQuarantine && removalError != NULL) { + *removalError = [NSError errorWithDomain:NSPOSIXErrorDomain code:errno userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Failed to remove file quarantine on %@.", fileURL.lastPathComponent] }]; + } + return removedQuarantine; + } + isAccessError:^BOOL(NSError *removalError) { + return (removalError.code == EACCES); + } + error:error]; } - (BOOL)releaseItemFromQuarantineAtRootURL:(NSURL *)rootURL error:(NSError * __autoreleasing *)error