diff --git a/Frameworks/Sparkle/CHANGELOG b/Frameworks/Sparkle/CHANGELOG index 985d84d94..d8896f4a4 100644 --- a/Frameworks/Sparkle/CHANGELOG +++ b/Frameworks/Sparkle/CHANGELOG @@ -1,3 +1,42 @@ +# 1.10.0 + +* Massive improvements to the BinaryDelta tool (Zorg) + - Ability to track file permissions (Zorg) + - Nicely formatted log output (Zorg) + - Numerous bug fixes in handling of symlinks, empty directories, case-insensitive names, etc. (Zorg) + - Refactored and modernized code (Zorg) + - libxar is no longer weak-linked (C.W. Betts) +* Double-check the code signature of the the app after installation (Isaac Wankerl) +* Added headless guided package installation (Graham Miln) +* Added ability to inject custom HTTP headers in appcast request (Mattias Gunneras) +* Changes to make unarching more reliable (Zorg, Kornel Lesiński) +* Have Sparkle build a framework module (C.W. Betts) +* Stdout used for non error outputs (JDuquennoy) +* French locale update (Kent Sutherland) + +# 1.9.0 + +* Added SUUpdater delegate method for failures. (Benjamin Gordon) +* Make the error definitions public (C.W. Betts) +* Add support for lzma compressed tarballs (Kyle Fuller) +* Back to SKIP_INSTALL=YES by default (Tony Arnold) +* Properly set install names and rpaths for targets (Jake Petroules) +* Use Library/Caches rather than app support directory (Kornel Lesiński) +* Check for a modal window being onscreen before trying to put up the Sparkle prompt (Alf Watt) +* Fixed crashes on 10.7 (Chris Campbell, Ger Teunis) +* Fixed Sparkle tags parsing (Tamás Lustyik) +* SULog code cleanups (Kevin Wojniak) +* Make sure CFBundleVersion is a semantic version number. (Jake Petroules) +* Replace typedef enums with typedef NS_ENUM to make Swift happier (C.W. Betts) +* Fix warnings under Xcode 6.1 relating the SUUpdateAlert XIB (Tony Arnold) +* Prefer string constants to strings (Jake Petroules) +* Use Info.plist keys instead of macros (Jake Petroules) +* Only export public symbols. (Jake Petroules) +* BinaryDelta: avoid crash with bad paths (Jake Petroules) +* Fixing Swedish translations (Erik Vikström) +* Turkish localization fixes (Emir) +* Proofing of Ukrainian localization (Vera Tkachenko) + # 1.8.0 * New SUDSAVerifier based on up-to-date OS X APIs (Zachary Waldowski) diff --git a/Frameworks/Sparkle/Configurations/ConfigCommon.xcconfig b/Frameworks/Sparkle/Configurations/ConfigCommon.xcconfig index 477cf7463..d8a107054 100644 --- a/Frameworks/Sparkle/Configurations/ConfigCommon.xcconfig +++ b/Frameworks/Sparkle/Configurations/ConfigCommon.xcconfig @@ -15,7 +15,7 @@ SPARKLE_AUTOMATED_DOWNGRADES = 0 SPARKLE_NORMALIZE_INSTALLED_APPLICATION_NAME = 0 SPARKLE_VERSION_MAJOR = 1 -SPARKLE_VERSION_MINOR = 8 +SPARKLE_VERSION_MINOR = 10 SPARKLE_VERSION_PATCH = 0 SPARKLE_VERSION = $(SPARKLE_VERSION_MAJOR).$(SPARKLE_VERSION_MINOR).$(SPARKLE_VERSION_PATCH) @@ -35,6 +35,7 @@ CLANG_ENABLE_OBJC_ARC = YES CLANG_ANALYZER_SECURITY_FLOATLOOPCOUNTER = YES CLANG_ANALYZER_SECURITY_INSECUREAPI_RAND = YES CLANG_ANALYZER_SECURITY_INSECUREAPI_STRCPY = YES +CLANG_ENABLE_MODULES = YES CLANG_WARN__DUPLICATE_METHOD_MATCH = YES CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES CLANG_WARN_DOCUMENTATION_COMMENTS = YES @@ -73,4 +74,4 @@ GCC_WARN_UNUSED_VARIABLE = YES WARNING_CFLAGS_EXTRA = -Wno-custom-atomic-properties -Wno-implicit-atomic-properties // Turn on all warnings, then disable a few which are almost impossible to avoid -WARNING_CFLAGS = -Wall -Weverything -Wno-empty-translation-unit -Wno-unused-macros -Wno-gnu-statement-expression -Wno-receiver-is-weak -Wno-arc-repeated-use-of-weak $(WARNING_CFLAGS_EXTRA) +WARNING_CFLAGS = -Wall -Weverything -Wno-empty-translation-unit -Wno-unused-macros -Wno-gnu-statement-expression -Wno-receiver-is-weak -Wno-arc-repeated-use-of-weak -Wno-auto-import -Wno-cstring-format-directive $(WARNING_CFLAGS_EXTRA) diff --git a/Frameworks/Sparkle/Configurations/ConfigFramework.xcconfig b/Frameworks/Sparkle/Configurations/ConfigFramework.xcconfig index e8e9d867d..ca9b25b63 100644 --- a/Frameworks/Sparkle/Configurations/ConfigFramework.xcconfig +++ b/Frameworks/Sparkle/Configurations/ConfigFramework.xcconfig @@ -9,3 +9,5 @@ FRAMEWORK_VERSION = A INFOPLIST_FILE = Sparkle/Sparkle-Info.plist GCC_PREPROCESSOR_DEFINITIONS = $(inherited) BUILDING_SPARKLE=1 OTHER_LDFLAGS = -Wl,-U,_NSURLQuarantinePropertiesKey +SKIP_INSTALL = YES +DEFINES_MODULE = YES diff --git a/Frameworks/Sparkle/Configurations/ConfigFrameworkDebug.xcconfig b/Frameworks/Sparkle/Configurations/ConfigFrameworkDebug.xcconfig index 58d6b5aa6..184b8a7d4 100644 --- a/Frameworks/Sparkle/Configurations/ConfigFrameworkDebug.xcconfig +++ b/Frameworks/Sparkle/Configurations/ConfigFrameworkDebug.xcconfig @@ -1,2 +1,4 @@ #include "ConfigFramework.xcconfig" +// Unit tests need access to non-public classes +GCC_SYMBOLS_PRIVATE_EXTERN = NO diff --git a/Frameworks/Sparkle/Configurations/ConfigRelaunch.xcconfig b/Frameworks/Sparkle/Configurations/ConfigRelaunch.xcconfig index 1537a724f..292437c93 100644 --- a/Frameworks/Sparkle/Configurations/ConfigRelaunch.xcconfig +++ b/Frameworks/Sparkle/Configurations/ConfigRelaunch.xcconfig @@ -4,4 +4,5 @@ INFOPLIST_FILE = Sparkle/Autoupdate/Autoupdate-Info.plist PRODUCT_NAME = $(SPARKLE_RELAUNCH_TOOL_NAME) SKIP_INSTALL = YES ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon +CLANG_ENABLE_MODULES = NO OTHER_LDFLAGS = -Wl,-U,_NSURLQuarantinePropertiesKey diff --git a/Frameworks/Sparkle/LICENSE b/Frameworks/Sparkle/LICENSE index f91fce3df..f4020f8d3 100644 --- a/Frameworks/Sparkle/LICENSE +++ b/Frameworks/Sparkle/LICENSE @@ -30,6 +30,9 @@ EXTERNAL LICENSES bspatch.c and bsdiff.c, from bsdiff 4.3 : Copyright (c) 2003-2005 Colin Percival. +sais.c and sais.c, from sais-lite (2010/08/07) : + Copyright (c) 2008-2010 Yuta Mori. + SUDSAVerifier.m: Copyright (c) 2011 Mark Hamlin. diff --git a/Frameworks/Sparkle/README.markdown b/Frameworks/Sparkle/README.markdown index 912b6ee3f..0e557c536 100644 --- a/Frameworks/Sparkle/README.markdown +++ b/Frameworks/Sparkle/README.markdown @@ -1,7 +1,8 @@ -# Sparkle -is an easy-to-use software update framework for Cocoa developers. +# Sparkle [![Build Status](https://travis-ci.org/sparkle-project/Sparkle.svg?branch=master)](https://travis-ci.org/sparkle-project/Sparkle) -[![Build Status](https://travis-ci.org/sparkle-project/Sparkle.svg?branch=master)](https://travis-ci.org/sparkle-project/Sparkle) +An easy-to-use software update framework for Cocoa developers. + +Sparkle shows familiar update window with release notes ## Changes since 1.5b diff --git a/Frameworks/Sparkle/Resources/Images.xcassets/AppIcon.appiconset/icon_128x128.png b/Frameworks/Sparkle/Resources/Images.xcassets/AppIcon.appiconset/icon_128x128.png index cab9bc5ae..4d7985201 100644 Binary files a/Frameworks/Sparkle/Resources/Images.xcassets/AppIcon.appiconset/icon_128x128.png and b/Frameworks/Sparkle/Resources/Images.xcassets/AppIcon.appiconset/icon_128x128.png differ diff --git a/Frameworks/Sparkle/Resources/Images.xcassets/AppIcon.appiconset/icon_128x128@2x.png b/Frameworks/Sparkle/Resources/Images.xcassets/AppIcon.appiconset/icon_128x128@2x.png index ea89c0608..2bf2b0b8a 100644 Binary files a/Frameworks/Sparkle/Resources/Images.xcassets/AppIcon.appiconset/icon_128x128@2x.png and b/Frameworks/Sparkle/Resources/Images.xcassets/AppIcon.appiconset/icon_128x128@2x.png differ diff --git a/Frameworks/Sparkle/Resources/Images.xcassets/AppIcon.appiconset/icon_16x16.png b/Frameworks/Sparkle/Resources/Images.xcassets/AppIcon.appiconset/icon_16x16.png index dea3255ce..23eada4e0 100644 Binary files a/Frameworks/Sparkle/Resources/Images.xcassets/AppIcon.appiconset/icon_16x16.png and b/Frameworks/Sparkle/Resources/Images.xcassets/AppIcon.appiconset/icon_16x16.png differ diff --git a/Frameworks/Sparkle/Resources/Images.xcassets/AppIcon.appiconset/icon_16x16@2x.png b/Frameworks/Sparkle/Resources/Images.xcassets/AppIcon.appiconset/icon_16x16@2x.png index 0e7f543e3..fa1e0dacd 100644 Binary files a/Frameworks/Sparkle/Resources/Images.xcassets/AppIcon.appiconset/icon_16x16@2x.png and b/Frameworks/Sparkle/Resources/Images.xcassets/AppIcon.appiconset/icon_16x16@2x.png differ diff --git a/Frameworks/Sparkle/Resources/Images.xcassets/AppIcon.appiconset/icon_256x256.png b/Frameworks/Sparkle/Resources/Images.xcassets/AppIcon.appiconset/icon_256x256.png index b4a17b8d0..2bf2b0b8a 100644 Binary files a/Frameworks/Sparkle/Resources/Images.xcassets/AppIcon.appiconset/icon_256x256.png and b/Frameworks/Sparkle/Resources/Images.xcassets/AppIcon.appiconset/icon_256x256.png differ diff --git a/Frameworks/Sparkle/Resources/Images.xcassets/AppIcon.appiconset/icon_32x32.png b/Frameworks/Sparkle/Resources/Images.xcassets/AppIcon.appiconset/icon_32x32.png index 77e8fde55..eb1d810a2 100644 Binary files a/Frameworks/Sparkle/Resources/Images.xcassets/AppIcon.appiconset/icon_32x32.png and b/Frameworks/Sparkle/Resources/Images.xcassets/AppIcon.appiconset/icon_32x32.png differ diff --git a/Frameworks/Sparkle/Resources/Images.xcassets/AppIcon.appiconset/icon_32x32@2x.png b/Frameworks/Sparkle/Resources/Images.xcassets/AppIcon.appiconset/icon_32x32@2x.png index a3f6f7890..f44f97798 100644 Binary files a/Frameworks/Sparkle/Resources/Images.xcassets/AppIcon.appiconset/icon_32x32@2x.png and b/Frameworks/Sparkle/Resources/Images.xcassets/AppIcon.appiconset/icon_32x32@2x.png differ diff --git a/Frameworks/Sparkle/Resources/Screenshot.png b/Frameworks/Sparkle/Resources/Screenshot.png new file mode 100644 index 000000000..19c755d44 Binary files /dev/null and b/Frameworks/Sparkle/Resources/Screenshot.png differ diff --git a/Frameworks/Sparkle/Resources/Sparkle.png b/Frameworks/Sparkle/Resources/Sparkle.png index 8ccf2e734..13d59cd27 100644 Binary files a/Frameworks/Sparkle/Resources/Sparkle.png and b/Frameworks/Sparkle/Resources/Sparkle.png differ diff --git a/Frameworks/Sparkle/Sparkle.podspec b/Frameworks/Sparkle/Sparkle.podspec index 36c44bdc3..be51c9447 100644 --- a/Frameworks/Sparkle/Sparkle.podspec +++ b/Frameworks/Sparkle/Sparkle.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = "Sparkle" - s.version = "1.8.0" + s.version = "1.9.0" s.summary = "A software update framework for OS X" s.description = "Sparkle is an easy-to-use software update framework for Cocoa developers." s.homepage = "http://sparkle-project.org" diff --git a/Frameworks/Sparkle/Sparkle.xcodeproj/project.pbxproj b/Frameworks/Sparkle/Sparkle.xcodeproj/project.pbxproj index 22ec44113..fc1c0c1ee 100644 --- a/Frameworks/Sparkle/Sparkle.xcodeproj/project.pbxproj +++ b/Frameworks/Sparkle/Sparkle.xcodeproj/project.pbxproj @@ -34,9 +34,7 @@ 14652F7C19A9725300959E44 /* SUBinaryDeltaCommon.m in Sources */ = {isa = PBXBuildFile; fileRef = 5D06E8E20FD68CC7005AE3F6 /* SUBinaryDeltaCommon.m */; }; 14652F7D19A9726700959E44 /* SUBinaryDeltaApply.m in Sources */ = {isa = PBXBuildFile; fileRef = 5D06E8E00FD68CC7005AE3F6 /* SUBinaryDeltaApply.m */; }; 14652F7E19A9728A00959E44 /* bspatch.c in Sources */ = {isa = PBXBuildFile; fileRef = 5D06E8DC0FD68CB9005AE3F6 /* bspatch.c */; settings = {COMPILER_FLAGS = "-w"; }; }; - 14652F7F19A973F900959E44 /* SUDSAVerifier.m in Sources */ = {isa = PBXBuildFile; fileRef = 61299A2E09CA2DAB00B7442F /* SUDSAVerifier.m */; }; 14652F8019A9740F00959E44 /* Security.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 61B5F8F609C4CEB300B25A18 /* Security.framework */; }; - 14652F8119A9744200959E44 /* SUBinaryDeltaCommon.m in Sources */ = {isa = PBXBuildFile; fileRef = 5D06E8E20FD68CC7005AE3F6 /* SUBinaryDeltaCommon.m */; }; 14652F8219A9746000959E44 /* SULog.m in Sources */ = {isa = PBXBuildFile; fileRef = 55C14F05136EF6DB00649790 /* SULog.m */; }; 14652F8419A978C200959E44 /* SUExport.h in Headers */ = {isa = PBXBuildFile; fileRef = 14652F8319A9759F00959E44 /* SUExport.h */; settings = {ATTRIBUTES = (Public, ); }; }; 14732BC01960F2C200593899 /* test_app_only_dsa_pub.pem in Resources */ = {isa = PBXBuildFile; fileRef = 14732BBF1960F0AC00593899 /* test_app_only_dsa_pub.pem */; }; @@ -49,7 +47,6 @@ 14950071195FCE3D00BC5B5B /* AppKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0867D6A5FE840307C02AAC07 /* AppKit.framework */; }; 14950072195FCE4B00BC5B5B /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0867D69BFE84028FC02AAC07 /* Foundation.framework */; }; 14950073195FCE4E00BC5B5B /* AppKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0867D6A5FE840307C02AAC07 /* AppKit.framework */; }; - 14950075195FDF5900BC5B5B /* SUUpdaterTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 14950074195FDF5900BC5B5B /* SUUpdaterTest.m */; }; 14958C6E19AEBC950061B14F /* signed-test-file.txt in Resources */ = {isa = PBXBuildFile; fileRef = 14958C6B19AEBC530061B14F /* signed-test-file.txt */; }; 14958C6F19AEBC980061B14F /* test-pubkey.pem in Resources */ = {isa = PBXBuildFile; fileRef = 14958C6C19AEBC610061B14F /* test-pubkey.pem */; }; 3772FEA913DE0B6B00F79537 /* SUVersionDisplayProtocol.h in Headers */ = {isa = PBXBuildFile; fileRef = 3772FEA813DE0B6B00F79537 /* SUVersionDisplayProtocol.h */; settings = {ATTRIBUTES = (Public, ); }; }; @@ -75,6 +72,11 @@ 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, ); }; }; + 5AF6C1E51AE86A410014A3AB /* SUPipedUnarchiverTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 5AF6C1EA1AE870A90014A3AB /* SUPipedUnarchiverTest.m */; }; + 5AF6C1E91AE86E270014A3AB /* test archive.zip in Resources */ = {isa = PBXBuildFile; fileRef = 5AF6C1E81AE86E270014A3AB /* test archive.zip */; }; + 5AF6C74F1AEA46D10014A3AB /* test.sparkle_guided.pkg in Resources */ = {isa = PBXBuildFile; fileRef = 5AF6C74E1AEA46D10014A3AB /* test.sparkle_guided.pkg */; }; + 5AF6C7541AEA49840014A3AB /* SUInstallerTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 5AF6C74C1AEA40760014A3AB /* SUInstallerTest.m */; }; 5AF9DC3C1981DBEE001EA135 /* SUDSAVerifierTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 5AF9DC3B1981DBEE001EA135 /* SUDSAVerifierTest.m */; }; 5D06E8E90FD68CDB005AE3F6 /* bsdiff.c in Sources */ = {isa = PBXBuildFile; fileRef = 5D06E8DB0FD68CB9005AE3F6 /* bsdiff.c */; settings = {COMPILER_FLAGS = "-w"; }; }; 5D06E8EA0FD68CDB005AE3F6 /* SUBinaryDeltaTool.m in Sources */ = {isa = PBXBuildFile; fileRef = 5D06E8E30FD68CC7005AE3F6 /* SUBinaryDeltaTool.m */; }; @@ -106,13 +108,12 @@ 61180BCB0D64138900B4E0D1 /* SUWindowController.m in Sources */ = {isa = PBXBuildFile; fileRef = 61180BC90D64138900B4E0D1 /* SUWindowController.m */; }; 6120721209CC5C4B007FE0F6 /* SUAutomaticUpdateAlert.h in Headers */ = {isa = PBXBuildFile; fileRef = 6120721009CC5C4B007FE0F6 /* SUAutomaticUpdateAlert.h */; settings = {ATTRIBUTES = (); }; }; 6120721309CC5C4B007FE0F6 /* SUAutomaticUpdateAlert.m in Sources */ = {isa = PBXBuildFile; fileRef = 6120721109CC5C4B007FE0F6 /* SUAutomaticUpdateAlert.m */; }; - 61227A160DB548B800AB99EA /* SUVersionComparisonTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 61227A150DB548B800AB99EA /* SUVersionComparisonTest.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 = (); }; }; + 61299A8D09CA790200B7442F /* SUUnarchiver.h in Headers */ = {isa = PBXBuildFile; fileRef = 61299A8B09CA790200B7442F /* SUUnarchiver.h */; settings = {ATTRIBUTES = (Private, ); }; }; 61299A8E09CA790200B7442F /* SUUnarchiver.m in Sources */ = {isa = PBXBuildFile; fileRef = 61299A8C09CA790200B7442F /* SUUnarchiver.m */; }; 61299B3609CB04E000B7442F /* Sparkle.h in Headers */ = {isa = PBXBuildFile; fileRef = 61299B3509CB04E000B7442F /* Sparkle.h */; settings = {ATTRIBUTES = (Public, ); }; }; 612DCBAF0D488BC60015DBEA /* SUUpdatePermissionPrompt.h in Headers */ = {isa = PBXBuildFile; fileRef = 612DCBAD0D488BC60015DBEA /* SUUpdatePermissionPrompt.h */; settings = {ATTRIBUTES = (); }; }; @@ -166,6 +167,20 @@ 61F83F720DBFE140006FDD30 /* SUBasicUpdateDriver.m in Sources */ = {isa = PBXBuildFile; fileRef = 61F83F700DBFE137006FDD30 /* SUBasicUpdateDriver.m */; }; 61F83F740DBFE141006FDD30 /* SUBasicUpdateDriver.h in Headers */ = {isa = PBXBuildFile; fileRef = 61F83F6F0DBFE137006FDD30 /* SUBasicUpdateDriver.h */; settings = {ATTRIBUTES = (); }; }; 61FA52880E2D9EA400EF58AD /* Sparkle.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 8DC2EF5B0486A6940098B216 /* Sparkle.framework */; settings = {ATTRIBUTES = (Required, ); }; }; + 721CF1A71AD7643600D9AC09 /* bsdiff.c in Sources */ = {isa = PBXBuildFile; fileRef = 5D06E8DB0FD68CB9005AE3F6 /* bsdiff.c */; }; + 721CF1A81AD7644100D9AC09 /* bspatch.c in Sources */ = {isa = PBXBuildFile; fileRef = 5D06E8DC0FD68CB9005AE3F6 /* bspatch.c */; }; + 721CF1A91AD7644C00D9AC09 /* sais.c in Sources */ = {isa = PBXBuildFile; fileRef = 7223E7611AD1AEFF008E3161 /* sais.c */; }; + 721CF1AA1AD7647000D9AC09 /* libxar.1.dylib in Frameworks */ = {isa = PBXBuildFile; fileRef = 5D1AF5890FD7678C0065DB48 /* libxar.1.dylib */; }; + 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 */; }; + 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 */; }; + 767B61AE1972D488004E0C3C /* SUGuidedPackageInstaller.m in Sources */ = {isa = PBXBuildFile; fileRef = 767B61AB1972D488004E0C3C /* SUGuidedPackageInstaller.m */; }; + F8761EB11ADC5068000C9034 /* SUCodeSigningVerifierTest.m in Sources */ = {isa = PBXBuildFile; fileRef = F8761EB01ADC5068000C9034 /* SUCodeSigningVerifierTest.m */; }; + F8761EB31ADC50EB000C9034 /* SparkleTestCodeSignApp.zip in Resources */ = {isa = PBXBuildFile; fileRef = F8761EB21ADC50EB000C9034 /* SparkleTestCodeSignApp.zip */; }; + F8761EB61ADC5E7A000C9034 /* SUCodeSigningVerifier.m in Sources */ = {isa = PBXBuildFile; fileRef = 61B078CD15A5FB6100600039 /* SUCodeSigningVerifier.m */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -398,7 +413,12 @@ 55C14F04136EF6DB00649790 /* SULog.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SULog.h; sourceTree = ""; }; 55C14F05136EF6DB00649790 /* SULog.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SULog.m; sourceTree = ""; }; 55C14F31136EFC2400649790 /* SystemConfiguration.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SystemConfiguration.framework; path = System/Library/Frameworks/SystemConfiguration.framework; sourceTree = SDKROOT; }; + 55E6F33219EC9F6C00005E76 /* SUErrors.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SUErrors.h; sourceTree = ""; }; 5AEF45D9189D1CC90030D7DC /* tr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = tr; path = tr.lproj/Sparkle.strings; sourceTree = ""; }; + 5AF6C1E81AE86E270014A3AB /* test archive.zip */ = {isa = PBXFileReference; lastKnownFileType = archive.zip; path = "test archive.zip"; sourceTree = ""; }; + 5AF6C1EA1AE870A90014A3AB /* SUPipedUnarchiverTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SUPipedUnarchiverTest.m; sourceTree = ""; }; + 5AF6C74C1AEA40760014A3AB /* SUInstallerTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SUInstallerTest.m; sourceTree = ""; }; + 5AF6C74E1AEA46D10014A3AB /* test.sparkle_guided.pkg */ = {isa = PBXFileReference; lastKnownFileType = file; path = test.sparkle_guided.pkg; sourceTree = ""; }; 5AF9DC3B1981DBEE001EA135 /* SUDSAVerifierTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SUDSAVerifierTest.m; sourceTree = ""; }; 5D06E8D00FD68C7C005AE3F6 /* BinaryDelta */ = {isa = PBXFileReference; explicitFileType = "compiled.mach-o.executable"; includeInIndex = 0; path = BinaryDelta; sourceTree = BUILT_PRODUCTS_DIR; }; 5D06E8DB0FD68CB9005AE3F6 /* bsdiff.c */ = {isa = PBXFileReference; comments = "-Wno-shorten-64-to-32"; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = bsdiff.c; sourceTree = ""; }; @@ -529,8 +549,16 @@ 61F614540E24A12D009F47E7 /* it */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.strings; name = it; path = it.lproj/Sparkle.strings; sourceTree = ""; }; 61F83F6F0DBFE137006FDD30 /* SUBasicUpdateDriver.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SUBasicUpdateDriver.h; sourceTree = ""; }; 61F83F700DBFE137006FDD30 /* SUBasicUpdateDriver.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SUBasicUpdateDriver.m; sourceTree = ""; }; + 7223E7611AD1AEFF008E3161 /* sais.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = sais.c; sourceTree = ""; }; + 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 = ""; }; + 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 = ""; }; 8DC2EF5B0486A6940098B216 /* Sparkle.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Sparkle.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + F8761EB01ADC5068000C9034 /* SUCodeSigningVerifierTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SUCodeSigningVerifierTest.m; sourceTree = ""; }; + F8761EB21ADC50EB000C9034 /* SparkleTestCodeSignApp.zip */ = {isa = PBXFileReference; lastKnownFileType = archive.zip; path = SparkleTestCodeSignApp.zip; sourceTree = ""; }; FA1941CA0D94A70100DD942E /* ConfigFrameworkDebug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = ConfigFrameworkDebug.xcconfig; sourceTree = ""; }; FA1941CB0D94A70100DD942E /* ConfigTestAppDebug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = ConfigTestAppDebug.xcconfig; sourceTree = ""; }; FA1941CC0D94A70100DD942E /* ConfigCommonRelease.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = ConfigCommonRelease.xcconfig; sourceTree = ""; }; @@ -579,8 +607,10 @@ files = ( 14732BD119610A1200593899 /* AppKit.framework in Frameworks */, 14732BD019610A0D00593899 /* Foundation.framework in Frameworks */, - 61FA52880E2D9EA400EF58AD /* Sparkle.framework in Frameworks */, + 721CF1AB1AD764EB00D9AC09 /* libbz2.dylib in Frameworks */, + 721CF1AA1AD7647000D9AC09 /* libxar.1.dylib in Frameworks */, 14652F8019A9740F00959E44 /* Security.framework in Frameworks */, + 61FA52880E2D9EA400EF58AD /* Sparkle.framework in Frameworks */, 14732BD319610A1800593899 /* XCTest.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -700,6 +730,8 @@ 5D06E8DB0FD68CB9005AE3F6 /* bsdiff.c */, 5D06E8DC0FD68CB9005AE3F6 /* bspatch.c */, 611142E810FB1BE5009810AA /* bspatch.h */, + 7223E7611AD1AEFF008E3161 /* sais.c */, + 7223E7621AD1AEFF008E3161 /* sais.h */, ); path = bsdiff; sourceTree = ""; @@ -756,7 +788,10 @@ isa = PBXGroup; children = ( 14958C6B19AEBC530061B14F /* signed-test-file.txt */, + F8761EB21ADC50EB000C9034 /* SparkleTestCodeSignApp.zip */, + 5AF6C1E81AE86E270014A3AB /* test archive.zip */, 14958C6C19AEBC610061B14F /* test-pubkey.pem */, + 5AF6C74E1AEA46D10014A3AB /* test.sparkle_guided.pkg */, ); path = Resources; sourceTree = ""; @@ -777,6 +812,8 @@ 5D06E8E00FD68CC7005AE3F6 /* SUBinaryDeltaApply.m */, 5D06E8E10FD68CC7005AE3F6 /* SUBinaryDeltaCommon.h */, 5D06E8E20FD68CC7005AE3F6 /* SUBinaryDeltaCommon.m */, + 7268AC641AD634E400C3E0C1 /* SUBinaryDeltaCreate.h */, + 7268AC621AD634C200C3E0C1 /* SUBinaryDeltaCreate.m */, 5D06E8E30FD68CC7005AE3F6 /* SUBinaryDeltaTool.m */, 5D06E9370FD69271005AE3F6 /* SUBinaryDeltaUnarchiver.h */, 5D06E9380FD69271005AE3F6 /* SUBinaryDeltaUnarchiver.m */, @@ -802,12 +839,15 @@ 61227A100DB5484000AB99EA /* Tests */ = { isa = PBXGroup; children = ( + 14958C7019AEBE350061B14F /* Resources */, 612279DA0DB5470200AB99EA /* SparkleTests-Info.plist */, 142E0E0819A83AAC00E4312B /* SUBinaryDeltaTest.m */, + F8761EB01ADC5068000C9034 /* SUCodeSigningVerifierTest.m */, 5AF9DC3B1981DBEE001EA135 /* SUDSAVerifierTest.m */, + 5AF6C74C1AEA40760014A3AB /* SUInstallerTest.m */, + 5AF6C1EA1AE870A90014A3AB /* SUPipedUnarchiverTest.m */, 14950074195FDF5900BC5B5B /* SUUpdaterTest.m */, 61227A150DB548B800AB99EA /* SUVersionComparisonTest.m */, - 14958C7019AEBE350061B14F /* Resources */, ); path = Tests; sourceTree = ""; @@ -849,6 +889,8 @@ 618FA6DB0DB485440026945C /* Installation */ = { isa = PBXGroup; children = ( + 767B61AA1972D488004E0C3C /* SUGuidedPackageInstaller.h */, + 767B61AB1972D488004E0C3C /* SUGuidedPackageInstaller.m */, 618FA4FF0DAE88B40026945C /* SUInstaller.h */, 618FA5000DAE88B40026945C /* SUInstaller.m */, 618FA5200DAE8E8A0026945C /* SUPackageInstaller.h */, @@ -868,6 +910,7 @@ 61CFB3280E385186007A1735 /* Sparkle.pch */, 61299A5B09CA6D4500B7442F /* SUConstants.h */, 61299A5F09CA6EB100B7442F /* SUConstants.m */, + 55E6F33219EC9F6C00005E76 /* SUErrors.h */, 14652F8319A9759F00959E44 /* SUExport.h */, 61EF67580E25C5B400F754E0 /* SUHost.h */, 61EF67550E25B58D00F754E0 /* SUHost.m */, @@ -881,10 +924,10 @@ 61B5F91D09C4CF7F00B25A18 /* Test Application */ = { isa = PBXGroup; children = ( - 14732BBF1960F0AC00593899 /* test_app_only_dsa_pub.pem */, 61B5F92A09C4CFD800B25A18 /* InfoPlist.strings */, 61B5F92409C4CFC900B25A18 /* main.m */, 61B5F92C09C4CFD800B25A18 /* MainMenu.xib */, + 14732BBF1960F0AC00593899 /* test_app_only_dsa_pub.pem */, 61B5F90409C4CEE200B25A18 /* TestApplication-Info.plist */, ); name = "Test Application"; @@ -983,9 +1026,11 @@ 5D06E9390FD69271005AE3F6 /* SUBinaryDeltaUnarchiver.h in Headers */, 61B078CE15A5FB6100600039 /* SUCodeSigningVerifier.h in Headers */, 61299A5C09CA6D4500B7442F /* SUConstants.h in Headers */, - 14652F8419A978C200959E44 /* SUExport.h in Headers */, 6102FE4A0E07803800F85D09 /* SUDiskImageUnarchiver.h in Headers */, 61299A2F09CA2DAB00B7442F /* SUDSAVerifier.h in Headers */, + 55E6F33319EC9F6C00005E76 /* SUErrors.h in Headers */, + 14652F8419A978C200959E44 /* SUExport.h in Headers */, + 767B61AC1972D488004E0C3C /* SUGuidedPackageInstaller.h in Headers */, 61EF67590E25C5B400F754E0 /* SUHost.h in Headers */, 618FA5010DAE88B40026945C /* SUInstaller.h in Headers */, 55C14F06136EF6DB00649790 /* SULog.h in Headers */, @@ -1156,7 +1201,7 @@ isa = PBXProject; attributes = { CLASSPREFIX = SU; - LastUpgradeCheck = 0600; + LastUpgradeCheck = 0630; ORGANIZATIONNAME = "Sparkle Project"; TargetAttributes = { 612279D80DB5470200AB99EA = { @@ -1233,8 +1278,11 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( - 14958C6F19AEBC980061B14F /* test-pubkey.pem in Resources */, 14958C6E19AEBC950061B14F /* signed-test-file.txt in Resources */, + F8761EB31ADC50EB000C9034 /* SparkleTestCodeSignApp.zip in Resources */, + 5AF6C1E91AE86E270014A3AB /* test archive.zip in Resources */, + 14958C6F19AEBC980061B14F /* test-pubkey.pem in Resources */, + 5AF6C74F1AEA46D10014A3AB /* test.sparkle_guided.pkg in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1252,10 +1300,10 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( - 14732BC01960F2C200593899 /* test_app_only_dsa_pub.pem in Resources */, 1420DF50196247F900203BB0 /* Images.xcassets in Resources */, 61B5F92E09C4CFD800B25A18 /* InfoPlist.strings in Resources */, 61B5F92F09C4CFD800B25A18 /* MainMenu.xib in Resources */, + 14732BC01960F2C200593899 /* test_app_only_dsa_pub.pem in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1361,7 +1409,9 @@ buildActionMask = 2147483647; files = ( 55C14BD4136EEFCE00649790 /* Autoupdate.m in Sources */, + F8761EB61ADC5E7A000C9034 /* SUCodeSigningVerifier.m in Sources */, 55C14F00136EF6B700649790 /* SUConstants.m in Sources */, + 767B61AE1972D488004E0C3C /* SUGuidedPackageInstaller.m in Sources */, 55C14F0C136EF6EA00649790 /* SUHost.m in Sources */, 55C14F0D136EF6F200649790 /* SUInstaller.m in Sources */, 55C14F08136EF6DB00649790 /* SULog.m in Sources */, @@ -1381,8 +1431,10 @@ files = ( 5D06E8E90FD68CDB005AE3F6 /* bsdiff.c in Sources */, 14652F7E19A9728A00959E44 /* bspatch.c in Sources */, + 7223E7631AD1AEFF008E3161 /* sais.c in Sources */, 14652F7D19A9726700959E44 /* SUBinaryDeltaApply.m in Sources */, 14652F7C19A9725300959E44 /* SUBinaryDeltaCommon.m in Sources */, + 7268AC631AD634C200C3E0C1 /* SUBinaryDeltaCreate.m in Sources */, 5D06E8EA0FD68CDB005AE3F6 /* SUBinaryDeltaTool.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -1391,13 +1443,16 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - 14652F8119A9744200959E44 /* SUBinaryDeltaCommon.m in Sources */, - 14652F7F19A973F900959E44 /* SUDSAVerifier.m in Sources */, - 14652F8219A9746000959E44 /* SULog.m in Sources */, - 5AF9DC3C1981DBEE001EA135 /* SUDSAVerifierTest.m in Sources */, + 721CF1A71AD7643600D9AC09 /* bsdiff.c in Sources */, + 721CF1A81AD7644100D9AC09 /* bspatch.c in Sources */, + 721CF1A91AD7644C00D9AC09 /* sais.c in Sources */, + 72D4DAA11AD7632900B211E2 /* SUBinaryDeltaCreate.m in Sources */, 142E0E0919A83AAC00E4312B /* SUBinaryDeltaTest.m in Sources */, - 14950075195FDF5900BC5B5B /* SUUpdaterTest.m in Sources */, - 61227A160DB548B800AB99EA /* SUVersionComparisonTest.m in Sources */, + F8761EB11ADC5068000C9034 /* SUCodeSigningVerifierTest.m in Sources */, + 5AF9DC3C1981DBEE001EA135 /* SUDSAVerifierTest.m in Sources */, + 5AF6C7541AEA49840014A3AB /* SUInstallerTest.m in Sources */, + 14652F8219A9746000959E44 /* SULog.m in Sources */, + 5AF6C1E51AE86A410014A3AB /* SUPipedUnarchiverTest.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1427,6 +1482,7 @@ 61299A6009CA6EB100B7442F /* SUConstants.m in Sources */, 6102FE4B0E07803800F85D09 /* SUDiskImageUnarchiver.m in Sources */, 61299A3009CA2DAB00B7442F /* SUDSAVerifier.m in Sources */, + 767B61AD1972D488004E0C3C /* SUGuidedPackageInstaller.m in Sources */, 61EF67560E25B58D00F754E0 /* SUHost.m in Sources */, 618FA5020DAE88B40026945C /* SUInstaller.m in Sources */, 55C14F07136EF6DB00649790 /* SULog.m in Sources */, diff --git a/Frameworks/Sparkle/Sparkle.xcodeproj/xcshareddata/xcschemes/Distribution.xcscheme b/Frameworks/Sparkle/Sparkle.xcodeproj/xcshareddata/xcschemes/Distribution.xcscheme index 9d6cba27c..d7da85d75 100644 --- a/Frameworks/Sparkle/Sparkle.xcodeproj/xcshareddata/xcschemes/Distribution.xcscheme +++ b/Frameworks/Sparkle/Sparkle.xcodeproj/xcshareddata/xcschemes/Distribution.xcscheme @@ -1,6 +1,6 @@ +@interface TerminationListener : NSObject @property (copy) NSString *hostpath; @property (copy) NSString *executablepath; @@ -32,6 +32,7 @@ static const NSTimeInterval SUParentQuitCheckInterval = .25; @property (strong) SUHost *host; @property (assign) BOOL shouldRelaunch; @property (assign) BOOL shouldShowUI; +@property (strong) SUStatusController *statusController; - (void)parentHasQuit; @@ -57,6 +58,7 @@ static const NSTimeInterval SUParentQuitCheckInterval = .25; @synthesize host; @synthesize shouldRelaunch; @synthesize shouldShowUI; +@synthesize statusController; - (instancetype)initWithHostPath:(NSString *)inhostpath executablePath:(NSString *)execpath parentProcessId:(pid_t)ppid folderPath:(NSString *)infolderpath shouldRelaunch:(BOOL)relaunch shouldShowUI:(BOOL)showUI selfPath:(NSString *)inSelfPath { @@ -86,6 +88,7 @@ static const NSTimeInterval SUParentQuitCheckInterval = .25; - (void)dealloc { [self.longInstallationTimer invalidate]; + [self.statusController close]; } @@ -152,40 +155,40 @@ static const NSTimeInterval SUParentQuitCheckInterval = .25; [statusCtl beginActionWithTitle:SULocalizedString(@"Installing update...", @"") maxProgressValue: 0 statusText: @""]; [statusCtl showWindow:self]; + + [self.statusController close]; // If there's an existing status controller, close it before we release our strong reference to it. + self.statusController = statusCtl; // Keep a strong reference to the status controller, or else it might get prematurely deallocated. } [SUInstaller installFromUpdateFolder:self.folderpath overHost:self.host installationPath:self.installationPath - delegate:self - versionComparator:[SUStandardVersionComparator defaultComparator]]; -} - -- (void)installerFinishedForHost:(SUHost *)__unused aHost -{ - [self relaunch]; -} - -- (void)installerForHost:(SUHost *)__unused host failedWithError:(NSError *)error __attribute__((noreturn)) -{ - if (self.shouldShowUI) - NSRunAlertPanel(@"", @"%@", @"OK", @"", @"", [error localizedDescription]); - exit(EXIT_FAILURE); + versionComparator:[SUStandardVersionComparator defaultComparator] + completionHandler:^(NSError *error) { + if (error) { + if (self.shouldShowUI) { + NSRunAlertPanel(@"", @"%@", @"OK", @"", @"", [error localizedDescription]); + } + exit(EXIT_FAILURE); + } else { + [self relaunch]; + } + }]; } @end int main(int __unused argc, const char __unused *argv[]) { - @autoreleasepool { + @autoreleasepool + { NSArray *args = [[NSProcessInfo processInfo] arguments]; if (args.count < 5 || args.count > 7) { return EXIT_FAILURE; } BOOL shouldShowUI = (args.count > 6) ? [args[6] boolValue] : YES; - if (shouldShowUI) - { + if (shouldShowUI) { [[NSApplication sharedApplication] activateIgnoringOtherApps:YES]; } @@ -198,9 +201,9 @@ int main(int __unused argc, const char __unused *argv[]) shouldShowUI:shouldShowUI selfPath:[[NSBundle mainBundle] bundlePath]]; - [termListen class]; [[NSApplication sharedApplication] run]; - + // Ensure termListen is not deallocated by ARC before caling -[NSApplication run] + [termListen class]; } return EXIT_SUCCESS; diff --git a/Frameworks/Sparkle/Sparkle/SUAppcast.h b/Frameworks/Sparkle/Sparkle/SUAppcast.h index 7d455231b..8f3efc8e8 100644 --- a/Frameworks/Sparkle/Sparkle/SUAppcast.h +++ b/Frameworks/Sparkle/Sparkle/SUAppcast.h @@ -9,24 +9,18 @@ #ifndef SUAPPCAST_H #define SUAPPCAST_H +#import #import "SUExport.h" -@protocol SUAppcastDelegate; - @class SUAppcastItem; -SU_EXPORT @interface SUAppcast : NSObject +SU_EXPORT @interface SUAppcast : NSObject -@property (weak) id delegate; @property (copy) NSString *userAgentString; +@property (copy) NSDictionary *httpHeaders; -- (void)fetchAppcastFromURL:(NSURL *)url; +- (void)fetchAppcastFromURL:(NSURL *)url completionBlock:(void (^)(NSError *))err; @property (readonly, copy) NSArray *items; @end -@protocol SUAppcastDelegate -- (void)appcastDidFinishLoading:(SUAppcast *)appcast; -- (void)appcast:(SUAppcast *)appcast failedToLoadWithError:(NSError *)error; -@end - #endif diff --git a/Frameworks/Sparkle/Sparkle/SUAppcast.m b/Frameworks/Sparkle/Sparkle/SUAppcast.m index 196267fb9..eda206a97 100644 --- a/Frameworks/Sparkle/Sparkle/SUAppcast.m +++ b/Frameworks/Sparkle/Sparkle/SUAppcast.m @@ -33,6 +33,7 @@ @end @interface SUAppcast () +@property (strong) void (^completionBlock)(NSError *); @property (copy) NSString *downloadFilename; @property (strong) NSURLDownload *download; @property (copy) NSArray *items; @@ -43,18 +44,28 @@ @implementation SUAppcast @synthesize downloadFilename; -@synthesize delegate; +@synthesize completionBlock; @synthesize userAgentString; +@synthesize httpHeaders; @synthesize download; @synthesize items; -- (void)fetchAppcastFromURL:(NSURL *)url +- (void)fetchAppcastFromURL:(NSURL *)url completionBlock:(void (^)(NSError *))block { + self.completionBlock = block; + NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url cachePolicy:NSURLRequestReloadIgnoringCacheData timeoutInterval:30.0]; if (self.userAgentString) { [request setValue:self.userAgentString forHTTPHeaderField:@"User-Agent"]; } + if (self.httpHeaders) { + for (NSString *key in self.httpHeaders) { + id value = [self.httpHeaders objectForKey:key]; + [request setValue:value forHTTPHeaderField:key]; + } + } + [request setValue:@"application/rss+xml,*/*;q=0.1" forHTTPHeaderField:@"Accept"]; self.download = [[NSURLDownload alloc] initWithRequest:request delegate:self]; @@ -202,31 +213,23 @@ } } - if ([appcastItems count]) - { -#pragma clang diagnostic push -#pragma clang diagnostic ignored "-Wselector" - // @selector(date) is from SUAppcastItem - NSSortDescriptor *sort = [[NSSortDescriptor alloc] initWithKey:NSStringFromSelector(@selector(date)) ascending:NO]; -#pragma clang diagnostic pop + if ([appcastItems count]) { + NSSortDescriptor *sort = [[NSSortDescriptor alloc] initWithKey:@"date" ascending:NO]; [appcastItems sortUsingDescriptors:@[sort]]; self.items = appcastItems; } - if (failed) - { + if (failed) { [self reportError:[NSError errorWithDomain:SUSparkleErrorDomain code:SUAppcastParseError userInfo:@{ NSLocalizedDescriptionKey: SULocalizedString(@"An error occurred while parsing the update feed.", nil) }]]; - } - else if ([self.delegate respondsToSelector:@selector(appcastDidFinishLoading:)]) - { - [self.delegate appcastDidFinishLoading:self]; + } else { + self.completionBlock(nil); + self.completionBlock = nil; } } - (void)download:(NSURLDownload *)__unused aDownload didFailWithError:(NSError *)error { - if (self.downloadFilename) - { + if (self.downloadFilename) { [[NSFileManager defaultManager] removeItemAtPath:self.downloadFilename error:nil]; } self.downloadFilename = nil; @@ -241,10 +244,15 @@ - (void)reportError:(NSError *)error { - if ([self.delegate respondsToSelector:@selector(appcast:failedToLoadWithError:)]) - { - [self.delegate appcast:self failedToLoadWithError:[NSError errorWithDomain:SUSparkleErrorDomain code:SUAppcastError userInfo:@{NSLocalizedDescriptionKey: SULocalizedString(@"An error occurred in retrieving update information. Please try again later.", nil), NSLocalizedFailureReasonErrorKey: [error localizedDescription]}]]; - } + NSURL *failingUrl = error.userInfo[NSURLErrorFailingURLErrorKey]; + + self.completionBlock([NSError errorWithDomain:SUSparkleErrorDomain code:SUAppcastError userInfo:@{ + NSLocalizedDescriptionKey: SULocalizedString(@"An error occurred in retrieving update information. Please try again later.", nil), + NSLocalizedFailureReasonErrorKey: [error localizedDescription], + NSUnderlyingErrorKey: error, + NSURLErrorFailingURLErrorKey: failingUrl ? failingUrl : [NSNull null], + }]); + self.completionBlock = nil; } - (NSXMLNode *)bestNodeInNodes:(NSArray *)nodes diff --git a/Frameworks/Sparkle/Sparkle/SUAutomaticUpdateAlert.h b/Frameworks/Sparkle/Sparkle/SUAutomaticUpdateAlert.h index 0a2c86102..1fcfcfb7b 100644 --- a/Frameworks/Sparkle/Sparkle/SUAutomaticUpdateAlert.h +++ b/Frameworks/Sparkle/Sparkle/SUAutomaticUpdateAlert.h @@ -11,8 +11,6 @@ #import "SUWindowController.h" -@protocol SUAutomaticUpdateAlertDelegate; - typedef NS_ENUM(NSInteger, SUAutomaticInstallationChoice) { SUInstallNowChoice, SUInstallLaterChoice, @@ -22,15 +20,11 @@ typedef NS_ENUM(NSInteger, SUAutomaticInstallationChoice) { @class SUAppcastItem, SUHost; @interface SUAutomaticUpdateAlert : SUWindowController -- (instancetype)initWithAppcastItem:(SUAppcastItem *)item host:(SUHost *)hostBundle delegate:(id)delegate; +- (instancetype)initWithAppcastItem:(SUAppcastItem *)item host:(SUHost *)hostBundle completionBlock:(void (^)(SUAutomaticInstallationChoice))c; - (IBAction)installNow:sender; - (IBAction)installLater:sender; - (IBAction)doNotInstall:sender; @end -@protocol SUAutomaticUpdateAlertDelegate -- (void)automaticUpdateAlert:(SUAutomaticUpdateAlert *)aua finishedWithChoice:(SUAutomaticInstallationChoice)choice; -@end - #endif diff --git a/Frameworks/Sparkle/Sparkle/SUAutomaticUpdateAlert.m b/Frameworks/Sparkle/Sparkle/SUAutomaticUpdateAlert.m index 85cb0cbcb..4271e6084 100644 --- a/Frameworks/Sparkle/Sparkle/SUAutomaticUpdateAlert.m +++ b/Frameworks/Sparkle/Sparkle/SUAutomaticUpdateAlert.m @@ -11,23 +11,22 @@ #import "SUHost.h" @interface SUAutomaticUpdateAlert () +@property (strong) void(^completionBlock)(SUAutomaticInstallationChoice); @property (strong) SUAppcastItem *updateItem; -@property (weak) id delegate; @property (strong) SUHost *host; @end @implementation SUAutomaticUpdateAlert -@synthesize delegate; @synthesize host; @synthesize updateItem; +@synthesize completionBlock; -- (instancetype)initWithAppcastItem:(SUAppcastItem *)item host:(SUHost *)aHost delegate:(id)del +- (instancetype)initWithAppcastItem:(SUAppcastItem *)item host:(SUHost *)aHost completionBlock:(void (^)(SUAutomaticInstallationChoice))block { - self = [super initWithHost:aHost windowNibName:@"SUAutomaticUpdateAlert"]; - if (self) - { + self = [super initWithWindowNibName:@"SUAutomaticUpdateAlert"]; + if (self) { self.updateItem = item; - self.delegate = del; + self.completionBlock = block; self.host = aHost; [self setShouldCascadeWindows:NO]; [[self window] center]; @@ -40,19 +39,22 @@ - (IBAction)installNow:(id)__unused sender { [self close]; - [self.delegate automaticUpdateAlert:self finishedWithChoice:SUInstallNowChoice]; + self.completionBlock(SUInstallNowChoice); + self.completionBlock = nil; } - (IBAction)installLater:(id)__unused sender { [self close]; - [self.delegate automaticUpdateAlert:self finishedWithChoice:SUInstallLaterChoice]; + self.completionBlock(SUInstallLaterChoice); + self.completionBlock = nil; } - (IBAction)doNotInstall:(id)__unused sender { [self close]; - [self.delegate automaticUpdateAlert:self finishedWithChoice:SUDoNotInstallChoice]; + self.completionBlock(SUDoNotInstallChoice); + self.completionBlock = nil; } - (NSImage *)applicationIcon diff --git a/Frameworks/Sparkle/Sparkle/SUAutomaticUpdateDriver.h b/Frameworks/Sparkle/Sparkle/SUAutomaticUpdateDriver.h index 9935936eb..4dd90e8f0 100644 --- a/Frameworks/Sparkle/Sparkle/SUAutomaticUpdateDriver.h +++ b/Frameworks/Sparkle/Sparkle/SUAutomaticUpdateDriver.h @@ -13,7 +13,7 @@ #import "SUBasicUpdateDriver.h" #import "SUAutomaticUpdateAlert.h" -@interface SUAutomaticUpdateDriver : SUBasicUpdateDriver +@interface SUAutomaticUpdateDriver : SUBasicUpdateDriver @end diff --git a/Frameworks/Sparkle/Sparkle/SUAutomaticUpdateDriver.m b/Frameworks/Sparkle/Sparkle/SUAutomaticUpdateDriver.m index 20a0a0658..45b3d5469 100644 --- a/Frameworks/Sparkle/Sparkle/SUAutomaticUpdateDriver.m +++ b/Frameworks/Sparkle/Sparkle/SUAutomaticUpdateDriver.m @@ -42,13 +42,14 @@ static const NSTimeInterval SUAutomaticUpdatePromptImpatienceTimer = 60 * 60 * 2 - (void)showUpdateAlert { self.interruptible = NO; - self.alert = [[SUAutomaticUpdateAlert alloc] initWithAppcastItem:self.updateItem host:self.host delegate:self]; + self.alert = [[SUAutomaticUpdateAlert alloc] initWithAppcastItem:self.updateItem host:self.host completionBlock:^(SUAutomaticInstallationChoice choice) { + [self automaticUpdateAlertFinishedWithChoice:choice]; + }]; // If the app is a menubar app or the like, we need to focus it first and alter the // update prompt to behave like a normal window. Otherwise if the window were hidden // there may be no way for the application to be activated to make it visible again. - if ([self.host isBackgroundApplication]) - { + if ([self.host isBackgroundApplication]) { [[self.alert window] setHidesOnDeactivate:NO]; [NSApp activateIgnoringOtherApps:YES]; } @@ -138,7 +139,7 @@ static const NSTimeInterval SUAutomaticUpdatePromptImpatienceTimer = 60 * 60 * 2 [[NSNotificationCenter defaultCenter] removeObserver:self name:NSApplicationDidBecomeActiveNotification object:NSApp]; } -- (void)automaticUpdateAlert:(SUAutomaticUpdateAlert *)__unused aua finishedWithChoice:(SUAutomaticInstallationChoice)choice +- (void)automaticUpdateAlertFinishedWithChoice:(SUAutomaticInstallationChoice)choice { switch (choice) { @@ -161,8 +162,6 @@ static const NSTimeInterval SUAutomaticUpdatePromptImpatienceTimer = 60 * 60 * 2 } } -- (BOOL)shouldInstallSynchronously { return self.postponingInstallation; } - - (void)installWithToolAndRelaunch:(BOOL)relaunch displayingUserInterface:(BOOL)showUI { if (relaunch) { @@ -180,10 +179,18 @@ static const NSTimeInterval SUAutomaticUpdatePromptImpatienceTimer = 60 * 60 * 2 - (void)abortUpdateWithError:(NSError *)error { - if (self.showErrors) + if (self.showErrors) { [super abortUpdateWithError:error]; - else + } else { + // Call delegate separately here because otherwise it won't know we stopped. + // Normally this gets called by the superclass + id updaterDelegate = [self.updater delegate]; + if ([updaterDelegate respondsToSelector:@selector(updater:didAbortWithError:)]) { + [updaterDelegate updater:self.updater didAbortWithError:error]; + } + [self abortUpdate]; + } } @end diff --git a/Frameworks/Sparkle/Sparkle/SUBasicUpdateDriver.h b/Frameworks/Sparkle/Sparkle/SUBasicUpdateDriver.h index 32e320bc6..8c6960d83 100644 --- a/Frameworks/Sparkle/Sparkle/SUBasicUpdateDriver.h +++ b/Frameworks/Sparkle/Sparkle/SUBasicUpdateDriver.h @@ -15,7 +15,7 @@ #import "SUAppcast.h" @class SUAppcastItem, SUHost; -@interface SUBasicUpdateDriver : SUUpdateDriver +@interface SUBasicUpdateDriver : SUUpdateDriver @property (strong, readonly) SUAppcastItem *updateItem; @property (strong, readonly) NSURLDownload *download; @@ -27,6 +27,7 @@ - (BOOL)hostSupportsItem:(SUAppcastItem *)ui; - (BOOL)itemContainsSkippedVersion:(SUAppcastItem *)ui; - (BOOL)itemContainsValidUpdate:(SUAppcastItem *)ui; +- (void)appcastDidFinishLoading:(SUAppcast *)ac; - (void)didFindValidUpdate; - (void)didNotFindUpdate; diff --git a/Frameworks/Sparkle/Sparkle/SUBasicUpdateDriver.m b/Frameworks/Sparkle/Sparkle/SUBasicUpdateDriver.m index 12147df72..a0536faba 100644 --- a/Frameworks/Sparkle/Sparkle/SUBasicUpdateDriver.m +++ b/Frameworks/Sparkle/Sparkle/SUBasicUpdateDriver.m @@ -54,9 +54,15 @@ SUAppcast *appcast = [[SUAppcast alloc] init]; - [appcast setDelegate:self]; [appcast setUserAgentString:[self.updater userAgentString]]; - [appcast fetchAppcastFromURL:URL]; + [appcast setHttpHeaders:[self.updater httpHeaders]]; + [appcast fetchAppcastFromURL:URL completionBlock:^(NSError *error) { + if (error) { + [self abortUpdateWithError:error]; + } else { + [self appcastDidFinishLoading:appcast]; + } + }]; } - (id)versionComparator @@ -136,12 +142,10 @@ item = [updateEnumerator nextObject]; } while (item && ![self hostSupportsItem:item]); - if (binaryDeltaSupported()) { - SUAppcastItem *deltaUpdateItem = [item deltaUpdates][[self.host version]]; - if (deltaUpdateItem && [self hostSupportsItem:deltaUpdateItem]) { - self.nonDeltaUpdateItem = item; - item = deltaUpdateItem; - } + SUAppcastItem *deltaUpdateItem = [item deltaUpdates][[self.host version]]; + if (deltaUpdateItem && [self hostSupportsItem:deltaUpdateItem]) { + self.nonDeltaUpdateItem = item; + item = deltaUpdateItem; } } @@ -154,11 +158,6 @@ } } -- (void)appcast:(SUAppcast *)__unused ac failedToLoadWithError:(NSError *)error -{ - [self abortUpdateWithError:error]; -} - - (void)didFindValidUpdate { assert(self.updateItem); @@ -200,11 +199,11 @@ NSString *downloadFileName = [NSString stringWithFormat:@"%@ %@", [self.host name], [self.updateItem versionString]]; - self.tempDir = [[self.host appSupportPath] stringByAppendingPathComponent:downloadFileName]; + self.tempDir = [self.host.appCachePath stringByAppendingPathComponent:downloadFileName]; int cnt = 1; while ([[NSFileManager defaultManager] fileExistsAtPath:self.tempDir] && cnt <= 999) { - self.tempDir = [[self.host appSupportPath] stringByAppendingPathComponent:[NSString stringWithFormat:@"%@ %d", downloadFileName, cnt++]]; + self.tempDir = [self.host.appCachePath stringByAppendingPathComponent:[NSString stringWithFormat:@"%@ %d", downloadFileName, cnt++]]; } // Create the temporary directory if necessary. @@ -227,7 +226,7 @@ if (newBundlePath) { if ([SUCodeSigningVerifier hostApplicationIsCodeSigned]) { NSError *error = nil; - if ([SUCodeSigningVerifier codeSignatureIsValidAtPath:newBundlePath error:&error]) { + if ([SUCodeSigningVerifier codeSignatureMatchesHostAndIsValidAtPath:newBundlePath error:&error]) { return YES; } else { SULog(@"Code signature check on update failed: %@. Sparkle will use DSA signature instead.", error); @@ -254,7 +253,16 @@ - (void)download:(NSURLDownload *)__unused download didFailWithError:(NSError *)error { - [self abortUpdateWithError:[NSError errorWithDomain:SUSparkleErrorDomain code:SURelaunchError userInfo:@{NSLocalizedDescriptionKey: SULocalizedString(@"An error occurred while downloading the update. Please try again later.", nil), NSLocalizedFailureReasonErrorKey: [error localizedDescription]}]]; + NSURL *failingUrl = error.userInfo[NSURLErrorFailingURLErrorKey]; + if (!failingUrl) { + failingUrl = [self.updateItem fileURL]; + } + + [self abortUpdateWithError:[NSError errorWithDomain:SUSparkleErrorDomain code:SURelaunchError userInfo:@{ + NSLocalizedDescriptionKey: SULocalizedString(@"An error occurred while downloading the update. Please try again later.", nil), + NSUnderlyingErrorKey: error, + NSURLErrorFailingURLErrorKey: failingUrl ? failingUrl : [NSNull null], + }]]; } - (BOOL)download:(NSURLDownload *)__unused download shouldDecodeSourceDataOfMIMEType:(NSString *)encodingType @@ -266,14 +274,13 @@ - (void)extractUpdate { - SUUnarchiver *unarchiver = [SUUnarchiver unarchiverForPath:self.downloadPath updatingHost:self.host]; - if (!unarchiver) - { + SUUnarchiver *unarchiver = [SUUnarchiver unarchiverForPath:self.downloadPath updatingHostBundlePath:[[self.host bundle] bundlePath]]; + if (!unarchiver) { SULog(@"Error: No valid unarchiver for %@!", self.downloadPath); [self unarchiverDidFail:nil]; return; } - [unarchiver setDelegate:self]; + unarchiver.delegate = self; [unarchiver start]; } @@ -303,8 +310,6 @@ [self abortUpdateWithError:[NSError errorWithDomain:SUSparkleErrorDomain code:SUUnarchivingError userInfo:@{ NSLocalizedDescriptionKey: SULocalizedString(@"An error occurred while extracting the archive. Please try again later.", nil) }]]; } -- (BOOL)shouldInstallSynchronously { return NO; } - - (void)installWithToolAndRelaunch:(BOOL)relaunch { // Perhaps a poor assumption but: if we're not relaunching, we assume we shouldn't be showing any UI either. Because non-relaunching installations are kicked off without any user interaction, we shouldn't be interrupting them. @@ -356,27 +361,32 @@ // 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 appSupportPath] stringByAppendingPathComponent:[relaunchPathToCopy lastPathComponent]]; + 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. NSError *error = nil; [[NSFileManager defaultManager] createDirectoryAtPath:[targetPath stringByDeletingLastPathComponent] withIntermediateDirectories:YES attributes:@{} error:&error]; - if ([SUPlainInstaller copyPathWithAuthentication:relaunchPathToCopy overPath:targetPath temporaryName:nil error:&error]) + if ([SUPlainInstaller copyPathWithAuthentication:relaunchPathToCopy overPath:targetPath temporaryName:nil error:&error]) { self.relaunchPath = targetPath; - else - [self abortUpdateWithError:[NSError errorWithDomain:SUSparkleErrorDomain code:SURelaunchError userInfo:@{NSLocalizedDescriptionKey: SULocalizedString(@"An error occurred while extracting the archive. Please try again later.", nil), NSLocalizedFailureReasonErrorKey: [NSString stringWithFormat:@"Couldn't copy relauncher (%@) to temporary path (%@)! %@", relaunchPathToCopy, targetPath, (error ? [error localizedDescription] : @"")]}]]; + } else { + [self abortUpdateWithError:[NSError errorWithDomain:SUSparkleErrorDomain code:SURelaunchError userInfo:@{ + NSLocalizedDescriptionKey: SULocalizedString(@"An error occurred while extracting the archive. Please try again later.", nil), + NSLocalizedFailureReasonErrorKey: [NSString stringWithFormat:@"Couldn't copy relauncher (%@) to temporary path (%@)! %@", relaunchPathToCopy, targetPath, (error ? [error localizedDescription] : @"")] + }]]; + } } [[NSNotificationCenter defaultCenter] postNotificationName:SUUpdaterWillRestartNotification object:self]; if ([updaterDelegate respondsToSelector:@selector(updaterWillRelaunchApplication:)]) [updaterDelegate updaterWillRelaunchApplication:self.updater]; - if(!self.relaunchPath || ![[NSFileManager defaultManager] fileExistsAtPath:self.relaunchPath]) - { + if (!self.relaunchPath || ![[NSFileManager defaultManager] fileExistsAtPath:self.relaunchPath]) { // Note that we explicitly use the host app's name here, since updating plugin for Mail relaunches Mail, not just the plugin. - [self abortUpdateWithError:[NSError errorWithDomain:SUSparkleErrorDomain code:SURelaunchError userInfo:@{NSLocalizedDescriptionKey: [NSString stringWithFormat:SULocalizedString(@"An error occurred while relaunching %1$@, but the new version will be available next time you run %1$@.", nil), [self.host name]], NSLocalizedFailureReasonErrorKey: [NSString stringWithFormat:@"Couldn't find the relauncher (expected to find it at %@)", self.relaunchPath]}]]; + [self abortUpdateWithError:[NSError errorWithDomain:SUSparkleErrorDomain code:SURelaunchError userInfo:@{ + NSLocalizedDescriptionKey: [NSString stringWithFormat:SULocalizedString(@"An error occurred while relaunching %1$@, but the new version will be available next time you run %1$@.", nil), [self.host name]], + NSLocalizedFailureReasonErrorKey: [NSString stringWithFormat:@"Couldn't find the relauncher (expected to find it at %@)", self.relaunchPath] + }]]; // We intentionally don't abandon the update here so that the host won't initiate another. return; } @@ -414,10 +424,16 @@ - (void)installerForHost:(SUHost *)aHost failedWithError:(NSError *)error { - if (aHost != self.host) { return; } + if (aHost != self.host) { + return; + } NSError *dontThrow = nil; [[NSFileManager defaultManager] removeItemAtPath:self.relaunchPath error:&dontThrow]; // Clean up the copied relauncher - [self abortUpdateWithError:[NSError errorWithDomain:SUSparkleErrorDomain code:SUInstallationError userInfo:@{NSLocalizedDescriptionKey: SULocalizedString(@"An error occurred while installing the update. Please try again later.", nil), NSLocalizedFailureReasonErrorKey: [error localizedDescription]}]]; + [self abortUpdateWithError:[NSError errorWithDomain:SUSparkleErrorDomain code:SUInstallationError userInfo:@{ + NSLocalizedDescriptionKey: SULocalizedString(@"An error occurred while installing the update. Please try again later.", nil), + NSLocalizedFailureReasonErrorKey: [error localizedDescription], + NSUnderlyingErrorKey: error, + }]]; } - (void)abortUpdate @@ -431,11 +447,18 @@ - (void)abortUpdateWithError:(NSError *)error { if ([error code] != SUNoUpdateError) { // Let's not bother logging this. - SULog(@"Error: %@ %@", [error localizedDescription], [error localizedFailureReason]); + SULog(@"Error: %@ %@ (URL %@)", error.localizedDescription, error.localizedFailureReason, error.userInfo[NSURLErrorFailingURLErrorKey]); } if (self.download) { [self.download cancel]; } + + // Notify host app that update has aborted + id updaterDelegate = [self.updater delegate]; + if ([updaterDelegate respondsToSelector:@selector(updater:didAbortWithError:)]) { + [updaterDelegate updater:self.updater didAbortWithError:error]; + } + [self abortUpdate]; } diff --git a/Frameworks/Sparkle/Sparkle/SUBinaryDeltaApply.h b/Frameworks/Sparkle/Sparkle/SUBinaryDeltaApply.h index d44501093..293c1e11a 100644 --- a/Frameworks/Sparkle/Sparkle/SUBinaryDeltaApply.h +++ b/Frameworks/Sparkle/Sparkle/SUBinaryDeltaApply.h @@ -10,6 +10,6 @@ #define SUBINARYDELTAAPPLY_H @class NSString; -int applyBinaryDelta(NSString *source, NSString *destination, NSString *patchFile); +int applyBinaryDelta(NSString *source, NSString *destination, NSString *patchFile, BOOL verbose); #endif diff --git a/Frameworks/Sparkle/Sparkle/SUBinaryDeltaApply.m b/Frameworks/Sparkle/Sparkle/SUBinaryDeltaApply.m index 85dbd0cd5..43952df98 100644 --- a/Frameworks/Sparkle/Sparkle/SUBinaryDeltaApply.m +++ b/Frameworks/Sparkle/Sparkle/SUBinaryDeltaApply.m @@ -15,57 +15,125 @@ #include #include -static void applyBinaryDeltaToFile(xar_t x, xar_file_t file, NSString *sourceFilePath, NSString *destinationFilePath) +static BOOL applyBinaryDeltaToFile(xar_t x, xar_file_t file, NSString *sourceFilePath, NSString *destinationFilePath) { NSString *patchFile = temporaryFilename(@"apply-binary-delta"); xar_extract_tofile(x, file, [patchFile fileSystemRepresentation]); const char *argv[] = {"/usr/bin/bspatch", [sourceFilePath fileSystemRepresentation], [destinationFilePath fileSystemRepresentation], [patchFile fileSystemRepresentation]}; - bspatch(4, (char **)argv); + BOOL success = (bspatch(4, (char **)argv) == 0); unlink([patchFile fileSystemRepresentation]); + return success; } -int applyBinaryDelta(NSString *source, NSString *destination, NSString *patchFile) +int applyBinaryDelta(NSString *source, NSString *destination, NSString *patchFile, BOOL verbose) { - xar_t x = xar_open([patchFile UTF8String], READ); + xar_t x = xar_open([patchFile fileSystemRepresentation], READ); if (!x) { - fprintf(stderr, "Unable to open %s. Giving up.\n", [patchFile UTF8String]); + fprintf(stderr, "Unable to open %s. Giving up.\n", [patchFile fileSystemRepresentation]); return 1; } + + SUBinaryDeltaMajorVersion majorDiffVersion = FIRST_DELTA_DIFF_MAJOR_VERSION; + SUBinaryDeltaMinorVersion minorDiffVersion = FIRST_DELTA_DIFF_MINOR_VERSION; - NSString *expectedBeforeHash = nil; - NSString *expectedAfterHash = nil; + NSString *expectedBeforeHashv1 = nil; + NSString *expectedAfterHashv1 = nil; + + NSString *expectedNewBeforeHash = nil; + NSString *expectedNewAfterHash = nil; + xar_subdoc_t subdoc; for (subdoc = xar_subdoc_first(x); subdoc; subdoc = xar_subdoc_next(subdoc)) { - if (!strcmp(xar_subdoc_name(subdoc), "binary-delta-attributes")) { + if (!strcmp(xar_subdoc_name(subdoc), BINARY_DELTA_ATTRIBUTES_KEY)) { const char *value = 0; - xar_subdoc_prop_get(subdoc, "before-sha1", &value); + + // available in version 2.0 or later + xar_subdoc_prop_get(subdoc, MAJOR_DIFF_VERSION_KEY, &value); if (value) - expectedBeforeHash = @(value); - - xar_subdoc_prop_get(subdoc, "after-sha1", &value); + majorDiffVersion = (SUBinaryDeltaMajorVersion)[@(value) intValue]; + + // available in version 2.0 or later + xar_subdoc_prop_get(subdoc, MINOR_DIFF_VERSION_KEY, &value); if (value) - expectedAfterHash = @(value); + minorDiffVersion = (SUBinaryDeltaMinorVersion)[@(value) intValue]; + + // available in version 2.0 or later + xar_subdoc_prop_get(subdoc, BEFORE_TREE_SHA1_KEY, &value); + if (value) + expectedNewBeforeHash = @(value); + + // available in version 2.0 or later + xar_subdoc_prop_get(subdoc, AFTER_TREE_SHA1_KEY, &value); + if (value) + expectedNewAfterHash = @(value); + + // only available in version 1.0 + xar_subdoc_prop_get(subdoc, BEFORE_TREE_SHA1_OLD_KEY, &value); + if (value) + expectedBeforeHashv1 = @(value); + + // only available in version 1.0 + xar_subdoc_prop_get(subdoc, AFTER_TREE_SHA1_OLD_KEY, &value); + if (value) + expectedAfterHashv1 = @(value); } } + + if (majorDiffVersion < FIRST_DELTA_DIFF_MAJOR_VERSION) { + fprintf(stderr, "Unable to identify diff-version %u in delta. Giving up.\n", majorDiffVersion); + return 1; + } + + if (majorDiffVersion > LATEST_DELTA_DIFF_MAJOR_VERSION) { + fprintf(stderr, "A later version is needed to apply this patch (on major version %u, but patch requests version %u).\n", LATEST_DELTA_DIFF_MAJOR_VERSION, majorDiffVersion); + return 1; + } + + BOOL usesNewTreeHash = MAJOR_VERSION_IS_AT_LEAST(majorDiffVersion, SUBeigeMajorVersion); + + NSString *expectedBeforeHash = usesNewTreeHash ? expectedNewBeforeHash : expectedBeforeHashv1; + NSString *expectedAfterHash = usesNewTreeHash ? expectedNewAfterHash : expectedAfterHashv1; if (!expectedBeforeHash || !expectedAfterHash) { fprintf(stderr, "Unable to find before-sha1 or after-sha1 metadata in delta. Giving up.\n"); return 1; } - fprintf(stderr, "Verifying source... "); - NSString *beforeHash = hashOfTree(source); - - if (![beforeHash isEqualToString:expectedBeforeHash]) { - fprintf(stderr, "Source doesn't have expected hash (%s != %s). Giving up.\n", [expectedBeforeHash UTF8String], [beforeHash UTF8String]); + if (verbose) { + fprintf(stderr, "Applying version %u.%u patch...\n", majorDiffVersion, minorDiffVersion); + fprintf(stderr, "Verifying source..."); + } + + NSString *beforeHash = hashOfTreeWithVersion(source, majorDiffVersion); + if (!beforeHash) { + fprintf(stderr, "\nUnable to calculate hash of tree %s\n", [source fileSystemRepresentation]); return 1; } - fprintf(stderr, "\nCopying files... "); - removeTree(destination); - copyTree(source, destination); + if (![beforeHash isEqualToString:expectedBeforeHash]) { + fprintf(stderr, "\nSource doesn't have expected hash (%s != %s). Giving up.\n", [expectedBeforeHash UTF8String], [beforeHash UTF8String]); + return 1; + } - fprintf(stderr, "\nPatching... "); + if (verbose) { + fprintf(stderr, "\nCopying files..."); + } + + if (!removeTree(destination)) { + fprintf(stderr, "\nFailed to remove %s\n", [destination fileSystemRepresentation]); + return 1; + } + if (!copyTree(source, destination)) { + fprintf(stderr, "\nFailed to copy %s to %s\n", [source fileSystemRepresentation], [destination fileSystemRepresentation]); + return 1; + } + + BOOL hasExtractKeyAvailable = MAJOR_VERSION_IS_AT_LEAST(majorDiffVersion, SUBeigeMajorVersion); + + if (verbose) { + fprintf(stderr, "\nPatching..."); + } + NSFileManager *fileManager = [[NSFileManager alloc] init]; xar_file_t file; xar_iter_t iter = xar_iter_new(); for (file = xar_file_first(x, iter); file; file = xar_file_next(iter)) { @@ -73,29 +141,86 @@ int applyBinaryDelta(NSString *source, NSString *destination, NSString *patchFil NSString *sourceFilePath = [source stringByAppendingPathComponent:path]; NSString *destinationFilePath = [destination stringByAppendingPathComponent:path]; + // Don't use -[NSFileManager fileExistsAtPath:] because it will follow symbolic links + BOOL fileExisted = verbose && [fileManager attributesOfItemAtPath:destinationFilePath error:nil]; + BOOL removedFile = NO; + const char *value; - if (!xar_prop_get(file, "delete", &value) || !xar_prop_get(file, "delete-then-extract", &value)) { - removeTree(destinationFilePath); - if (!xar_prop_get(file, "delete", &value)) + if (!xar_prop_get(file, DELETE_KEY, &value) || + (!hasExtractKeyAvailable && !xar_prop_get(file, DELETE_THEN_EXTRACT_OLD_KEY, &value))) { + if (!removeTree(destinationFilePath)) { + fprintf(stderr, "\n%s or %s: failed to remove %s\n", DELETE_KEY, DELETE_THEN_EXTRACT_OLD_KEY, [destination fileSystemRepresentation]); + return 1; + } + if (!hasExtractKeyAvailable && !xar_prop_get(file, DELETE_KEY, &value)) { + if (verbose) { + fprintf(stderr, "\n❌ %s %s", VERBOSE_DELETED, [path fileSystemRepresentation]); + } continue; + } + + removedFile = YES; } - if (!xar_prop_get(file, "binary-delta", &value)) - applyBinaryDeltaToFile(x, file, sourceFilePath, destinationFilePath); - else - xar_extract_tofile(x, file, [destinationFilePath fileSystemRepresentation]); + if (!xar_prop_get(file, BINARY_DELTA_KEY, &value)) { + if (!applyBinaryDeltaToFile(x, file, sourceFilePath, destinationFilePath)) { + fprintf(stderr, "\nUnable to patch %s to destination %s\n", [sourceFilePath fileSystemRepresentation], [destinationFilePath fileSystemRepresentation]); + return 1; + } + + if (verbose) { + fprintf(stderr, "\n🔨 %s %s", VERBOSE_PATCHED, [path fileSystemRepresentation]); + } + } else if ((hasExtractKeyAvailable && !xar_prop_get(file, EXTRACT_KEY, &value)) || + (!hasExtractKeyAvailable && xar_prop_get(file, MODIFY_PERMISSIONS_KEY, &value))) { // extract and permission modifications don't coexist + + if (xar_extract_tofile(x, file, [destinationFilePath fileSystemRepresentation]) != 0) { + fprintf(stderr, "\nUnable to extract file to %s\n", [destinationFilePath fileSystemRepresentation]); + return 1; + } + + if (verbose) { + if (fileExisted) { + fprintf(stderr, "\n✏️ %s %s", VERBOSE_UPDATED, [path fileSystemRepresentation]); + } else { + fprintf(stderr, "\n✅ %s %s", VERBOSE_ADDED, [path fileSystemRepresentation]); + } + } + } else if (verbose && removedFile) { + fprintf(stderr, "\n❌ %s %s", VERBOSE_DELETED, [path fileSystemRepresentation]); + } + + if (!xar_prop_get(file, MODIFY_PERMISSIONS_KEY, &value)) { + mode_t mode = (mode_t)[[NSString stringWithUTF8String:value] intValue]; + if (!modifyPermissions(destinationFilePath, mode)) { + fprintf(stderr, "\nUnable to modify permissions (%s) on file %s\n", value, [destinationFilePath fileSystemRepresentation]); + return 1; + } + + if (verbose) { + fprintf(stderr, "\n👮 %s %s (0%o)", VERBOSE_MODIFIED, [path fileSystemRepresentation], mode); + } + } } xar_close(x); - fprintf(stderr, "\nVerifying destination... "); - NSString *afterHash = hashOfTree(destination); + if (verbose) { + fprintf(stderr, "\nVerifying destination..."); + } + NSString *afterHash = hashOfTreeWithVersion(destination, majorDiffVersion); + if (!afterHash) { + fprintf(stderr, "\nUnable to calculate hash of tree %s\n", [destination fileSystemRepresentation]); + return 1; + } if (![afterHash isEqualToString:expectedAfterHash]) { - fprintf(stderr, "Destination doesn't have expected hash (%s != %s). Giving up.\n", [expectedAfterHash UTF8String], [afterHash UTF8String]); + fprintf(stderr, "\nDestination doesn't have expected hash (%s != %s). Giving up.\n", [expectedAfterHash UTF8String], [afterHash UTF8String]); removeTree(destination); return 1; } - fprintf(stderr, "\nDone!\n"); + if (verbose) { + fprintf(stderr, "\nDone!\n"); + } return 0; } diff --git a/Frameworks/Sparkle/Sparkle/SUBinaryDeltaCommon.h b/Frameworks/Sparkle/Sparkle/SUBinaryDeltaCommon.h index 4e330da7c..44a5d04e4 100644 --- a/Frameworks/Sparkle/Sparkle/SUBinaryDeltaCommon.h +++ b/Frameworks/Sparkle/Sparkle/SUBinaryDeltaCommon.h @@ -11,16 +11,71 @@ #include +#define PERMISSION_FLAGS (S_IRWXU | S_IRWXG | S_IRWXO | S_ISUID | S_ISGID | S_ISVTX) + +#define IS_VALID_PERMISSIONS(mode) \ + (((mode & PERMISSION_FLAGS) == 0755) || ((mode & PERMISSION_FLAGS) == 0644)) + +#define BINARY_DELTA_ATTRIBUTES_KEY "binary-delta-attributes" +#define MAJOR_DIFF_VERSION_KEY "major-version" +#define MINOR_DIFF_VERSION_KEY "minor-version" +#define BEFORE_TREE_SHA1_KEY "before-tree-sha1" +#define AFTER_TREE_SHA1_KEY "after-tree-sha1" +#define DELETE_KEY "delete" +#define EXTRACT_KEY "extract" +#define BINARY_DELTA_KEY "binary-delta" +#define MODIFY_PERMISSIONS_KEY "mod-permissions" + +// Properties no longer used in new patches +#define DELETE_THEN_EXTRACT_OLD_KEY "delete-then-extract" +#define BEFORE_TREE_SHA1_OLD_KEY "before-sha1" +#define AFTER_TREE_SHA1_OLD_KEY "after-sha1" + +#define VERBOSE_DELETED "Deleted" // file is deleted from the file system when applying a patch +#define VERBOSE_REMOVED "Removed" // file is set to be removed when creating a patch +#define VERBOSE_ADDED "Added" // file is added to the patch or file system +#define VERBOSE_DIFFED "Diffed" // file is diffed when creating a patch +#define VERBOSE_PATCHED "Patched" // file is patched when applying a patch +#define VERBOSE_UPDATED "Updated" // file's contents are updated +#define VERBOSE_MODIFIED "Modified" // file's metadata is modified + +#define MAJOR_VERSION_IS_AT_LEAST(actualMajor, expectedMajor) (actualMajor >= expectedMajor) + +// Each major version will be assigned a name of a color +// Changes that break backwards compatibility will have different major versions +// Changes that affect creating but not applying patches will have different minor versions + +typedef NS_ENUM(uint16_t, SUBinaryDeltaMajorVersion) +{ + SUAzureMajorVersion = 1, + SUBeigeMajorVersion = 2 +}; + +// Only keep track of the latest minor version for each major version +typedef NS_ENUM(uint16_t, SUBinaryDeltaMinorVersion) +{ + SUAzureMinorVersion = 1, + SUBeigeMinorVersion = 0, +}; + +#define FIRST_DELTA_DIFF_MAJOR_VERSION SUAzureMajorVersion +#define FIRST_DELTA_DIFF_MINOR_VERSION 0 + +#define LATEST_DELTA_DIFF_MAJOR_VERSION SUBeigeMajorVersion + @class NSString; @class NSData; -extern int binaryDeltaSupported(void); extern int compareFiles(const FTSENT **a, const FTSENT **b); -extern NSData *hashOfFile(FTSENT *ent); +extern NSData *hashOfFileContents(FTSENT *ent); +extern NSString *hashOfTreeWithVersion(NSString *path, uint16_t majorVersion); extern NSString *hashOfTree(NSString *path); -extern void removeTree(NSString *path); -extern void copyTree(NSString *source, NSString *dest); +extern BOOL removeTree(NSString *path); +extern BOOL copyTree(NSString *source, NSString *dest); +extern BOOL modifyPermissions(NSString *path, mode_t desiredPermissions); extern NSString *pathRelativeToDirectory(NSString *directory, NSString *path); NSString *temporaryFilename(NSString *base); +NSString *temporaryDirectory(NSString *base); NSString *stringWithFileSystemRepresentation(const char*); +SUBinaryDeltaMinorVersion latestMinorVersionForMajorVersion(SUBinaryDeltaMajorVersion majorVersion); #endif diff --git a/Frameworks/Sparkle/Sparkle/SUBinaryDeltaCommon.m b/Frameworks/Sparkle/Sparkle/SUBinaryDeltaCommon.m index 78ba2606b..9bde20c31 100644 --- a/Frameworks/Sparkle/Sparkle/SUBinaryDeltaCommon.m +++ b/Frameworks/Sparkle/Sparkle/SUBinaryDeltaCommon.m @@ -17,13 +17,6 @@ #include #include -int binaryDeltaSupported(void) -{ - // OS X 10.4 didn't include libxar, so we link against it weakly. - // This checks whether libxar is available at runtime. - return xar_close != 0; -} - int compareFiles(const FTSENT **a, const FTSENT **b) { return strcoll((*a)->fts_name, (*b)->fts_name); @@ -42,6 +35,17 @@ NSString *stringWithFileSystemRepresentation(const char *input) { return [[NSFileManager defaultManager] stringWithFileSystemRepresentation:input length:strlen(input)]; } +SUBinaryDeltaMinorVersion latestMinorVersionForMajorVersion(SUBinaryDeltaMajorVersion majorVersion) +{ + switch (majorVersion) { + case SUAzureMajorVersion: + return SUAzureMinorVersion; + case SUBeigeMajorVersion: + return SUBeigeMinorVersion; + } + return 0; +} + NSString *temporaryFilename(NSString *base) { NSString *template = [NSTemporaryDirectory() stringByAppendingPathComponent:[NSString stringWithFormat:@"%@.XXXXXXXXXX", base]]; @@ -63,6 +67,22 @@ NSString *temporaryFilename(NSString *base) return stringWithFileSystemRepresentation(buffer); } +NSString *temporaryDirectory(NSString *base) +{ + NSString *template = [NSTemporaryDirectory() stringByAppendingPathComponent:[NSString stringWithFormat:@"%@.XXXXXXXXXX", base]]; + NSMutableData *data = [NSMutableData data]; + [data appendBytes:template.fileSystemRepresentation length:strlen(template.fileSystemRepresentation) + 1]; + + char *buffer = data.mutableBytes; + char *templateResult = mkdtemp(buffer); + if (templateResult == NULL) { + perror("mkdtemp"); + return nil; + } + + return stringWithFileSystemRepresentation(templateResult); +} + static void _hashOfBuffer(unsigned char *hash, const char* buffer, ssize_t bufferLength) { assert(bufferLength >= 0 && bufferLength <= UINT32_MAX); @@ -72,59 +92,57 @@ static void _hashOfBuffer(unsigned char *hash, const char* buffer, ssize_t buffe CC_SHA1_Final(hash, &hashContext); } -static void _hashOfFile(unsigned char* hash, FTSENT *ent) +static BOOL _hashOfFileContents(unsigned char* hash, FTSENT *ent) { if (ent->fts_info == FTS_SL) { char linkDestination[MAXPATHLEN + 1]; ssize_t linkDestinationLength = readlink(ent->fts_path, linkDestination, MAXPATHLEN); if (linkDestinationLength < 0) { perror("readlink"); - return; + return NO; } _hashOfBuffer(hash, linkDestination, linkDestinationLength); - return; - } - - if (ent->fts_info == FTS_F) { + } else if (ent->fts_info == FTS_F) { int fileDescriptor = open(ent->fts_path, O_RDONLY); if (fileDescriptor == -1) { perror("open"); - return; + return NO; } ssize_t fileSize = ent->fts_statp->st_size; if (fileSize == 0) { _hashOfBuffer(hash, NULL, 0); - close(fileDescriptor); - return; + } else { + void *buffer = mmap(0, (size_t)fileSize, PROT_READ, MAP_FILE | MAP_PRIVATE, fileDescriptor, 0); + if (buffer == (void*)-1) { + close(fileDescriptor); + perror("mmap"); + return NO; + } + + _hashOfBuffer(hash, buffer, fileSize); + munmap(buffer, (size_t)fileSize); } - - void *buffer = mmap(0, (size_t)fileSize, PROT_READ, MAP_FILE | MAP_PRIVATE, fileDescriptor, 0); - if (buffer == (void*)-1) { - close(fileDescriptor); - perror("mmap"); - return; - } - - _hashOfBuffer(hash, buffer, fileSize); - munmap(buffer, (size_t)fileSize); close(fileDescriptor); - return; - } - - if (ent->fts_info == FTS_D) + } else if (ent->fts_info == FTS_D) { memset(hash, 0xdd, CC_SHA1_DIGEST_LENGTH); + } else { + return NO; + } + return YES; } -NSData *hashOfFile(FTSENT *ent) +NSData *hashOfFileContents(FTSENT *ent) { unsigned char fileHash[CC_SHA1_DIGEST_LENGTH]; - _hashOfFile(fileHash, ent); + if (!_hashOfFileContents(fileHash, ent)) { + return nil; + } return [NSData dataWithBytes:fileHash length:CC_SHA1_DIGEST_LENGTH]; } -NSString *hashOfTree(NSString *path) +NSString *hashOfTreeWithVersion(NSString *path, uint16_t majorVersion) { const char *sourcePaths[] = {[path fileSystemRepresentation], 0}; FTS *fts = fts_open((char* const*)sourcePaths, FTS_PHYSICAL | FTS_NOCHDIR, compareFiles); @@ -138,16 +156,34 @@ NSString *hashOfTree(NSString *path) FTSENT *ent = 0; while ((ent = fts_read(fts))) { - if (ent->fts_info != FTS_F && ent->fts_info != FTS_SL) + if (ent->fts_info != FTS_F && ent->fts_info != FTS_SL && ent->fts_info != FTS_D) + continue; + + if (ent->fts_info == FTS_D && !MAJOR_VERSION_IS_AT_LEAST(majorVersion, SUBeigeMajorVersion)) { + continue; + } + + NSString *relativePath = pathRelativeToDirectory(path, stringWithFileSystemRepresentation(ent->fts_path)); + if (relativePath.length == 0) continue; unsigned char fileHash[CC_SHA1_DIGEST_LENGTH]; - _hashOfFile(fileHash, ent); + if (!_hashOfFileContents(fileHash, ent)) { + return nil; + } CC_SHA1_Update(&hashContext, fileHash, sizeof(fileHash)); - NSString *relativePath = pathRelativeToDirectory(path, stringWithFileSystemRepresentation(ent->fts_path)); const char *relativePathBytes = [relativePath fileSystemRepresentation]; CC_SHA1_Update(&hashContext, relativePathBytes, (CC_LONG)strlen(relativePathBytes)); + + if (MAJOR_VERSION_IS_AT_LEAST(majorVersion, SUBeigeMajorVersion)) { + uint16_t mode = ent->fts_statp->st_mode; + uint16_t type = ent->fts_info; + uint16_t permissions = mode & PERMISSION_FLAGS; + + CC_SHA1_Update(&hashContext, &type, sizeof(type)); + CC_SHA1_Update(&hashContext, &permissions, sizeof(permissions)); + } } fts_close(fts); @@ -162,12 +198,41 @@ NSString *hashOfTree(NSString *path) return @(hexHash); } -void removeTree(NSString *path) +extern NSString *hashOfTree(NSString *path) { - [[NSFileManager defaultManager] removeItemAtPath:path error:nil]; + return hashOfTreeWithVersion(path, LATEST_DELTA_DIFF_MAJOR_VERSION); } -void copyTree(NSString *source, NSString *dest) +BOOL removeTree(NSString *path) { - [[NSFileManager defaultManager] copyItemAtPath:source toPath:dest error:nil]; + NSFileManager *fileManager = [NSFileManager defaultManager]; + // Don't use fileExistsForPath: because it will try to follow symbolic links + if (![fileManager attributesOfItemAtPath:path error:nil]) { + return YES; + } + return [fileManager removeItemAtPath:path error:nil]; +} + +BOOL copyTree(NSString *source, NSString *dest) +{ + return [[NSFileManager defaultManager] copyItemAtPath:source toPath:dest error:nil]; +} + +BOOL modifyPermissions(NSString *path, mode_t desiredPermissions) +{ + NSFileManager *fileManager = [NSFileManager defaultManager]; + NSDictionary *attributes = [fileManager attributesOfItemAtPath:path error:nil]; + if (!attributes) { + return NO; + } + NSNumber *permissions = attributes[NSFilePosixPermissions]; + if (!permissions) { + return NO; + } + mode_t newMode = ([permissions unsignedShortValue] & ~PERMISSION_FLAGS) | desiredPermissions; + int (*changeModeFunc)(const char *, mode_t) = [attributes[NSFileType] isEqualToString:NSFileTypeSymbolicLink] ? lchmod : chmod; + if (changeModeFunc([path fileSystemRepresentation], newMode) != 0) { + return NO; + } + return YES; } diff --git a/Frameworks/Sparkle/Sparkle/SUBinaryDeltaCreate.h b/Frameworks/Sparkle/Sparkle/SUBinaryDeltaCreate.h new file mode 100644 index 000000000..4afc22817 --- /dev/null +++ b/Frameworks/Sparkle/Sparkle/SUBinaryDeltaCreate.h @@ -0,0 +1,17 @@ +// +// SUBinaryDeltaCreate.m +// Sparkle +// +// Created by Mayur Pawashe on 4/9/15. +// Copyright (c) 2015 Sparkle Project. All rights reserved. +// + +#ifndef SUBINARYDELTACREATE_H +#define SUBINARYDELTACREATE_H + +#import "SUBinaryDeltaCommon.h" + +@class NSString; +int createBinaryDelta(NSString *source, NSString *destination, NSString *patchFile, SUBinaryDeltaMajorVersion majorVersion, BOOL verbose); + +#endif diff --git a/Frameworks/Sparkle/Sparkle/SUBinaryDeltaCreate.m b/Frameworks/Sparkle/Sparkle/SUBinaryDeltaCreate.m new file mode 100644 index 000000000..a477d6740 --- /dev/null +++ b/Frameworks/Sparkle/Sparkle/SUBinaryDeltaCreate.m @@ -0,0 +1,462 @@ +// +// SUBinaryDeltaCreate.m +// Sparkle +// +// Created by Mayur Pawashe on 4/9/15. +// Copyright (c) 2015 Sparkle Project. All rights reserved. +// + +#import "SUBinaryDeltaCreate.h" +#import +#include "SUBinaryDeltaCommon.h" +#import +#include +#include +#include +#include +#include +#include +#include +#include +#include + +extern int bsdiff(int argc, const char **argv); + +@interface CreateBinaryDeltaOperation : NSOperation +@property (copy) NSString *relativePath; +@property (strong) NSString *resultPath; +@property (strong) NSNumber *oldPermissions; +@property (strong) NSNumber *permissions; +@property (strong) NSString *_fromPath; +@property (strong) NSString *_toPath; +- (id)initWithRelativePath:(NSString *)relativePath oldTree:(NSString *)oldTree newTree:(NSString *)newTree oldPermissions:(NSNumber *)oldPermissions newPermissions:(NSNumber *)permissions; +@end + +@implementation CreateBinaryDeltaOperation +@synthesize relativePath = _relativePath; +@synthesize resultPath = _resultPath; +@synthesize oldPermissions = _oldPermissions; +@synthesize permissions = _permissions; +@synthesize _fromPath = _fromPath; +@synthesize _toPath = _toPath; + +- (id)initWithRelativePath:(NSString *)relativePath oldTree:(NSString *)oldTree newTree:(NSString *)newTree oldPermissions:(NSNumber *)oldPermissions newPermissions:(NSNumber *)permissions +{ + if ((self = [super init])) { + self.relativePath = relativePath; + self.oldPermissions = oldPermissions; + self.permissions = permissions; + self._fromPath = [oldTree stringByAppendingPathComponent:relativePath]; + self._toPath = [newTree stringByAppendingPathComponent:relativePath]; + } + return self; +} + +- (void)main +{ + NSString *temporaryFile = temporaryFilename(@"BinaryDelta"); + const char *argv[] = {"/usr/bin/bsdiff", [self._fromPath fileSystemRepresentation], [self._toPath fileSystemRepresentation], [temporaryFile fileSystemRepresentation]}; + int result = bsdiff(4, argv); + if (!result) + self.resultPath = temporaryFile; +} + +@end + +#define INFO_HASH_KEY @"hash" +#define INFO_TYPE_KEY @"type" +#define INFO_PERMISSIONS_KEY @"permissions" +#define INFO_SIZE_KEY @"size" + +static NSDictionary *infoForFile(FTSENT *ent) +{ + NSData *hash = hashOfFileContents(ent); + if (!hash) { + return nil; + } + + off_t size = (ent->fts_info != FTS_D) ? ent->fts_statp->st_size : 0; + + assert(ent->fts_statp != NULL); + + mode_t permissions = ent->fts_statp->st_mode & PERMISSION_FLAGS; + + return @{INFO_HASH_KEY: hash, INFO_TYPE_KEY: @(ent->fts_info), INFO_PERMISSIONS_KEY : @(permissions), INFO_SIZE_KEY: @(size)}; +} + +static bool aclExists(const FTSENT *ent) +{ + // OS X does not currently support ACLs for symlinks + if (ent->fts_info == FTS_SL) { + return NO; + } + + acl_t acl = acl_get_link_np(ent->fts_path, ACL_TYPE_EXTENDED); + if (acl != NULL) { + acl_entry_t entry; + int result = acl_get_entry(acl, ACL_FIRST_ENTRY, &entry); + assert(acl_free((void *)acl) == 0); + return (result == 0); + } + return false; +} + +static NSString *absolutePath(NSString *path) +{ + NSURL *url = [[NSURL alloc] initFileURLWithPath:path]; + return [[url absoluteURL] path]; +} + +static NSString *temporaryPatchFile(NSString *patchFile) +{ + NSString *path = absolutePath(patchFile); + NSString *directory = [path stringByDeletingLastPathComponent]; + NSString *file = [path lastPathComponent]; + return [NSString stringWithFormat:@"%@/.%@.tmp", directory, file]; +} + +#define MIN_FILE_SIZE_FOR_CREATING_DELTA 4096 + +static BOOL shouldSkipDeltaCompression(NSDictionary* originalInfo, NSDictionary *newInfo) +{ + unsigned long long fileSize = [newInfo[INFO_SIZE_KEY] unsignedLongLongValue]; + if (fileSize < MIN_FILE_SIZE_FOR_CREATING_DELTA) { + return YES; + } + + if (!originalInfo) { + return YES; + } + + if ([originalInfo[INFO_TYPE_KEY] unsignedShortValue] != [newInfo[INFO_TYPE_KEY] unsignedShortValue]) { + return YES; + } + + if ([originalInfo[INFO_HASH_KEY] isEqual:newInfo[INFO_HASH_KEY]]) { + // this is possible if just the permissions have changed + return YES; + } + + return NO; +} + +static BOOL shouldDeleteThenExtract(NSDictionary* originalInfo, NSDictionary *newInfo) +{ + if (!originalInfo) { + return NO; + } + + if ([originalInfo[INFO_TYPE_KEY] unsignedShortValue] != [newInfo[INFO_TYPE_KEY] unsignedShortValue]) { + return YES; + } + + return NO; +} + +static BOOL shouldSkipExtracting(NSDictionary *originalInfo, NSDictionary *newInfo) +{ + if (!originalInfo) { + return NO; + } + + if ([originalInfo[INFO_TYPE_KEY] unsignedShortValue] != [newInfo[INFO_TYPE_KEY] unsignedShortValue]) { + return NO; + } + + if (![originalInfo[INFO_HASH_KEY] isEqual:newInfo[INFO_HASH_KEY]]) { + return NO; + } + + return YES; +} + +static BOOL shouldChangePermissions(NSDictionary *originalInfo, NSDictionary *newInfo) +{ + if (!originalInfo) { + return NO; + } + + if ([originalInfo[INFO_TYPE_KEY] unsignedShortValue] != [newInfo[INFO_TYPE_KEY] unsignedShortValue]) { + return NO; + } + + if ([originalInfo[INFO_PERMISSIONS_KEY] unsignedShortValue] == [newInfo[INFO_PERMISSIONS_KEY] unsignedShortValue]) { + return NO; + } + + return YES; +} + +int createBinaryDelta(NSString *source, NSString *destination, NSString *patchFile, SUBinaryDeltaMajorVersion majorVersion, BOOL verbose) +{ + if (majorVersion < FIRST_DELTA_DIFF_MAJOR_VERSION) { + fprintf(stderr, "Version provided (%u) is not valid\n", majorVersion); + return 1; + } + + if (majorVersion > LATEST_DELTA_DIFF_MAJOR_VERSION) { + fprintf(stderr, "This program is too old to create a version %u patch, or the version number provided is invalid\n", majorVersion); + return 1; + } + + SUBinaryDeltaMinorVersion minorVersion = latestMinorVersionForMajorVersion(majorVersion); + + NSMutableDictionary *originalTreeState = [NSMutableDictionary dictionary]; + + const char *sourcePaths[] = {[source fileSystemRepresentation], 0}; + FTS *fts = fts_open((char* const*)sourcePaths, FTS_PHYSICAL | FTS_NOCHDIR, compareFiles); + if (!fts) { + perror("fts_open"); + return 1; + } + + if (verbose) { + fprintf(stderr, "Creating version %u.%u patch...\n", majorVersion, minorVersion); + fprintf(stderr, "Processing %s...", [source fileSystemRepresentation]); + } + + FTSENT *ent = 0; + while ((ent = fts_read(fts))) { + if (ent->fts_info != FTS_F && ent->fts_info != FTS_SL && ent->fts_info != FTS_D) { + continue; + } + + NSString *key = pathRelativeToDirectory(source, stringWithFileSystemRepresentation(ent->fts_path)); + if (![key length]) { + continue; + } + + NSDictionary *info = infoForFile(ent); + if (!info) { + fprintf(stderr, "\nFailed to retrieve info for file %s\n", ent->fts_path); + return 1; + } + originalTreeState[key] = info; + + if (aclExists(ent)) { + fprintf(stderr, "\nDiffing ACLs are not supported. Detected ACL in before-tree on file %s\n", ent->fts_path); + return 1; + } + } + fts_close(fts); + + NSString *beforeHash = hashOfTreeWithVersion(source, majorVersion); + + if (!beforeHash) { + fprintf(stderr, "\nFailed to generate hash for tree %s\n", [source fileSystemRepresentation]); + return 1; + } + + NSMutableDictionary *newTreeState = [NSMutableDictionary dictionary]; + for (NSString *key in originalTreeState) + { + newTreeState[key] = [NSNull null]; + } + + if (verbose) { + fprintf(stderr, "\nProcessing %s...", [destination fileSystemRepresentation]); + } + + sourcePaths[0] = [destination fileSystemRepresentation]; + fts = fts_open((char* const*)sourcePaths, FTS_PHYSICAL | FTS_NOCHDIR, compareFiles); + if (!fts) { + perror("fts_open"); + return 1; + } + + + while ((ent = fts_read(fts))) { + if (ent->fts_info != FTS_F && ent->fts_info != FTS_SL && ent->fts_info != FTS_D) { + continue; + } + + NSString *key = pathRelativeToDirectory(destination, stringWithFileSystemRepresentation(ent->fts_path)); + if (![key length]) { + continue; + } + + NSDictionary *info = infoForFile(ent); + if (!info) { + fprintf(stderr, "\nFailed to retrieve info from file %s\n", ent->fts_path); + return 1; + } + + // We should validate permissions and ACLs even if we don't store the info in the diff in the case of ACLs, + // or in the case of permissions if the patch version is 1 + + mode_t permissions = [info[INFO_PERMISSIONS_KEY] unsignedShortValue]; + if (!IS_VALID_PERMISSIONS(permissions)) { + fprintf(stderr, "\nInvalid file permissions after-tree on file %s\nOnly permissions with modes 0755 and 0644 are supported\n", ent->fts_path); + return 1; + } + + if (aclExists(ent)) { + fprintf(stderr, "\nDiffing ACLs are not supported. Detected ACL in after-tree on file %s\n", ent->fts_path); + return 1; + } + + NSDictionary *oldInfo = originalTreeState[key]; + + if ([info isEqual:oldInfo]) { + [newTreeState removeObjectForKey:key]; + } else { + newTreeState[key] = info; + + if (oldInfo && [oldInfo[INFO_TYPE_KEY] unsignedShortValue] == FTS_D && [info[INFO_TYPE_KEY] unsignedShortValue] != FTS_D) { + NSArray *parentPathComponents = key.pathComponents; + + for (NSString *childPath in originalTreeState) { + NSArray *childPathComponents = childPath.pathComponents; + if (childPathComponents.count > parentPathComponents.count && + [parentPathComponents isEqualToArray:[childPathComponents subarrayWithRange:NSMakeRange(0, parentPathComponents.count)]]) { + [newTreeState removeObjectForKey:childPath]; + } + } + } + } + } + fts_close(fts); + + NSString *afterHash = hashOfTreeWithVersion(destination, majorVersion); + if (!afterHash) { + fprintf(stderr, "\nFailed to generate hash for tree %s\n", [destination fileSystemRepresentation]); + return 1; + } + + if (verbose) { + fprintf(stderr, "\nGenerating delta..."); + } + + NSString *temporaryFile = temporaryPatchFile(patchFile); + xar_t x = xar_open([temporaryFile fileSystemRepresentation], WRITE); + xar_opt_set(x, XAR_OPT_COMPRESSION, "bzip2"); + + xar_subdoc_t attributes = xar_subdoc_new(x, BINARY_DELTA_ATTRIBUTES_KEY); + + xar_subdoc_prop_set(attributes, MAJOR_DIFF_VERSION_KEY, [[NSString stringWithFormat:@"%u", majorVersion] UTF8String]); + xar_subdoc_prop_set(attributes, MINOR_DIFF_VERSION_KEY, [[NSString stringWithFormat:@"%u", minorVersion] UTF8String]); + + // Version 1 patches don't have a major or minor version field, so we need to differentiate between the hash keys + const char *beforeHashKey = + MAJOR_VERSION_IS_AT_LEAST(majorVersion, SUBeigeMajorVersion) ? BEFORE_TREE_SHA1_KEY : BEFORE_TREE_SHA1_OLD_KEY; + const char *afterHashKey = + MAJOR_VERSION_IS_AT_LEAST(majorVersion, SUBeigeMajorVersion) ? AFTER_TREE_SHA1_KEY : AFTER_TREE_SHA1_OLD_KEY; + + xar_subdoc_prop_set(attributes, beforeHashKey, [beforeHash UTF8String]); + xar_subdoc_prop_set(attributes, afterHashKey, [afterHash UTF8String]); + + NSOperationQueue *deltaQueue = [[NSOperationQueue alloc] init]; + NSMutableArray *deltaOperations = [NSMutableArray array]; + + // Sort the keys by preferring the ones from the original tree to appear first + // We want to enforce deleting before extracting in the case paths differ only by case + NSArray *keys = [[newTreeState allKeys] sortedArrayUsingComparator:^NSComparisonResult(NSString *key1, NSString *key2) { + NSComparisonResult insensitiveCompareResult = [key1 caseInsensitiveCompare:key2]; + if (insensitiveCompareResult != NSOrderedSame) { + return insensitiveCompareResult; + } + + return originalTreeState[key1] ? NSOrderedAscending : NSOrderedDescending; + }]; + for (NSString* key in keys) { + id value = [newTreeState valueForKey:key]; + + if ([value isEqual:[NSNull null]]) { + xar_file_t newFile = xar_add_frombuffer(x, 0, [key fileSystemRepresentation], (char *)"", 1); + assert(newFile); + xar_prop_set(newFile, DELETE_KEY, "true"); + + if (verbose) { + fprintf(stderr, "\n❌ %s %s", VERBOSE_REMOVED, [key fileSystemRepresentation]); + } + continue; + } + + NSDictionary *originalInfo = originalTreeState[key]; + NSDictionary *newInfo = newTreeState[key]; + if (shouldSkipDeltaCompression(originalInfo, newInfo)) { + if (MAJOR_VERSION_IS_AT_LEAST(majorVersion, SUBeigeMajorVersion) && shouldSkipExtracting(originalInfo, newInfo)) { + if (shouldChangePermissions(originalInfo, newInfo)) { + xar_file_t newFile = xar_add_frombuffer(x, 0, [key fileSystemRepresentation], (char *)"", 1); + assert(newFile); + xar_prop_set(newFile, MODIFY_PERMISSIONS_KEY, [[NSString stringWithFormat:@"%u", [newInfo[INFO_PERMISSIONS_KEY] unsignedShortValue]] UTF8String]); + + if (verbose) { + fprintf(stderr, "\n👮 %s %s (0%o -> 0%o)", VERBOSE_MODIFIED, [key fileSystemRepresentation], [originalInfo[INFO_PERMISSIONS_KEY] unsignedShortValue], [newInfo[INFO_PERMISSIONS_KEY] unsignedShortValue]); + } + } + } else { + NSString *path = [destination stringByAppendingPathComponent:key]; + xar_file_t newFile = xar_add_frompath(x, 0, [key fileSystemRepresentation], [path fileSystemRepresentation]); + assert(newFile); + + if (shouldDeleteThenExtract(originalInfo, newInfo)) { + if (MAJOR_VERSION_IS_AT_LEAST(majorVersion, SUBeigeMajorVersion)) { + xar_prop_set(newFile, DELETE_KEY, "true"); + } else { + xar_prop_set(newFile, DELETE_THEN_EXTRACT_OLD_KEY, "true"); + } + } + + if (MAJOR_VERSION_IS_AT_LEAST(majorVersion, SUBeigeMajorVersion)) { + xar_prop_set(newFile, EXTRACT_KEY, "true"); + } + + if (verbose) { + if (originalInfo) { + fprintf(stderr, "\n✏️ %s %s", VERBOSE_UPDATED, [key fileSystemRepresentation]); + } else { + fprintf(stderr, "\n✅ %s %s", VERBOSE_ADDED, [key fileSystemRepresentation]); + } + } + } + } else { + NSNumber *permissions = + (MAJOR_VERSION_IS_AT_LEAST(majorVersion, SUBeigeMajorVersion) && shouldChangePermissions(originalInfo, newInfo)) ? + newInfo[INFO_PERMISSIONS_KEY] : + nil; + CreateBinaryDeltaOperation *operation = [[CreateBinaryDeltaOperation alloc] initWithRelativePath:key oldTree:source newTree:destination oldPermissions:originalInfo[INFO_PERMISSIONS_KEY] newPermissions:permissions]; + [deltaQueue addOperation:operation]; + [deltaOperations addObject:operation]; + } + } + + [deltaQueue waitUntilAllOperationsAreFinished]; + + for (CreateBinaryDeltaOperation *operation in deltaOperations) { + NSString *resultPath = [operation resultPath]; + if (!resultPath) { + fprintf(stderr, "\nFailed to create patch from source %s and destination %s\n", [[operation relativePath] fileSystemRepresentation], [resultPath fileSystemRepresentation]); + return 1; + } + + if (verbose) { + fprintf(stderr, "\n🔨 %s %s", VERBOSE_DIFFED, [[operation relativePath] fileSystemRepresentation]); + } + + xar_file_t newFile = xar_add_frompath(x, 0, [[operation relativePath] fileSystemRepresentation], [resultPath fileSystemRepresentation]); + assert(newFile); + xar_prop_set(newFile, BINARY_DELTA_KEY, "true"); + unlink([resultPath fileSystemRepresentation]); + + if (operation.permissions) { + xar_prop_set(newFile, MODIFY_PERMISSIONS_KEY, [[NSString stringWithFormat:@"%u", [operation.permissions unsignedShortValue]] UTF8String]); + + if (verbose) { + fprintf(stderr, "\n👮 %s %s (0%o -> 0%o)", VERBOSE_MODIFIED, [[operation relativePath] fileSystemRepresentation], operation.oldPermissions.unsignedShortValue, operation.permissions.unsignedShortValue); + } + } + } + + xar_close(x); + + unlink([patchFile fileSystemRepresentation]); + link([temporaryFile fileSystemRepresentation], [patchFile fileSystemRepresentation]); + unlink([temporaryFile fileSystemRepresentation]); + + if (verbose) { + fprintf(stderr, "\nDone!\n"); + } + + return 0; +} diff --git a/Frameworks/Sparkle/Sparkle/SUBinaryDeltaTool.m b/Frameworks/Sparkle/Sparkle/SUBinaryDeltaTool.m index a09d8c6d5..804663dd8 100644 --- a/Frameworks/Sparkle/Sparkle/SUBinaryDeltaTool.m +++ b/Frameworks/Sparkle/Sparkle/SUBinaryDeltaTool.m @@ -6,275 +6,213 @@ // Copyright 2009 Mark Rowe. All rights reserved. // -#define _DARWIN_NO_64_BIT_INODE 1 - -#include "SUBinaryDeltaCommon.h" #include "SUBinaryDeltaApply.h" -#include +#include "SUBinaryDeltaCreate.h" +#import "SUBinaryDeltaCommon.h" #include -#include -#include -#include -#include -#include -#include -#include -#include #include -extern int bsdiff(int argc, const char **argv); +#define VERBOSE_FLAG @"--verbose" +#define VERSION_FLAG @"--version" -@interface CreateBinaryDeltaOperation : NSOperation -@property (copy) NSString *relativePath; -@property (strong) NSString *resultPath; -@property (strong) NSString *_fromPath; -@property (strong) NSString *_toPath; -- (id)initWithRelativePath:(NSString *)relativePath oldTree:(NSString *)oldTree newTree:(NSString *)newTree; -@end +#define CREATE_COMMAND @"create" +#define APPLY_COMMAND @"apply" +#define VERSION_COMMAND @"version" +#define VERSION_ALTERNATE_COMMAND @"--version" -@implementation CreateBinaryDeltaOperation -@synthesize relativePath = _relativePath; -@synthesize resultPath = _resultPath; -@synthesize _fromPath = _fromPath; -@synthesize _toPath = _toPath; - -- (id)initWithRelativePath:(NSString *)relativePath oldTree:(NSString *)oldTree newTree:(NSString *)newTree +static void printUsage(NSString *programName) { - if ((self = [super init])) { - self.relativePath = relativePath; - self._fromPath = [oldTree stringByAppendingPathComponent:relativePath]; - self._toPath = [newTree stringByAppendingPathComponent:relativePath]; + fprintf(stderr, "Usage:\n"); + fprintf(stderr, "%s create [--verbose] [--version=] \n", [programName UTF8String]); + fprintf(stderr, "%s apply [--verbose] \n", [programName UTF8String]); + fprintf(stderr, "%s version []\n", [programName UTF8String]); +} + +static int runCreateCommand(NSString *programName, NSArray *args) +{ + if (args.count < 3 || args.count > 5) { + printUsage(programName); + return 1; } - return self; + + NSUInteger numberOflagsFound = 0; + NSUInteger verboseIndex = [args indexOfObject:VERBOSE_FLAG]; + NSUInteger versionIndex = NSNotFound; + for (NSUInteger argumentIndex = 0; argumentIndex < args.count; ++argumentIndex) { + if ([args[argumentIndex] hasPrefix:VERSION_FLAG]) { + versionIndex = argumentIndex; + break; + } + } + + if (verboseIndex != NSNotFound) { + ++numberOflagsFound; + } + if (versionIndex != NSNotFound) { + ++numberOflagsFound; + } + + if (args.count - numberOflagsFound < 3) { + printUsage(programName); + return 1; + } + + BOOL verbose = (verboseIndex != NSNotFound); + NSString *versionField = (versionIndex != NSNotFound) ? args[versionIndex] : nil; + + NSArray *versionComponents = nil; + if (versionField) { + versionComponents = [versionField componentsSeparatedByString:@"="]; + if (versionComponents.count != 2) { + printUsage(programName); + return 1; + } + } + + SUBinaryDeltaMajorVersion patchVersion = + !versionComponents ? + LATEST_DELTA_DIFF_MAJOR_VERSION : + (SUBinaryDeltaMajorVersion)[[versionComponents[1] componentsSeparatedByString:@"."][0] intValue]; // ignore minor version if provided + + NSMutableArray *fileArgs = [NSMutableArray array]; + for (NSString *argument in args) { + if (![argument hasPrefix:VERSION_FLAG] && ![argument isEqualToString:VERBOSE_FLAG]) { + [fileArgs addObject:argument]; + } + } + + if (fileArgs.count != 3) { + printUsage(programName); + return 1; + } + + BOOL isDirectory; + if (![[NSFileManager defaultManager] fileExistsAtPath:fileArgs[0] isDirectory:&isDirectory] || !isDirectory) { + printUsage(programName); + fprintf(stderr, "Error: before-tree must be a directory\n"); + return 1; + } + + if (![[NSFileManager defaultManager] fileExistsAtPath:fileArgs[1] isDirectory:&isDirectory] || !isDirectory) { + printUsage(programName); + fprintf(stderr, "Error: after-tree must be a directory\n"); + return 1; + } + + return createBinaryDelta(fileArgs[0], fileArgs[1], fileArgs[2], patchVersion, verbose); } -- (void)main +static int runApplyCommand(NSString *programName, NSArray *args) { - NSString *temporaryFile = temporaryFilename(@"BinaryDelta"); - const char *argv[] = {"/usr/bin/bsdiff", [self._fromPath fileSystemRepresentation], [self._toPath fileSystemRepresentation], [temporaryFile fileSystemRepresentation]}; - int result = bsdiff(4, argv); - if (!result) - self.resultPath = temporaryFile; + if (args.count < 3 || args.count > 4) { + printUsage(programName); + return 1; + } + + BOOL verbose = [args containsObject:VERBOSE_FLAG]; + + if (args.count == 4 && !verbose) { + printUsage(programName); + return 1; + } + + NSMutableArray *fileArgs = [NSMutableArray array]; + for (NSString *argument in args) { + if (![argument isEqualToString:VERBOSE_FLAG]) { + [fileArgs addObject:argument]; + } + } + + if (fileArgs.count != 3) { + printUsage(programName); + return 1; + } + + BOOL isDirectory; + if (![[NSFileManager defaultManager] fileExistsAtPath:fileArgs[0] isDirectory:&isDirectory] || !isDirectory) { + printUsage(programName); + fprintf(stderr, "Error: before-tree must be a directory\n"); + return 1; + } + + if (![[NSFileManager defaultManager] fileExistsAtPath:fileArgs[2] isDirectory:&isDirectory] || isDirectory) { + printUsage(programName); + fprintf(stderr, "Error: patch-file must be a file %d\n", isDirectory); + return 1; + } + + return applyBinaryDelta(fileArgs[0], fileArgs[1], fileArgs[2], verbose); } -@end - -static NSDictionary *infoForFile(FTSENT *ent) +static int runVersionCommand(NSString *programName, NSArray *args) { - NSData *hash = hashOfFile(ent); - NSNumber *size = @0; - if (ent->fts_info != FTS_D) { - size = @(ent->fts_statp->st_size); - } - return @{@"hash": hash, @"type": @(ent->fts_info), @"size": size}; -} - -static NSString *absolutePath(NSString *path) -{ - NSURL *url = [[NSURL alloc] initFileURLWithPath:path]; - return [[url absoluteURL] path]; -} - -static NSString *temporaryPatchFile(NSString *patchFile) -{ - NSString *path = absolutePath(patchFile); - NSString *directory = [path stringByDeletingLastPathComponent]; - NSString *file = [path lastPathComponent]; - return [NSString stringWithFormat:@"%@/.%@.tmp", directory, file]; -} - -static BOOL shouldSkipDeltaCompression(NSString * __unused key, NSDictionary* originalInfo, NSDictionary *newInfo) -{ - unsigned long long fileSize = [newInfo[@"size"] unsignedLongLongValue]; - if (fileSize < 4096) { - return YES; - } - - if (!originalInfo) { - return YES; - } - - if ([originalInfo[@"type"] unsignedShortValue] != [newInfo[@"type"] unsignedShortValue]) { - return YES; - } - - return NO; -} - -static BOOL shouldDeleteThenExtract(NSString * __unused key, NSDictionary* originalInfo, NSDictionary *newInfo) -{ - if (!originalInfo) { - return NO; - } - - if ([originalInfo[@"type"] unsignedShortValue] != [newInfo[@"type"] unsignedShortValue]) { - return YES; - } - - return NO; + if (args.count > 1) { + printUsage(programName); + return 1; + } + + if (args.count == 0) { + fprintf(stdout, "%u.%u\n", LATEST_DELTA_DIFF_MAJOR_VERSION, latestMinorVersionForMajorVersion(LATEST_DELTA_DIFF_MAJOR_VERSION)); + } else { + NSString *patchFile = args[0]; + xar_t x = xar_open([patchFile fileSystemRepresentation], READ); + if (!x) { + fprintf(stderr, "Unable to open patch %s\n", [patchFile fileSystemRepresentation]); + return 1; + } + + SUBinaryDeltaMajorVersion majorDiffVersion = FIRST_DELTA_DIFF_MAJOR_VERSION; + SUBinaryDeltaMinorVersion minorDiffVersion = FIRST_DELTA_DIFF_MINOR_VERSION; + + xar_subdoc_t subdoc; + for (subdoc = xar_subdoc_first(x); subdoc; subdoc = xar_subdoc_next(subdoc)) { + if (!strcmp(xar_subdoc_name(subdoc), BINARY_DELTA_ATTRIBUTES_KEY)) { + const char *value = 0; + + // available in version 2.0 or later + xar_subdoc_prop_get(subdoc, MAJOR_DIFF_VERSION_KEY, &value); + if (value) + majorDiffVersion = (SUBinaryDeltaMajorVersion)[@(value) intValue]; + + // available in version 2.0 or later + xar_subdoc_prop_get(subdoc, MINOR_DIFF_VERSION_KEY, &value); + if (value) + minorDiffVersion = (SUBinaryDeltaMinorVersion)[@(value) intValue]; + } + } + + fprintf(stdout, "%u.%u\n", majorDiffVersion, minorDiffVersion); + } + + return 0; } int main(int __unused argc, char __unused *argv[]) { @autoreleasepool { NSArray *args = [[NSProcessInfo processInfo] arguments]; - if (args.count != 5) { - usage: - fprintf(stderr, "Usage: BinaryDelta [create | apply] before-tree after-tree patch-file\n"); + NSString *programName = [args[0] lastPathComponent]; + + if (args.count < 2) { + printUsage(programName); return 1; } NSString *command = args[1]; - NSString *oldPath = args[2]; - NSString *newPath = args[3]; - NSString *patchFile = args[4]; - - BOOL isDirectory; - [[NSFileManager defaultManager] fileExistsAtPath:oldPath isDirectory:&isDirectory]; - if (!isDirectory) { - fprintf(stderr, "Usage: before-tree must be a directory\n"); - return 1; + NSArray *commandArguments = [args subarrayWithRange:NSMakeRange(2, args.count - 2)]; + + int result; + if ([command isEqualToString:CREATE_COMMAND]) { + result = runCreateCommand(programName, commandArguments); + } else if ([command isEqualToString:APPLY_COMMAND]) { + result = runApplyCommand(programName, commandArguments); + } else if ([command isEqualToString:VERSION_COMMAND] || [command isEqualToString:VERSION_ALTERNATE_COMMAND]) { + result = runVersionCommand(programName, commandArguments); + } else { + result = 1; + printUsage(programName); } - - [[NSFileManager defaultManager] fileExistsAtPath:newPath isDirectory:&isDirectory]; - if (!isDirectory) { - fprintf(stderr, "Usage: after-tree must be a directory\n"); - return 1; - } - - if ([command isEqualToString:@"apply"]) { - int result = applyBinaryDelta(oldPath, newPath, patchFile); - return result; - } - if (![command isEqualToString:@"create"]) { - goto usage; - } - - NSMutableDictionary *originalTreeState = [NSMutableDictionary dictionary]; - - const char *sourcePaths[] = {[oldPath fileSystemRepresentation], 0}; - FTS *fts = fts_open((char* const*)sourcePaths, FTS_PHYSICAL | FTS_NOCHDIR, compareFiles); - if (!fts) { - perror("fts_open"); - return 1; - } - - fprintf(stderr, "Processing %s...", [oldPath UTF8String]); - FTSENT *ent = 0; - while ((ent = fts_read(fts))) { - if (ent->fts_info != FTS_F && ent->fts_info != FTS_SL && ent->fts_info != FTS_D) { - continue; - } - - NSString *key = pathRelativeToDirectory(oldPath, stringWithFileSystemRepresentation(ent->fts_path)); - if (![key length]) { - continue; - } - - NSDictionary *info = infoForFile(ent); - originalTreeState[key] = info; - } - fts_close(fts); - - NSString *beforeHash = hashOfTree(oldPath); - - NSMutableDictionary *newTreeState = [NSMutableDictionary dictionary]; - for (NSString *key in originalTreeState) - { - newTreeState[key] = [NSNull null]; - } - - fprintf(stderr, "\nProcessing %s... ", [newPath UTF8String]); - sourcePaths[0] = [newPath fileSystemRepresentation]; - fts = fts_open((char* const*)sourcePaths, FTS_PHYSICAL | FTS_NOCHDIR, compareFiles); - if (!fts) { - perror("fts_open"); - return 1; - } - - - while ((ent = fts_read(fts))) { - if (ent->fts_info != FTS_F && ent->fts_info != FTS_SL && ent->fts_info != FTS_D) { - continue; - } - - NSString *key = pathRelativeToDirectory(newPath, stringWithFileSystemRepresentation(ent->fts_path)); - if (![key length]) { - continue; - } - - NSDictionary *info = infoForFile(ent); - NSDictionary *oldInfo = originalTreeState[key]; - - if ([info isEqual:oldInfo]) - [newTreeState removeObjectForKey:key]; - else - newTreeState[key] = info; - } - fts_close(fts); - - NSString *afterHash = hashOfTree(newPath); - - fprintf(stderr, "\nGenerating delta... "); - - NSString *temporaryFile = temporaryPatchFile(patchFile); - xar_t x = xar_open([temporaryFile fileSystemRepresentation], WRITE); - xar_opt_set(x, XAR_OPT_COMPRESSION, "bzip2"); - xar_subdoc_t attributes = xar_subdoc_new(x, "binary-delta-attributes"); - xar_subdoc_prop_set(attributes, "before-sha1", [beforeHash UTF8String]); - xar_subdoc_prop_set(attributes, "after-sha1", [afterHash UTF8String]); - - NSOperationQueue *deltaQueue = [[NSOperationQueue alloc] init]; - NSMutableArray *deltaOperations = [NSMutableArray array]; - -#pragma clang diagnostic push -#pragma clang diagnostic ignored "-Wselector" - // Xcode 5.1.1: compare: is clearly declared, must warn due to a compiler bug? - NSArray *keys = [[newTreeState allKeys] sortedArrayUsingSelector:@selector(compare:)]; -#pragma clang diagnostic pop - for (NSString* key in keys) { - id value = [newTreeState valueForKey:key]; - - if ([value isEqual:[NSNull null]]) { - xar_file_t newFile = xar_add_frombuffer(x, 0, [key fileSystemRepresentation], (char *)"", 1); - assert(newFile); - xar_prop_set(newFile, "delete", "true"); - continue; - } - - NSDictionary *originalInfo = originalTreeState[key]; - NSDictionary *newInfo = newTreeState[key]; - if (shouldSkipDeltaCompression(key, originalInfo, newInfo)) { - NSString *path = [newPath stringByAppendingPathComponent:key]; - xar_file_t newFile = xar_add_frompath(x, 0, [key fileSystemRepresentation], [path fileSystemRepresentation]); - assert(newFile); - if (shouldDeleteThenExtract(key, originalInfo, newInfo)) { - xar_prop_set(newFile, "delete-then-extract", "true"); - } - } else { - CreateBinaryDeltaOperation *operation = [[CreateBinaryDeltaOperation alloc] initWithRelativePath:key oldTree:oldPath newTree:newPath]; - [deltaQueue addOperation:operation]; - [deltaOperations addObject:operation]; - } - } - - [deltaQueue waitUntilAllOperationsAreFinished]; - - for (CreateBinaryDeltaOperation *operation in deltaOperations) { - NSString *resultPath = [operation resultPath]; - xar_file_t newFile = xar_add_frompath(x, 0, [[operation relativePath] fileSystemRepresentation], [resultPath fileSystemRepresentation]); - assert(newFile); - xar_prop_set(newFile, "binary-delta", "true"); - unlink([resultPath fileSystemRepresentation]); - } - - xar_close(x); - - unlink([patchFile fileSystemRepresentation]); - link([temporaryFile fileSystemRepresentation], [patchFile fileSystemRepresentation]); - unlink([temporaryFile fileSystemRepresentation]); - fprintf(stderr, "Done!\n"); - - return 0; + + return result; } } diff --git a/Frameworks/Sparkle/Sparkle/SUBinaryDeltaUnarchiver.m b/Frameworks/Sparkle/Sparkle/SUBinaryDeltaUnarchiver.m index df8b67ef2..0c238df7f 100644 --- a/Frameworks/Sparkle/Sparkle/SUBinaryDeltaUnarchiver.m +++ b/Frameworks/Sparkle/Sparkle/SUBinaryDeltaUnarchiver.m @@ -17,16 +17,16 @@ + (BOOL)canUnarchivePath:(NSString *)path { - return binaryDeltaSupported() && [[path pathExtension] isEqualToString:@"delta"]; + return [[path pathExtension] isEqualToString:@"delta"]; } - (void)applyBinaryDelta { @autoreleasepool { - NSString *sourcePath = [[self.updateHost bundle] bundlePath]; + NSString *sourcePath = self.updateHostBundlePath; NSString *targetPath = [[self.archivePath stringByDeletingLastPathComponent] stringByAppendingPathComponent:[sourcePath lastPathComponent]]; - int result = applyBinaryDelta(sourcePath, targetPath, self.archivePath); + int result = applyBinaryDelta(sourcePath, targetPath, self.archivePath, NO); if (!result) { dispatch_async(dispatch_get_main_queue(), ^{ [self notifyDelegateOfSuccess]; diff --git a/Frameworks/Sparkle/Sparkle/SUCodeSigningVerifier.h b/Frameworks/Sparkle/Sparkle/SUCodeSigningVerifier.h index 1f01d1b55..eddd1b261 100644 --- a/Frameworks/Sparkle/Sparkle/SUCodeSigningVerifier.h +++ b/Frameworks/Sparkle/Sparkle/SUCodeSigningVerifier.h @@ -12,8 +12,10 @@ #import @interface SUCodeSigningVerifier : NSObject -+ (BOOL)codeSignatureIsValidAtPath:(NSString *)destinationPath error:(NSError **)error; ++ (BOOL)codeSignatureMatchesHostAndIsValidAtPath:(NSString *)applicationPath error:(NSError **)error; ++ (BOOL)codeSignatureIsValidAtPath:(NSString *)applicationPath error:(NSError **)error; + (BOOL)hostApplicationIsCodeSigned; ++ (BOOL)applicationAtPathIsCodeSigned:(NSString *)applicationPath; @end #endif diff --git a/Frameworks/Sparkle/Sparkle/SUCodeSigningVerifier.m b/Frameworks/Sparkle/Sparkle/SUCodeSigningVerifier.m index 692865928..73efc6c11 100644 --- a/Frameworks/Sparkle/Sparkle/SUCodeSigningVerifier.m +++ b/Frameworks/Sparkle/Sparkle/SUCodeSigningVerifier.m @@ -13,7 +13,7 @@ @implementation SUCodeSigningVerifier -+ (BOOL)codeSignatureIsValidAtPath:(NSString *)destinationPath error:(NSError *__autoreleasing *)error ++ (BOOL)codeSignatureMatchesHostAndIsValidAtPath:(NSString *)applicationPath error:(NSError *__autoreleasing *)error { OSStatus result; SecRequirementRef requirement = NULL; @@ -37,20 +37,23 @@ goto finally; } - newBundle = [NSBundle bundleWithPath:destinationPath]; + newBundle = [NSBundle bundleWithPath:applicationPath]; if (!newBundle) { SULog(@"Failed to load NSBundle for update"); result = -1; goto finally; } - result = SecStaticCodeCreateWithPath((__bridge CFURLRef)[newBundle executableURL], kSecCSDefaultFlags, &staticCode); + result = SecStaticCodeCreateWithPath((__bridge CFURLRef)[newBundle bundleURL], kSecCSDefaultFlags, &staticCode); if (result != noErr) { SULog(@"Failed to get static code %d", result); goto finally; } - result = SecStaticCodeCheckValidityWithErrors(staticCode, kSecCSDefaultFlags | kSecCSCheckAllArchitectures, requirement, &cfError); + // Note that kSecCSCheckNestedCode may not work with pre-Mavericks code signing. + // See https://github.com/sparkle-project/Sparkle/issues/376#issuecomment-48824267 and https://developer.apple.com/library/mac/technotes/tn2206 + SecCSFlags flags = kSecCSDefaultFlags | kSecCSCheckAllArchitectures; + result = SecStaticCodeCheckValidityWithErrors(staticCode, flags, requirement, &cfError); if (cfError) { NSError *tmpError = CFBridgingRelease(cfError); @@ -80,6 +83,53 @@ finally: return (result == noErr); } ++ (BOOL)codeSignatureIsValidAtPath:(NSString *)applicationPath error:(NSError *__autoreleasing *)error +{ + OSStatus result; + SecStaticCodeRef staticCode = NULL; + NSBundle *newBundle; + CFErrorRef cfError = NULL; + if (error) { + *error = nil; + } + + newBundle = [NSBundle bundleWithPath:applicationPath]; + if (!newBundle) { + SULog(@"Failed to load NSBundle"); + result = -1; + goto finally; + } + + result = SecStaticCodeCreateWithPath((__bridge CFURLRef)[newBundle bundleURL], kSecCSDefaultFlags, &staticCode); + if (result != noErr) { + SULog(@"Failed to get static code %d", result); + goto finally; + } + + // Note that kSecCSCheckNestedCode may not work with pre-Mavericks code signing. + // See https://github.com/sparkle-project/Sparkle/issues/376#issuecomment-48824267 and https://developer.apple.com/library/mac/technotes/tn2206 + SecCSFlags flags = kSecCSDefaultFlags | kSecCSCheckAllArchitectures; + result = SecStaticCodeCheckValidityWithErrors(staticCode, flags, NULL, &cfError); + + if (cfError) { + NSError *tmpError = CFBridgingRelease(cfError); + if (error) *error = tmpError; + } + + if (result != noErr) { + if (result == errSecCSUnsigned) { + SULog(@"Error: The app is not signed using Apple Code Signing. %@", applicationPath); + } + if (result == errSecCSReqFailed) { + [self logSigningInfoForCode:staticCode label:@"new info"]; + } + } + +finally: + if (staticCode) CFRelease(staticCode); + return (result == noErr); +} + static id valueOrNSNull(id value) { return value ? value : [NSNull null]; } @@ -114,4 +164,35 @@ static id valueOrNSNull(id value) { return (result == 0); } ++ (BOOL)applicationAtPathIsCodeSigned:(NSString *)applicationPath +{ + OSStatus result; + SecStaticCodeRef staticCode = NULL; + NSBundle *newBundle; + + newBundle = [NSBundle bundleWithPath:applicationPath]; + if (!newBundle) { + SULog(@"Failed to load NSBundle"); + return NO; + } + + result = SecStaticCodeCreateWithPath((__bridge CFURLRef)[newBundle bundleURL], kSecCSDefaultFlags, &staticCode); + if (result == errSecCSUnsigned) { + return NO; + } + + SecRequirementRef requirement = NULL; + result = SecCodeCopyDesignatedRequirement(staticCode, kSecCSDefaultFlags, &requirement); + if (staticCode) { + CFRelease(staticCode); + } + if (requirement) { + CFRelease(requirement); + } + if (result == errSecCSUnsigned) { + return NO; + } + return (result == 0); +} + @end diff --git a/Frameworks/Sparkle/Sparkle/SUConstants.h b/Frameworks/Sparkle/Sparkle/SUConstants.h index d9d7887bf..e6bfcb31c 100644 --- a/Frameworks/Sparkle/Sparkle/SUConstants.h +++ b/Frameworks/Sparkle/Sparkle/SUConstants.h @@ -79,33 +79,4 @@ extern NSString *const SURSSElementLink; extern NSString *const SURSSElementPubDate; extern NSString *const SURSSElementTitle; -// ----------------------------------------------------------------------------- -// Errors: -// ----------------------------------------------------------------------------- - -extern NSString *const SUSparkleErrorDomain; -typedef NS_ENUM(OSStatus, SUError) { - // Appcast phase errors. - SUAppcastParseError = 1000, - SUNoUpdateError = 1001, - SUAppcastError = 1002, - SURunningFromDiskImageError = 1003, - - // Downlaod phase errors. - SUTemporaryDirectoryError = 2000, - - // Extraction phase errors. - SUUnarchivingError = 3000, - SUSignatureError = 3001, - - // Installation phase errors. - SUFileCopyFailure = 4000, - SUAuthenticationFailure = 4001, - SUMissingUpdateError = 4002, - SUMissingInstallerToolError = 4003, - SURelaunchError = 4004, - SUInstallationError = 4005, - SUDowngradeError = 4006 -}; - #endif diff --git a/Frameworks/Sparkle/Sparkle/SUErrors.h b/Frameworks/Sparkle/Sparkle/SUErrors.h new file mode 100644 index 000000000..e624bb056 --- /dev/null +++ b/Frameworks/Sparkle/Sparkle/SUErrors.h @@ -0,0 +1,44 @@ +// +// SUErrors.h +// Sparkle +// +// Created by C.W. Betts on 10/13/14. +// Copyright (c) 2014 Sparkle Project. All rights reserved. +// + +#ifndef SUERRORS_H +#define SUERRORS_H + +#import +#import "SUExport.h" + +/** + * Error domain used by Sparkle + */ +SU_EXPORT extern NSString *const SUSparkleErrorDomain; + +typedef NS_ENUM(OSStatus, SUError) { + // Appcast phase errors. + SUAppcastParseError = 1000, + SUNoUpdateError = 1001, + SUAppcastError = 1002, + SURunningFromDiskImageError = 1003, + + // Downlaod phase errors. + SUTemporaryDirectoryError = 2000, + + // Extraction phase errors. + SUUnarchivingError = 3000, + SUSignatureError = 3001, + + // Installation phase errors. + SUFileCopyFailure = 4000, + SUAuthenticationFailure = 4001, + SUMissingUpdateError = 4002, + SUMissingInstallerToolError = 4003, + SURelaunchError = 4004, + SUInstallationError = 4005, + SUDowngradeError = 4006 +}; + +#endif diff --git a/Frameworks/Sparkle/Sparkle/SUGuidedPackageInstaller.h b/Frameworks/Sparkle/Sparkle/SUGuidedPackageInstaller.h new file mode 100644 index 000000000..92082a340 --- /dev/null +++ b/Frameworks/Sparkle/Sparkle/SUGuidedPackageInstaller.h @@ -0,0 +1,35 @@ +// +// SUGuidedPackageInstaller.h +// Sparkle +// +// Created by Graham Miln on 14/05/2010. +// Copyright 2010 Dragon Systems Software Limited. All rights reserved. +// + +/*! +# Sparkle Guided Installations + +A guided installation allows Sparkle to download and install a package (pkg) or multi-package (mpkg) without user interaction. + +The installer package is installed using Mac OS X's built-in command line installer, `/usr/sbin/installer`. No installation interface is shown to the user. + +A guided installation can be started by applications other than the application being replaced. This is particularly useful where helper applications or agents are used. + +## To Do +- Replace the use of `AuthorizationExecuteWithPrivilegesAndWait`. This method remains because it is well supported and tested. Ideally a helper tool or XPC would be used. +*/ + +#ifndef SUGUIDEDPACKAGEINSTALLER_H +#define SUGUIDEDPACKAGEINSTALLER_H + +#import "Sparkle.h" +#import "SUInstaller.h" + +@interface SUGuidedPackageInstaller : SUInstaller { +} + +/*! Perform the guided installation */ ++ (void)performInstallationToPath:(NSString *)path fromPath:(NSString *)installerGuide host:(SUHost *)host versionComparator:(id)comparator completionHandler:(void (^)(NSError *))completionHandler; +@end + +#endif diff --git a/Frameworks/Sparkle/Sparkle/SUGuidedPackageInstaller.m b/Frameworks/Sparkle/Sparkle/SUGuidedPackageInstaller.m new file mode 100644 index 000000000..bc83d42be --- /dev/null +++ b/Frameworks/Sparkle/Sparkle/SUGuidedPackageInstaller.m @@ -0,0 +1,139 @@ +// +// SUGuidedPackageInstaller.m +// Sparkle +// +// Created by Graham Miln on 14/05/2010. +// Copyright 2010 Dragon Systems Software Limited. All rights reserved. +// + +#import +#import + +#import "SUGuidedPackageInstaller.h" + +static BOOL AuthorizationExecuteWithPrivilegesAndWait(AuthorizationRef authorization, const char* executablePath, AuthorizationFlags options, const char* const* arguments) +{ + sig_t oldSigChildHandler = signal(SIGCHLD, SIG_DFL); + BOOL returnValue = YES; + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" + /* AuthorizationExecuteWithPrivileges used to support 10.4+; should be replaced with XPC or external process */ + if (AuthorizationExecuteWithPrivileges(authorization, executablePath, options, (char* const*)arguments, NULL) == errAuthorizationSuccess) +#pragma clang diagnostic pop + { + int status = 0; + pid_t pid = wait(&status); + if (pid == -1 || !WIFEXITED(status) || WEXITSTATUS(status) != 0) + returnValue = NO; + } + else + returnValue = NO; + + signal(SIGCHLD, oldSigChildHandler); + return returnValue; +} + +@implementation SUGuidedPackageInstaller (SUGuidedPackageInstallerAuthentication) + ++ (AuthorizationRef)authorizationForExecutable:(NSString*)executablePath +{ + NSParameterAssert(executablePath); + + // Get authorization using advice in Apple's Technical Q&A1172 + + // ...create authorization without specific rights + AuthorizationRef auth = NULL; + OSStatus validAuth = AuthorizationCreate(NULL, + kAuthorizationEmptyEnvironment, + kAuthorizationFlagDefaults, + &auth); + // ...then extend authorization with desired rights + if ((validAuth == errAuthorizationSuccess) && + (auth != NULL)) + { + const char* executableFileSystemRepresentation = [executablePath fileSystemRepresentation]; + + // Prepare a right allowing script to execute with privileges + AuthorizationItem right; + memset(&right,0,sizeof(right)); + right.name = kAuthorizationRightExecute; + right.value = (void*) executableFileSystemRepresentation; + right.valueLength = strlen(executableFileSystemRepresentation); + + // Package up the single right + AuthorizationRights rights; + memset(&rights,0,sizeof(rights)); + rights.count = 1; + rights.items = &right; + + // Extend rights to run script + validAuth = AuthorizationCopyRights(auth, + &rights, + kAuthorizationEmptyEnvironment, + kAuthorizationFlagPreAuthorize | + kAuthorizationFlagExtendRights | + kAuthorizationFlagInteractionAllowed, + NULL); + if (validAuth != errAuthorizationSuccess) + { + // Error, clean up authorization + (void) AuthorizationFree(auth,kAuthorizationFlagDefaults); + auth = NULL; + } + } + + return auth; +} + +@end + +@implementation SUGuidedPackageInstaller + ++ (void)performInstallationToPath:(NSString *)destinationPath fromPath:(NSString *)packagePath host:(SUHost *)__unused host versionComparator:(id)__unused comparator completionHandler:(void (^)(NSError *))completionHandler +{ + NSParameterAssert(packagePath); + + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + + // Preflight + NSString* installerPath = @"/usr/sbin/installer"; // Mac OS X 10.2+ command line installer tool + NSError* error = nil; + + // Create authorization for installer executable + BOOL validInstallation = NO; + AuthorizationRef auth = [self authorizationForExecutable:installerPath]; + if (auth != NULL) + { + // Permission was granted to execute the installer with privileges + const char* const arguments[] = { + "-pkg", + [packagePath fileSystemRepresentation], + "-target", + "/", + NULL + }; + validInstallation = AuthorizationExecuteWithPrivilegesAndWait(auth, + [installerPath fileSystemRepresentation], + kAuthorizationFlagDefaults, + arguments); + // TODO: wait for communications pipe to close via fileno & CFSocketCreateWithNative + AuthorizationFree(auth,kAuthorizationFlagDefaults); + } + else + { + NSString* errorMessage = [NSString stringWithFormat:@"Sparkle Updater: Script authorization denied."]; + error = [NSError errorWithDomain:SUSparkleErrorDomain code:SUInstallationError userInfo:[NSDictionary dictionaryWithObject:errorMessage forKey:NSLocalizedDescriptionKey]]; + } + + dispatch_async(dispatch_get_main_queue(), ^{ + [self finishInstallationToPath:destinationPath + withResult:validInstallation + error:error + completionHandler:completionHandler]; + + }); + }); +} + +@end diff --git a/Frameworks/Sparkle/Sparkle/SUHost.h b/Frameworks/Sparkle/Sparkle/SUHost.h index 6e4e49f9a..4ec132867 100644 --- a/Frameworks/Sparkle/Sparkle/SUHost.h +++ b/Frameworks/Sparkle/Sparkle/SUHost.h @@ -18,7 +18,7 @@ - (instancetype)initWithBundle:(NSBundle *)aBundle; @property (readonly, copy) NSString *bundlePath; -@property (readonly, copy) NSString *appSupportPath; +@property (readonly, copy) NSString *appCachePath; @property (readonly, copy) NSString *installationPath; @property (readonly, copy) NSString *name; @property (readonly, copy) NSString *version; diff --git a/Frameworks/Sparkle/Sparkle/SUHost.m b/Frameworks/Sparkle/Sparkle/SUHost.m index 58c536513..8f559a87e 100644 --- a/Frameworks/Sparkle/Sparkle/SUHost.m +++ b/Frameworks/Sparkle/Sparkle/SUHost.m @@ -66,21 +66,26 @@ typedef struct { return [self.bundle bundlePath]; } -- (NSString *)appSupportPath +- (NSString *)appCachePath { - NSArray *appSupportPaths = NSSearchPathForDirectoriesInDomains(NSApplicationSupportDirectory, NSUserDomainMask, YES); - NSString *appSupportPath = nil; - if (!appSupportPaths || [appSupportPaths count] == 0) - { - SULog(@"Failed to find app support directory! Using ~/Library/Application Support..."); - appSupportPath = [@"~/Library/Application Support" stringByExpandingTildeInPath]; + NSArray *cachePaths = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES); + NSString *cachePath = nil; + if ([cachePaths count]) { + cachePath = cachePaths[0]; } - else { - appSupportPath = appSupportPaths[0]; + if (!cachePath) { + SULog(@"Failed to find user's cache directory! Using system default"); + cachePath = NSTemporaryDirectory(); } - appSupportPath = [appSupportPath stringByAppendingPathComponent:[self name]]; - appSupportPath = [appSupportPath stringByAppendingPathComponent:@".Sparkle"]; - return appSupportPath; + + NSString *name = [self.bundle bundleIdentifier]; + if (!name) { + name = [self name]; + } + + cachePath = [cachePath stringByAppendingPathComponent:name]; + cachePath = [cachePath stringByAppendingPathComponent:@"Sparkle"]; + return cachePath; } - (NSString *)installationPath @@ -97,7 +102,13 @@ typedef struct { - (NSString *)name { - NSString *name = [self.bundle objectForInfoDictionaryKey:@"CFBundleDisplayName"]; + NSString *name; + + // Allow host bundle to provide a custom name + name = [self objectForInfoDictionaryKey:@"SUBundleName"]; + if (name) return name; + + name = [self.bundle objectForInfoDictionaryKey:@"CFBundleDisplayName"]; if (name) return name; name = [self objectForInfoDictionaryKey:(__bridge NSString *)kCFBundleNameKey]; diff --git a/Frameworks/Sparkle/Sparkle/SUInstaller.h b/Frameworks/Sparkle/Sparkle/SUInstaller.h index c1b01898e..fe82f885f 100644 --- a/Frameworks/Sparkle/Sparkle/SUInstaller.h +++ b/Frameworks/Sparkle/Sparkle/SUInstaller.h @@ -12,21 +12,14 @@ #import #import "SUVersionComparisonProtocol.h" -@protocol SUInstallerDelegate; - @class SUHost; @interface SUInstaller : NSObject + (NSString *)appPathInUpdateFolder:(NSString *)updateFolder forHost:(SUHost *)host; -+ (void)installFromUpdateFolder:(NSString *)updateFolder overHost:(SUHost *)host installationPath:(NSString *)installationPath delegate:(id)delegate versionComparator:(id)comparator; -+ (void)finishInstallationToPath:(NSString *)installationPath withResult:(BOOL)result host:(SUHost *)host error:(NSError *)error delegate:(id)delegate; ++ (void)installFromUpdateFolder:(NSString *)updateFolder overHost:(SUHost *)host installationPath:(NSString *)installationPath versionComparator:(id)comparator completionHandler:(void (^)(NSError *))completionHandler; ++ (void)finishInstallationToPath:(NSString *)installationPath withResult:(BOOL)result error:(NSError *)error completionHandler:(void (^)(NSError *))completionHandler; + (NSString *)updateFolder; @end -@protocol SUInstallerDelegate -- (void)installerFinishedForHost:(SUHost *)host; -- (void)installerForHost:(SUHost *)host failedWithError:(NSError *)error; -@end - #endif diff --git a/Frameworks/Sparkle/Sparkle/SUInstaller.m b/Frameworks/Sparkle/Sparkle/SUInstaller.m index 6eff13942..2513973a9 100644 --- a/Frameworks/Sparkle/Sparkle/SUInstaller.m +++ b/Frameworks/Sparkle/Sparkle/SUInstaller.m @@ -9,11 +9,11 @@ #import "SUInstaller.h" #import "SUPlainInstaller.h" #import "SUPackageInstaller.h" +#import "SUGuidedPackageInstaller.h" #import "SUHost.h" #import "SUConstants.h" #import "SULog.h" - @implementation SUInstaller static NSString *sUpdateFolder = nil; @@ -42,50 +42,49 @@ static NSString *sUpdateFolder = nil; return NO; } -+ (NSString *)installSourcePathInUpdateFolder:(NSString *)inUpdateFolder forHost:(SUHost *)host isPackage:(BOOL *)isPackagePtr ++ (NSString *)installSourcePathInUpdateFolder:(NSString *)inUpdateFolder forHost:(SUHost *)host isPackage:(BOOL *)isPackagePtr isGuided:(BOOL *)isGuidedPtr { + NSParameterAssert(inUpdateFolder); + NSParameterAssert(host); + // Search subdirectories for the application NSString *currentFile, *newAppDownloadPath = nil, *bundleFileName = [[host bundlePath] lastPathComponent], *alternateBundleFileName = [[host name] stringByAppendingPathExtension:[[host bundlePath] pathExtension]]; BOOL isPackage = NO; + BOOL isGuided = NO; NSString *fallbackPackagePath = nil; NSDirectoryEnumerator *dirEnum = [[NSFileManager defaultManager] enumeratorAtPath:inUpdateFolder]; + NSString *bundleFileNameNoExtension = [bundleFileName stringByDeletingPathExtension]; sUpdateFolder = inUpdateFolder; - while ((currentFile = [dirEnum nextObject])) - { + while ((currentFile = [dirEnum nextObject])) { NSString *currentPath = [inUpdateFolder stringByAppendingPathComponent:currentFile]; - if ([[currentFile lastPathComponent] isEqualToString:bundleFileName] || - [[currentFile lastPathComponent] isEqualToString:alternateBundleFileName]) // We found one! + NSString *currentFilename = [currentFile lastPathComponent]; + NSString *currentExtension = [currentFile pathExtension]; + NSString *currentFilenameNoExtension = [currentFilename stringByDeletingPathExtension]; + if ([currentFilename isEqualToString:bundleFileName] || + [currentFilename isEqualToString:alternateBundleFileName]) // We found one! { isPackage = NO; newAppDownloadPath = currentPath; break; - } - else if ([[currentFile pathExtension] isEqualToString:@"pkg"] || - [[currentFile pathExtension] isEqualToString:@"mpkg"]) - { - if ([[[currentFile lastPathComponent] stringByDeletingPathExtension] isEqualToString:[bundleFileName stringByDeletingPathExtension]]) - { + } else if ([currentExtension isEqualToString:@"pkg"] || + [currentExtension isEqualToString:@"mpkg"]) { + if ([currentFilenameNoExtension isEqualToString:bundleFileNameNoExtension]) { isPackage = YES; newAppDownloadPath = currentPath; break; - } - else - { + } else { // Remember any other non-matching packages we have seen should we need to use one of them as a fallback. fallbackPackagePath = currentPath; } - } - else - { + } else { // Try matching on bundle identifiers in case the user has changed the name of the host app NSBundle *incomingBundle = [NSBundle bundleWithPath:currentPath]; - if(incomingBundle && [[incomingBundle bundleIdentifier] isEqualToString:[[host bundle] bundleIdentifier]]) - { + if (incomingBundle && [[incomingBundle bundleIdentifier] isEqualToString:[[host bundle] bundleIdentifier]]) { isPackage = NO; newAppDownloadPath = currentPath; break; @@ -99,35 +98,52 @@ static NSString *sUpdateFolder = nil; // We don't have a valid path. Try to use the fallback package. - if (newAppDownloadPath == nil && fallbackPackagePath != nil) - { + if (newAppDownloadPath == nil && fallbackPackagePath != nil) { isPackage = YES; newAppDownloadPath = fallbackPackagePath; } - if (isPackagePtr) *isPackagePtr = isPackage; + if (isPackage) { + // foo.app -> foo.sparkle_guided.pkg or foo.sparkle_guided.mpkg + if ([[[newAppDownloadPath stringByDeletingPathExtension] pathExtension] isEqualToString:@"sparkle_guided"]) { + isGuided = YES; + } + } + + if (isPackagePtr) + *isPackagePtr = isPackage; + if (isGuidedPtr) + *isGuidedPtr = isGuided; + + if (!newAppDownloadPath) { + SULog(@"Searched %@ for %@.(app|pkg)", inUpdateFolder, bundleFileNameNoExtension); + } return newAppDownloadPath; } + (NSString *)appPathInUpdateFolder:(NSString *)updateFolder forHost:(SUHost *)host { BOOL isPackage = NO; - NSString *path = [self installSourcePathInUpdateFolder:updateFolder forHost:host isPackage:&isPackage]; + NSString *path = [self installSourcePathInUpdateFolder:updateFolder forHost:host isPackage:&isPackage isGuided:nil]; return isPackage ? nil : path; } -+ (void)installFromUpdateFolder:(NSString *)inUpdateFolder overHost:(SUHost *)host installationPath:(NSString *)installationPath delegate:(id)delegate versionComparator:(id)comparator ++ (void)installFromUpdateFolder:(NSString *)inUpdateFolder overHost:(SUHost *)host installationPath:(NSString *)installationPath versionComparator:(id)comparator completionHandler:(void (^)(NSError *))completionHandler { BOOL isPackage = NO; - NSString *newAppDownloadPath = [self installSourcePathInUpdateFolder:inUpdateFolder forHost:host isPackage:&isPackage]; + BOOL isGuided = NO; + NSString *newAppDownloadPath = [self installSourcePathInUpdateFolder:inUpdateFolder forHost:host isPackage:&isPackage isGuided:&isGuided]; - if (newAppDownloadPath == nil) - { - [self finishInstallationToPath:installationPath withResult:NO host:host error:[NSError errorWithDomain:SUSparkleErrorDomain code:SUMissingUpdateError userInfo:@{ NSLocalizedDescriptionKey: @"Couldn't find an appropriate update in the downloaded package." }] delegate:delegate]; - } - else - { - [(isPackage ? [SUPackageInstaller class] : [SUPlainInstaller class])performInstallationToPath:installationPath fromPath:newAppDownloadPath host:host delegate:delegate versionComparator:comparator]; + if (newAppDownloadPath == nil) { + [self finishInstallationToPath:installationPath withResult:NO error:[NSError errorWithDomain:SUSparkleErrorDomain code:SUMissingUpdateError userInfo:@{ NSLocalizedDescriptionKey: @"Couldn't find an appropriate update in the downloaded package." }] completionHandler:completionHandler]; + } else { + if (isPackage && isGuided) { + [SUGuidedPackageInstaller performInstallationToPath:installationPath fromPath:newAppDownloadPath host:host versionComparator:comparator completionHandler:completionHandler]; + } else if (isPackage) { + [SUPackageInstaller performInstallationToPath:installationPath fromPath:newAppDownloadPath host:host versionComparator:comparator completionHandler:completionHandler]; + } else { + [SUPlainInstaller performInstallationToPath:installationPath fromPath:newAppDownloadPath host:host versionComparator:comparator completionHandler:completionHandler]; + } } } @@ -140,8 +156,7 @@ static NSString *sUpdateFolder = nil; NSTask *mdimport = [[NSTask alloc] init]; [mdimport setLaunchPath:@"/usr/bin/mdimport"]; [mdimport setArguments:@[installationPath]]; - @try - { + @try { [mdimport launch]; [mdimport waitUntilExit]; } @@ -152,24 +167,20 @@ static NSString *sUpdateFolder = nil; } } -+ (void)finishInstallationToPath:(NSString *)installationPath withResult:(BOOL)result host:(SUHost *)host error:(NSError *)error delegate:(id)delegate ++ (void)finishInstallationToPath:(NSString *)installationPath withResult:(BOOL)result error:(NSError *)error completionHandler:(void (^)(NSError *))completionHandler { - if (result) - { + if (result) { [self mdimportInstallationPath:installationPath]; - if ([delegate respondsToSelector:@selector(installerFinishedForHost:)]) { - dispatch_async(dispatch_get_main_queue(), ^{ - [delegate installerFinishedForHost:host]; - }); - } - } - else - { - if ([delegate respondsToSelector:@selector(installerForHost:failedWithError:)]) { - dispatch_async(dispatch_get_main_queue(), ^{ - [delegate installerForHost:host failedWithError:error]; - }); + dispatch_async(dispatch_get_main_queue(), ^{ + completionHandler(nil); + }); + } else { + if (!error) { + error = [NSError errorWithDomain:SUSparkleErrorDomain code:SUInstallationError userInfo:nil]; } + dispatch_async(dispatch_get_main_queue(), ^{ + completionHandler(error); + }); } } diff --git a/Frameworks/Sparkle/Sparkle/SULog.h b/Frameworks/Sparkle/Sparkle/SULog.h index e3559b5ea..1693f5355 100644 --- a/Frameworks/Sparkle/Sparkle/SULog.h +++ b/Frameworks/Sparkle/Sparkle/SULog.h @@ -12,7 +12,8 @@ Your tech support will hug you if you tell them about this. */ -#pragma once +#ifndef SULOG_H +#define SULOG_H // ----------------------------------------------------------------------------- // Headers: @@ -28,4 +29,4 @@ void SUClearLog(void); void SULog(NSString *format, ...) NS_FORMAT_FUNCTION(1, 2); - +#endif diff --git a/Frameworks/Sparkle/Sparkle/SULog.m b/Frameworks/Sparkle/Sparkle/SULog.m index 02c3be10c..5941aac7e 100644 --- a/Frameworks/Sparkle/Sparkle/SULog.m +++ b/Frameworks/Sparkle/Sparkle/SULog.m @@ -30,10 +30,6 @@ static NSString *const SULogFilePath = @"~/Library/Logs/SparkleUpdateLog.log"; // // TAKES: // sender - Object that sent this message, typically of type X. -// -// GIVES: -// param - who owns the returned value? -// result - same here. // ----------------------------------------------------------------------------- void SUClearLog(void) @@ -41,6 +37,7 @@ void SUClearLog(void) FILE *logfile = fopen([[SULogFilePath stringByExpandingTildeInPath] fileSystemRepresentation], "w"); if (logfile) { fclose(logfile); + SULog(@"===== %@ =====", [[NSFileManager defaultManager] displayNameAtPath:[[NSBundle mainBundle] bundlePath]]); } } @@ -57,6 +54,12 @@ void SUClearLog(void) void SULog(NSString *format, ...) { + static BOOL loggedYet = NO; + if (!loggedYet) { + loggedYet = YES; + SUClearLog(); + } + va_list ap; va_start(ap, format); NSString *theStr = [[NSString alloc] initWithFormat:format arguments:ap]; diff --git a/Frameworks/Sparkle/Sparkle/SUPackageInstaller.m b/Frameworks/Sparkle/Sparkle/SUPackageInstaller.m index 23ad954f5..64637433c 100644 --- a/Frameworks/Sparkle/Sparkle/SUPackageInstaller.m +++ b/Frameworks/Sparkle/Sparkle/SUPackageInstaller.m @@ -12,7 +12,7 @@ @implementation SUPackageInstaller -+ (void)performInstallationToPath:(NSString *)installationPath fromPath:(NSString *)path host:(SUHost *)host delegate:(id)delegate versionComparator:(id)__unused comparator ++ (void)performInstallationToPath:(NSString *)installationPath fromPath:(NSString *)path host:(SUHost *)__unused host versionComparator:(id)__unused comparator completionHandler:(void (^)(NSError *))completionHandler { // Run installer using the "open" command to ensure it is launched in front of current application. // -W = wait until the app has quit. @@ -23,7 +23,7 @@ if (![[NSFileManager defaultManager] fileExistsAtPath:command]) { NSError *error = [NSError errorWithDomain:SUSparkleErrorDomain code:SUMissingInstallerToolError userInfo:@{ NSLocalizedDescriptionKey: @"Couldn't find Apple's installer tool!" }]; - [self finishInstallationToPath:installationPath withResult:NO host:host error:error delegate:delegate]; + [self finishInstallationToPath:installationPath withResult:NO error:error completionHandler:completionHandler]; return; } @@ -33,7 +33,7 @@ // Known bug: if the installation fails or is canceled, Sparkle goes ahead and restarts, thinking everything is fine. dispatch_async(dispatch_get_main_queue(), ^{ - [self finishInstallationToPath:installationPath withResult:YES host:host error:nil delegate:delegate]; + [self finishInstallationToPath:installationPath withResult:YES error:nil completionHandler:completionHandler]; }); }); } diff --git a/Frameworks/Sparkle/Sparkle/SUPipedUnarchiver.m b/Frameworks/Sparkle/Sparkle/SUPipedUnarchiver.m index 1f2f68eaa..7a65054c1 100644 --- a/Frameworks/Sparkle/Sparkle/SUPipedUnarchiver.m +++ b/Frameworks/Sparkle/Sparkle/SUPipedUnarchiver.m @@ -22,7 +22,10 @@ @".tar.gz": @"extractTGZ", @".tgz": @"extractTGZ", @".tar.bz2": @"extractTBZ", - @".tbz": @"extractTBZ" }; + @".tbz": @"extractTBZ", + @".tar.xz": @"extractTXZ", + @".txz": @"extractTXZ", + @".tar.lzma": @"extractTXZ"}; NSString *lastPathComponent = [path lastPathComponent]; for (NSString *currentType in typeSelectorDictionary) @@ -45,73 +48,69 @@ } // This method abstracts the types that use a command line tool piping data from stdin. -- (void)extractArchivePipingDataToCommand:(NSString *)command +- (void)extractArchivePipingDataToCommand:(NSString *)command args:(NSArray*)args { // *** GETS CALLED ON NON-MAIN THREAD!!! @autoreleasepool { - FILE *fp = NULL, *cmdFP = NULL; - char *oldDestinationString = NULL; - // We have to declare these before a goto to prevent an error under ARC. - // No, we cannot have them in the dispatch_async calls, as the goto "jump enters - // lifetime of block which strongly captures a variable" - dispatch_block_t delegateSuccess = ^{ - [self notifyDelegateOfSuccess]; - }; - dispatch_block_t delegateFailure = ^{ - [self notifyDelegateOfFailure]; - }; - SULog(@"Extracting %@ using '%@'", self.archivePath, command); + NSString *destination = [self.archivePath stringByDeletingLastPathComponent]; + + SULog(@"Extracting using '%@' '%@' < '%@' '%@'", command, [args componentsJoinedByString:@"' '"], self.archivePath, destination); // Get the file size. - NSNumber *fs = [[NSFileManager defaultManager] attributesOfItemAtPath:self.archivePath error:nil][NSFileSize]; - if (fs == nil) goto reportError; + NSUInteger expectedLength = [[[NSFileManager defaultManager] attributesOfItemAtPath:self.archivePath error:nil][NSFileSize] unsignedIntegerValue]; + if (expectedLength > 0) { + NSFileHandle *archiveInput = [NSFileHandle fileHandleForReadingAtPath:self.archivePath]; - // Thank you, Allan Odgaard! - // (who wrote the following extraction alg.) - fp = fopen([self.archivePath fileSystemRepresentation], "r"); - if (!fp) goto reportError; + NSPipe *pipe = [NSPipe pipe]; + NSFileHandle *archiveOutput = [pipe fileHandleForWriting]; - oldDestinationString = getenv("DESTINATION"); - setenv("DESTINATION", [[self.archivePath stringByDeletingLastPathComponent] fileSystemRepresentation], 1); - cmdFP = popen([command fileSystemRepresentation], "w"); - size_t written; - if (!cmdFP) goto reportError; + NSTask *task = [[NSTask alloc] init]; + [task setStandardInput:[pipe fileHandleForReading]]; + [task setStandardError:[NSFileHandle fileHandleWithStandardError]]; + [task setStandardOutput:[NSFileHandle fileHandleWithStandardOutput]]; + [task setLaunchPath:command]; + [task setArguments:[args arrayByAddingObject:destination]]; + [task launch]; - char buf[32 * 1024]; - size_t len; - while((len = fread(buf, 1, 32*1024, fp))) - { - written = fwrite(buf, 1, len, cmdFP); - if( written < len ) - { - pclose(cmdFP); - goto reportError; + NSUInteger bytesRead = 0; + do { + NSData *data = [archiveInput readDataOfLength:256*1024]; + NSUInteger len = [data length]; + if (!len) { + break; + } + bytesRead += len; + [archiveOutput writeData:data]; + dispatch_async(dispatch_get_main_queue(), ^{ + [self notifyDelegateOfProgress:(double)bytesRead / (double)expectedLength]; + }); } + while(bytesRead < expectedLength); + + [archiveOutput closeFile]; - dispatch_async(dispatch_get_main_queue(), ^{ - [self notifyDelegateOfExtractedLength:len]; - }); - } - pclose(cmdFP); - - if (ferror(fp)) { - goto reportError; + [task waitUntilExit]; + + if ([task terminationStatus] == 0) { + if (bytesRead == expectedLength) { + dispatch_async(dispatch_get_main_queue(), ^{ + [self notifyDelegateOfSuccess]; + }); + return; + } else { + SULog(@"Extraction failed, command '%@' got only %ld of %ld bytes", command, (long)bytesRead, (long)expectedLength); + } + } else { + SULog(@"Extraction failed, command '%@' returned %d", command, [task terminationStatus]); + } + } else { + SULog(@"Extraction failed, archive '%@' is empty", self.archivePath); } - dispatch_async(dispatch_get_main_queue(), delegateSuccess); - goto finally; - - reportError: - dispatch_async(dispatch_get_main_queue(), delegateFailure); - - finally: - if (fp) - fclose(fp); - if (oldDestinationString) - setenv("DESTINATION", oldDestinationString, 1); - else - unsetenv("DESTINATION"); + dispatch_async(dispatch_get_main_queue(), ^{ + [self notifyDelegateOfFailure]; + }); } } @@ -119,28 +118,35 @@ { // *** GETS CALLED ON NON-MAIN THREAD!!! - [self extractArchivePipingDataToCommand:@"tar -xC \"$DESTINATION\""]; + [self extractArchivePipingDataToCommand:@"/usr/bin/tar" args:@[@"-xC"]]; } - (void)extractTGZ { // *** GETS CALLED ON NON-MAIN THREAD!!! - [self extractArchivePipingDataToCommand:@"tar -zxC \"$DESTINATION\""]; + [self extractArchivePipingDataToCommand:@"/usr/bin/tar" args:@[@"-zxC"]]; } - (void)extractTBZ { // *** GETS CALLED ON NON-MAIN THREAD!!! - [self extractArchivePipingDataToCommand:@"tar -jxC \"$DESTINATION\""]; + [self extractArchivePipingDataToCommand:@"/usr/bin/tar" args:@[@"-jxC"]]; } - (void)extractZIP { // *** GETS CALLED ON NON-MAIN THREAD!!! - [self extractArchivePipingDataToCommand:@"ditto -x -k - \"$DESTINATION\""]; + [self extractArchivePipingDataToCommand:@"/usr/bin/ditto" args:@[@"-x",@"-k",@"-"]]; +} + +- (void)extractTXZ +{ + // *** GETS CALLED ON NON-MAIN THREAD!!! + + [self extractArchivePipingDataToCommand:@"/usr/bin/tar" args:@[@"-zxC"]]; } + (void)load diff --git a/Frameworks/Sparkle/Sparkle/SUPlainInstaller.h b/Frameworks/Sparkle/Sparkle/SUPlainInstaller.h index bb7827a67..9fd78f6ff 100644 --- a/Frameworks/Sparkle/Sparkle/SUPlainInstaller.h +++ b/Frameworks/Sparkle/Sparkle/SUPlainInstaller.h @@ -21,7 +21,7 @@ @interface SUPlainInstaller : SUInstaller -+ (void)performInstallationToPath:(NSString *)installationPath fromPath:(NSString *)path host:(SUHost *)host delegate:(id)delegate versionComparator:(id)comparator; ++ (void)performInstallationToPath:(NSString *)installationPath fromPath:(NSString *)path host:(SUHost *)host versionComparator:(id)comparator completionHandler:(void (^)(NSError *))completionHandler; @end diff --git a/Frameworks/Sparkle/Sparkle/SUPlainInstaller.m b/Frameworks/Sparkle/Sparkle/SUPlainInstaller.m index 96973d465..1aeb63171 100644 --- a/Frameworks/Sparkle/Sparkle/SUPlainInstaller.m +++ b/Frameworks/Sparkle/Sparkle/SUPlainInstaller.m @@ -8,20 +8,22 @@ #import "SUPlainInstaller.h" #import "SUPlainInstallerInternals.h" +#import "SUCodeSigningVerifier.h" #import "SUConstants.h" #import "SUHost.h" @implementation SUPlainInstaller -+ (void)performInstallationToPath:(NSString *)installationPath fromPath:(NSString *)path host:(SUHost *)host delegate:(id)delegate versionComparator:(id)comparator ++ (void)performInstallationToPath:(NSString *)installationPath fromPath:(NSString *)path host:(SUHost *)host versionComparator:(id)comparator completionHandler:(void (^)(NSError *))completionHandler { + NSParameterAssert(host); + // Prevent malicious downgrades if (![[[NSBundle bundleWithIdentifier:SUBundleIdentifier] infoDictionary][SUEnableAutomatedDowngradesKey] boolValue]) { - if ([comparator compareVersion:[host version] toVersion:[[NSBundle bundleWithPath:path] objectForInfoDictionaryKey:(__bridge NSString *)kCFBundleVersionKey]] == NSOrderedDescending) - { + if ([comparator compareVersion:[host version] toVersion:[[NSBundle bundleWithPath:path] objectForInfoDictionaryKey:(__bridge NSString *)kCFBundleVersionKey]] == NSOrderedDescending) { NSString *errorMessage = [NSString stringWithFormat:@"Sparkle Updater: Possible attack in progress! Attempting to \"upgrade\" from %@ to %@. Aborting update.", [host version], [[NSBundle bundleWithPath:path] objectForInfoDictionaryKey:(__bridge NSString *)kCFBundleVersionKey]]; NSError *error = [NSError errorWithDomain:SUSparkleErrorDomain code:SUDowngradeError userInfo:@{ NSLocalizedDescriptionKey: errorMessage }]; - [self finishInstallationToPath:installationPath withResult:NO host:host error:error delegate:delegate]; + [self finishInstallationToPath:installationPath withResult:NO error:error completionHandler:completionHandler]; return; } } @@ -30,8 +32,17 @@ NSError *error = nil; NSString *oldPath = [host bundlePath]; NSString *tempName = [self temporaryNameForPath:[host installationPath]]; + BOOL hostIsCodeSigned = [SUCodeSigningVerifier applicationAtPathIsCodeSigned:oldPath]; BOOL result = [self copyPathWithAuthentication:path overPath:installationPath temporaryName:tempName error:&error]; + + if (result) { + // If the host is code signed, then the replacement should be be too (and the signature should be valid). + BOOL needToCheckCodeSignature = (hostIsCodeSigned || [SUCodeSigningVerifier applicationAtPathIsCodeSigned:installationPath]); + if (needToCheckCodeSignature) { + result = [SUCodeSigningVerifier codeSignatureIsValidAtPath:installationPath error:&error]; + } + } if (result) { BOOL haveOld = [[NSFileManager defaultManager] fileExistsAtPath:oldPath]; @@ -42,7 +53,7 @@ } dispatch_async(dispatch_get_main_queue(), ^{ - [self finishInstallationToPath:installationPath withResult:result host:host error:error delegate:delegate]; + [self finishInstallationToPath:installationPath withResult:result error:error completionHandler:completionHandler]; }); }); } diff --git a/Frameworks/Sparkle/Sparkle/SUPlainInstallerInternals.m b/Frameworks/Sparkle/Sparkle/SUPlainInstallerInternals.m index e06e5bdf7..41b297e8b 100644 --- a/Frameworks/Sparkle/Sparkle/SUPlainInstallerInternals.m +++ b/Frameworks/Sparkle/Sparkle/SUPlainInstallerInternals.m @@ -599,7 +599,11 @@ static BOOL AuthorizationExecuteWithPrivilegesAndWait(AuthorizationRef authoriza } #endif NSURL *rootURL = [NSURL fileURLWithPath:root]; - [rootURL setResourceValue:[NSNull null] forKey:NSURLQuarantinePropertiesKey error:NULL]; + 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. @@ -612,7 +616,11 @@ static BOOL AuthorizationExecuteWithPrivilegesAndWait(AuthorizationRef authoriza NSDirectoryEnumerator *directoryEnumerator = [manager enumeratorAtURL:rootURL includingPropertiesForKeys:nil options:(NSDirectoryEnumerationOptions)0 errorHandler:nil]; for (NSURL *file in directoryEnumerator) { - [file setResourceValue:[NSNull null] forKey:NSURLQuarantinePropertiesKey error:NULL]; + id fileResourceValue = nil; + [file getResourceValue:&fileResourceValue forKey:NSURLQuarantinePropertiesKey error:NULL]; + if (fileResourceValue) { + [file setResourceValue:[NSNull null] forKey:NSURLQuarantinePropertiesKey error:NULL]; + } } } } diff --git a/Frameworks/Sparkle/Sparkle/SUScheduledUpdateDriver.m b/Frameworks/Sparkle/Sparkle/SUScheduledUpdateDriver.m index b51fd91dc..9d6bf141b 100644 --- a/Frameworks/Sparkle/Sparkle/SUScheduledUpdateDriver.m +++ b/Frameworks/Sparkle/Sparkle/SUScheduledUpdateDriver.m @@ -43,10 +43,18 @@ - (void)abortUpdateWithError:(NSError *)error { - if (self.showErrors) + if (self.showErrors) { [super abortUpdateWithError:error]; - else + } else { + // Call delegate separately here because otherwise it won't know we stopped. + // Normally this gets called by the superclass + id updaterDelegate = [self.updater delegate]; + if ([updaterDelegate respondsToSelector:@selector(updater:didAbortWithError:)]) { + [updaterDelegate updater:self.updater didAbortWithError:error]; + } + [self abortUpdate]; + } } @end diff --git a/Frameworks/Sparkle/Sparkle/SUStatusController.m b/Frameworks/Sparkle/Sparkle/SUStatusController.m index b1ca3b039..c7def5d5d 100644 --- a/Frameworks/Sparkle/Sparkle/SUStatusController.m +++ b/Frameworks/Sparkle/Sparkle/SUStatusController.m @@ -31,7 +31,7 @@ - (instancetype)initWithHost:(SUHost *)aHost { - self = [super initWithHost:aHost windowNibName:@"SUStatus"]; + self = [super initWithWindowNibName:@"SUStatus"]; if (self) { self.host = aHost; diff --git a/Frameworks/Sparkle/Sparkle/SUUIBasedUpdateDriver.h b/Frameworks/Sparkle/Sparkle/SUUIBasedUpdateDriver.h index fb1450724..65f374433 100644 --- a/Frameworks/Sparkle/Sparkle/SUUIBasedUpdateDriver.h +++ b/Frameworks/Sparkle/Sparkle/SUUIBasedUpdateDriver.h @@ -15,7 +15,7 @@ @class SUStatusController; -@interface SUUIBasedUpdateDriver : SUBasicUpdateDriver +@interface SUUIBasedUpdateDriver : SUBasicUpdateDriver - (void)showModalAlert:(NSAlert *)alert; - (IBAction)cancelDownload:(id)sender; diff --git a/Frameworks/Sparkle/Sparkle/SUUIBasedUpdateDriver.m b/Frameworks/Sparkle/Sparkle/SUUIBasedUpdateDriver.m index ef631466f..7875dadd9 100644 --- a/Frameworks/Sparkle/Sparkle/SUUIBasedUpdateDriver.m +++ b/Frameworks/Sparkle/Sparkle/SUUIBasedUpdateDriver.m @@ -28,8 +28,9 @@ - (void)didFindValidUpdate { - self.updateAlert = [[SUUpdateAlert alloc] initWithAppcastItem:self.updateItem host:self.host]; - [self.updateAlert setDelegate:self]; + self.updateAlert = [[SUUpdateAlert alloc] initWithAppcastItem:self.updateItem host:self.host completionBlock:^(SUUpdateAlertChoice choice) { + [self updateAlertFinishedWithChoice:choice]; + }]; id versDisp = nil; if ([[self.updater delegate] respondsToSelector:@selector(versionDisplayerForUpdater:)]) { @@ -44,8 +45,7 @@ // If the app is a menubar app or the like, we need to focus it first and alter the // update prompt to behave like a normal window. Otherwise if the window were hidden // there may be no way for the application to be activated to make it visible again. - if ([self.host isBackgroundApplication]) - { + if ([self.host isBackgroundApplication]) { [[self.updateAlert window] setHidesOnDeactivate:NO]; [NSApp activateIgnoringOtherApps:YES]; } @@ -78,12 +78,11 @@ [[NSNotificationCenter defaultCenter] removeObserver:self name:NSApplicationDidBecomeActiveNotification object:NSApp]; } -- (void)updateAlert:(SUUpdateAlert *)__unused alert finishedWithChoice:(SUUpdateAlertChoice)choice +- (void)updateAlertFinishedWithChoice:(SUUpdateAlertChoice)choice { self.updateAlert = nil; [self.host setObject:nil forUserDefaultsKey:SUSkippedVersionKey]; - switch (choice) - { + switch (choice) { case SUInstallUpdateChoice: self.statusController = [[SUStatusController alloc] initWithHost:self.host]; [self.statusController beginActionWithTitle:SULocalizedString(@"Downloading update...", @"Take care not to overflow the status window.") maxProgressValue:0.0 statusText:nil]; @@ -154,16 +153,13 @@ [super extractUpdate]; } -- (void)unarchiver:(SUUnarchiver *)__unused ua extractedLength:(unsigned long)length +- (void)unarchiver:(SUUnarchiver *)__unused ua extractedProgress:(double)progress { // We do this here instead of in extractUpdate so that we only have a determinate progress bar for archives with progress. - if ([self.statusController maxProgressValue] == 0.0) - { - NSDictionary *attributes; - attributes = [[NSFileManager defaultManager] attributesOfItemAtPath:self.downloadPath error:nil]; - [self.statusController setMaxProgressValue:[attributes[NSFileSize] doubleValue]]; + if ([self.statusController maxProgressValue] == 0.0) { + [self.statusController setMaxProgressValue:1]; } - [self.statusController setProgressValue:[self.statusController progressValue] + (double)length]; + [self.statusController setProgressValue:progress]; } - (void)unarchiverDidFinish:(SUUnarchiver *)__unused ua diff --git a/Frameworks/Sparkle/Sparkle/SUUnarchiver.h b/Frameworks/Sparkle/Sparkle/SUUnarchiver.h index 523bdf257..9b7d07807 100644 --- a/Frameworks/Sparkle/Sparkle/SUUnarchiver.h +++ b/Frameworks/Sparkle/Sparkle/SUUnarchiver.h @@ -15,10 +15,10 @@ @interface SUUnarchiver : NSObject @property (copy, readonly) NSString *archivePath; -@property (weak, readonly) SUHost *updateHost; +@property (copy, readonly) NSString *updateHostBundlePath; @property (weak) id delegate; -+ (SUUnarchiver *)unarchiverForPath:(NSString *)path updatingHost:(SUHost *)host; ++ (SUUnarchiver *)unarchiverForPath:(NSString *)path updatingHostBundlePath:(NSString *)host; - (void)start; @end @@ -27,7 +27,7 @@ - (void)unarchiverDidFinish:(SUUnarchiver *)unarchiver; - (void)unarchiverDidFail:(SUUnarchiver *)unarchiver; @optional -- (void)unarchiver:(SUUnarchiver *)unarchiver extractedLength:(unsigned long)length; +- (void)unarchiver:(SUUnarchiver *)unarchiver extractedProgress:(double)progress; @end #endif diff --git a/Frameworks/Sparkle/Sparkle/SUUnarchiver.m b/Frameworks/Sparkle/Sparkle/SUUnarchiver.m index 99f777592..fefa840bf 100644 --- a/Frameworks/Sparkle/Sparkle/SUUnarchiver.m +++ b/Frameworks/Sparkle/Sparkle/SUUnarchiver.m @@ -18,15 +18,15 @@ @implementation SUUnarchiver @synthesize archivePath; -@synthesize updateHost; +@synthesize updateHostBundlePath; @synthesize delegate; -+ (SUUnarchiver *)unarchiverForPath:(NSString *)path updatingHost:(SUHost *)host ++ (SUUnarchiver *)unarchiverForPath:(NSString *)path updatingHostBundlePath:(NSString *)hostPath { for (id current in [self unarchiverImplementations]) { if ([current canUnarchivePath:path]) { - return [[current alloc] initWithPath:path host:host]; + return [[current alloc] initWithPath:path hostBundlePath:hostPath]; } } return nil; @@ -39,12 +39,12 @@ // No-op } -- (instancetype)initWithPath:(NSString *)path host:(SUHost *)host +- (instancetype)initWithPath:(NSString *)path hostBundlePath:(NSString *)hostPath { if ((self = [super init])) { archivePath = [path copy]; - updateHost = host; + updateHostBundlePath = hostPath; } return self; } @@ -54,10 +54,10 @@ return NO; } -- (void)notifyDelegateOfExtractedLength:(size_t)length +- (void)notifyDelegateOfProgress:(double)progress { - if ([self.delegate respondsToSelector:@selector(unarchiver:extractedLength:)]) { - [self.delegate unarchiver:self extractedLength:length]; + if ([self.delegate respondsToSelector:@selector(unarchiver:extractedProgress:)]) { + [self.delegate unarchiver:self extractedProgress:progress]; } } diff --git a/Frameworks/Sparkle/Sparkle/SUUnarchiver_Private.h b/Frameworks/Sparkle/Sparkle/SUUnarchiver_Private.h index e2d66bd08..c29d68cff 100644 --- a/Frameworks/Sparkle/Sparkle/SUUnarchiver_Private.h +++ b/Frameworks/Sparkle/Sparkle/SUUnarchiver_Private.h @@ -16,9 +16,9 @@ + (void)registerImplementation:(Class)implementation; + (NSArray *)unarchiverImplementations; + (BOOL)canUnarchivePath:(NSString *)path; -- (instancetype)initWithPath:(NSString *)path host:(SUHost *)host; +- (instancetype)initWithPath:(NSString *)archive hostBundlePath:(NSString *)host; -- (void)notifyDelegateOfExtractedLength:(size_t)length; +- (void)notifyDelegateOfProgress:(double)progress; - (void)notifyDelegateOfSuccess; - (void)notifyDelegateOfFailure; @end diff --git a/Frameworks/Sparkle/Sparkle/SUUpdateAlert.h b/Frameworks/Sparkle/Sparkle/SUUpdateAlert.h index caf9ccba9..2db57bf49 100644 --- a/Frameworks/Sparkle/Sparkle/SUUpdateAlert.h +++ b/Frameworks/Sparkle/Sparkle/SUUpdateAlert.h @@ -24,10 +24,9 @@ typedef NS_ENUM(NSInteger, SUUpdateAlertChoice) { @class WebView, SUAppcastItem, SUHost; @interface SUUpdateAlert : SUWindowController -@property (weak) id delegate; @property (weak) id versionDisplayer; -- (instancetype)initWithAppcastItem:(SUAppcastItem *)item host:(SUHost *)host; +- (instancetype)initWithAppcastItem:(SUAppcastItem *)item host:(SUHost *)host completionBlock:(void(^)(SUUpdateAlertChoice))c; - (IBAction)installUpdate:sender; - (IBAction)skipThisVersion:sender; @@ -35,10 +34,4 @@ typedef NS_ENUM(NSInteger, SUUpdateAlertChoice) { @end -@protocol SUUpdateAlertDelegate -- (void)updateAlert:(SUUpdateAlert *)updateAlert finishedWithChoice:(SUUpdateAlertChoice)updateChoice; -@optional -- (void)updateAlert:(SUUpdateAlert *)updateAlert shouldAllowAutoUpdate:(BOOL *)shouldAllowAutoUpdate; -@end - #endif diff --git a/Frameworks/Sparkle/Sparkle/SUUpdateAlert.m b/Frameworks/Sparkle/Sparkle/SUUpdateAlert.m index 2ca4093be..0b3610381 100644 --- a/Frameworks/Sparkle/Sparkle/SUUpdateAlert.m +++ b/Frameworks/Sparkle/Sparkle/SUUpdateAlert.m @@ -21,6 +21,7 @@ @property (strong) SUAppcastItem *updateItem; @property (strong) SUHost *host; +@property (strong) void(^completionBlock)(SUUpdateAlertChoice); @property (strong) NSProgressIndicator *releaseNotesSpinner; @property (assign) BOOL webViewFinishedLoading; @@ -36,7 +37,7 @@ @implementation SUUpdateAlert -@synthesize delegate; +@synthesize completionBlock; @synthesize versionDisplayer; @synthesize updateItem; @@ -52,11 +53,12 @@ @synthesize skipButton; @synthesize laterButton; -- (instancetype)initWithAppcastItem:(SUAppcastItem *)item host:(SUHost *)aHost +- (instancetype)initWithAppcastItem:(SUAppcastItem *)item host:(SUHost *)aHost completionBlock:(void (^)(SUUpdateAlertChoice))block { - self = [super initWithHost:host windowNibName:@"SUUpdateAlert"]; + self = [super initWithWindowNibName:@"SUUpdateAlert"]; if (self) { + self.completionBlock = block; host = aHost; updateItem = item; [self setShouldCascadeWindows:NO]; @@ -79,8 +81,8 @@ [self.releaseNotesView setPolicyDelegate:nil]; [self.releaseNotesView removeFromSuperview]; // Otherwise it gets sent Esc presses (why?!) and gets very confused. [self close]; - if ([self.delegate respondsToSelector:@selector(updateAlert:finishedWithChoice:)]) - [self.delegate updateAlert:self finishedWithChoice:choice]; + self.completionBlock(choice); + self.completionBlock = nil; } - (IBAction)installUpdate:(id)__unused sender @@ -164,10 +166,6 @@ if ([self.host objectForInfoDictionaryKey:SUAllowsAutomaticUpdatesKey]) allowAutoUpdates = [self.host boolForInfoDictionaryKey:SUAllowsAutomaticUpdatesKey]; - // Give delegate a chance to modify this choice: - if (self.delegate && [self.delegate respondsToSelector:@selector(updateAlert:shouldAllowAutoUpdate:)]) - [self.delegate updateAlert:self shouldAllowAutoUpdate:&allowAutoUpdates]; - return allowAutoUpdates; } diff --git a/Frameworks/Sparkle/Sparkle/SUUpdatePermissionPrompt.m b/Frameworks/Sparkle/Sparkle/SUUpdatePermissionPrompt.m index 6d31cdf69..6aef7bd2d 100644 --- a/Frameworks/Sparkle/Sparkle/SUUpdatePermissionPrompt.m +++ b/Frameworks/Sparkle/Sparkle/SUUpdatePermissionPrompt.m @@ -45,7 +45,7 @@ - (instancetype)initWithHost:(SUHost *)aHost systemProfile:(NSArray *)profile delegate:(id)d { - self = [super initWithHost:aHost windowNibName:@"SUUpdatePermissionPrompt"]; + self = [super initWithWindowNibName:@"SUUpdatePermissionPrompt"]; if (self) { host = aHost; @@ -65,8 +65,11 @@ // the user would not know why the application was paused. if ([aHost isBackgroundApplication]) { [[NSApplication sharedApplication] activateIgnoringOtherApps:YES]; } - SUUpdatePermissionPrompt *prompt = [[[self class] alloc] initWithHost:aHost systemProfile:profile delegate:d]; - [NSApp runModalForWindow:[prompt window]]; + if( ![NSApp modalWindow]) // do not prompt if there is is another modal window on screen + { + SUUpdatePermissionPrompt *prompt = [[[self class] alloc] initWithHost:aHost systemProfile:profile delegate:d]; + [NSApp runModalForWindow:[prompt window]]; + } } - (NSString *)description { return [NSString stringWithFormat:@"%@ <%@>", [self class], [self.host bundlePath]]; } diff --git a/Frameworks/Sparkle/Sparkle/SUUpdater.h b/Frameworks/Sparkle/Sparkle/SUUpdater.h index f83fed5bf..90d606fa4 100644 --- a/Frameworks/Sparkle/Sparkle/SUUpdater.h +++ b/Frameworks/Sparkle/Sparkle/SUUpdater.h @@ -25,7 +25,7 @@ */ SU_EXPORT @interface SUUpdater : NSObject -@property (weak) IBOutlet id delegate; +@property (unsafe_unretained) IBOutlet id delegate; + (SUUpdater *)sharedUpdater; + (SUUpdater *)updaterForBundle:(NSBundle *)bundle; @@ -46,6 +46,8 @@ SU_EXPORT @interface SUUpdater : NSObject @property (nonatomic, copy) NSString *userAgentString; +@property (copy) NSDictionary *httpHeaders; + @property BOOL sendsSystemProfile; @property BOOL automaticallyDownloadsUpdates; @@ -312,6 +314,14 @@ SU_EXPORT extern NSString *const SUUpdaterAppcastNotificationKey; */ - (void)updater:(SUUpdater *)updater didCancelInstallUpdateOnQuit:(SUAppcastItem *)item; +/*! + Called after an update is aborted due to an error. + + \param updater The SUUpdater instance. + \param error The error that caused the abort + */ +- (void)updater:(SUUpdater *)updater didAbortWithError:(NSError *)error; + @end #endif diff --git a/Frameworks/Sparkle/Sparkle/SUUpdater.m b/Frameworks/Sparkle/Sparkle/SUUpdater.m index 7d0e25d52..ee26656ce 100644 --- a/Frameworks/Sparkle/Sparkle/SUUpdater.m +++ b/Frameworks/Sparkle/Sparkle/SUUpdater.m @@ -51,12 +51,10 @@ NSString *const SUUpdaterAppcastNotificationKey = @"SUUpdaterAppCastNotification @synthesize delegate; @synthesize checkTimer; @synthesize userAgentString = customUserAgentString; - +@synthesize httpHeaders; @synthesize driver; @synthesize host; -#pragma mark Initialization - static NSMutableDictionary *sharedUpdaters = nil; static NSString *const SUUpdaterDefaultsObservationContext = @"SUUpdaterDefaultsObservationContext"; @@ -147,38 +145,28 @@ static NSString *const SUUpdaterDefaultsObservationContext = @"SUUpdaterDefaults - (void)startUpdateCycle { BOOL shouldPrompt = NO; + BOOL hasLaunchedBefore = [self.host boolForUserDefaultsKey:SUHasLaunchedBeforeKey]; // If the user has been asked about automatic checks, don't bother prompting - if ([self.host objectForUserDefaultsKey:SUEnableAutomaticChecksKey]) - { + if ([self.host objectForUserDefaultsKey:SUEnableAutomaticChecksKey]) { shouldPrompt = NO; } // Does the delegate want to take care of the logic for when we should ask permission to update? - else if ([self.delegate respondsToSelector:@selector(updaterShouldPromptForPermissionToCheckForUpdates:)]) - { + else if ([self.delegate respondsToSelector:@selector(updaterShouldPromptForPermissionToCheckForUpdates:)]) { shouldPrompt = [self.delegate updaterShouldPromptForPermissionToCheckForUpdates:self]; } // Has he been asked already? And don't ask if the host has a default value set in its Info.plist. - else if ([self.host objectForKey:SUEnableAutomaticChecksKey] == nil) - { - if ([self.host objectForUserDefaultsKey:SUEnableAutomaticChecksKeyOld]) { - [self setAutomaticallyChecksForUpdates:[self.host boolForUserDefaultsKey:SUEnableAutomaticChecksKeyOld]]; - } + else if ([self.host objectForKey:SUEnableAutomaticChecksKey] == nil) { // Now, we don't want to ask the user for permission to do a weird thing on the first launch. // We wait until the second launch, unless explicitly overridden via SUPromptUserOnFirstLaunchKey. - else if (![self.host objectForKey:SUPromptUserOnFirstLaunchKey]) - { - if ([self.host boolForUserDefaultsKey:SUHasLaunchedBeforeKey] == NO) - [self.host setBool:YES forUserDefaultsKey:SUHasLaunchedBeforeKey]; - else - shouldPrompt = YES; - } - else - shouldPrompt = YES; + shouldPrompt = [self.host objectForKey:SUPromptUserOnFirstLaunchKey] || hasLaunchedBefore; } - if (shouldPrompt) - { + if (!hasLaunchedBefore) { + [self.host setBool:YES forUserDefaultsKey:SUHasLaunchedBeforeKey]; + } + + if (shouldPrompt) { NSArray *profileInfo = [self.host systemProfile]; // Always say we're sending the system profile here so that the delegate displays the parameters it would send. if ([self.delegate respondsToSelector:@selector(feedParametersForUpdater:sendingSystemProfile:)]) { @@ -186,9 +174,7 @@ static NSString *const SUUpdaterDefaultsObservationContext = @"SUUpdaterDefaults } [SUUpdatePermissionPrompt promptWithHost:self.host systemProfile:profileInfo delegate:self]; // We start the update checks and register as observer for changes after the prompt finishes - } - else - { + } else { // We check if the user's said they want updates, or they haven't said anything, and the default is set to checking. [self scheduleNextUpdateCheck]; } @@ -334,9 +320,6 @@ static NSString *const SUUpdaterDefaultsObservationContext = @"SUUpdaterDefaults if ([self updateInProgress]) { return; } if (self.checkTimer) { [self.checkTimer invalidate]; self.checkTimer = nil; } // Timer is non-repeating, may have invalidated itself, so we had to retain it. - SUClearLog(); - SULog(@"===== %@ =====", [[NSFileManager defaultManager] displayNameAtPath:[[NSBundle mainBundle] bundlePath]]); - [self willChangeValueForKey:@"lastUpdateCheckDate"]; [self.host setObject:[NSDate date] forUserDefaultsKey:SULastCheckTimeKey]; [self didChangeValueForKey:@"lastUpdateCheckDate"]; diff --git a/Frameworks/Sparkle/Sparkle/SUUserInitiatedUpdateDriver.m b/Frameworks/Sparkle/Sparkle/SUUserInitiatedUpdateDriver.m index 5dd66f298..d1e0c9a65 100644 --- a/Frameworks/Sparkle/Sparkle/SUUserInitiatedUpdateDriver.m +++ b/Frameworks/Sparkle/Sparkle/SUUserInitiatedUpdateDriver.m @@ -78,16 +78,6 @@ [super abortUpdate]; } -- (void)appcast:(SUAppcast *)ac failedToLoadWithError:(NSError *)error -{ - if (self.isCanceled) - { - [self abortUpdate]; - return; - } - [super appcast:ac failedToLoadWithError:error]; -} - - (BOOL)itemContainsValidUpdate:(SUAppcastItem *)ui { // We don't check to see if this update's been skipped, because the user explicitly *asked* if he had the latest version. diff --git a/Frameworks/Sparkle/Sparkle/SUWindowController.h b/Frameworks/Sparkle/Sparkle/SUWindowController.h index 0cd60191b..f6af6fae3 100644 --- a/Frameworks/Sparkle/Sparkle/SUWindowController.h +++ b/Frameworks/Sparkle/Sparkle/SUWindowController.h @@ -14,7 +14,7 @@ @class SUHost; @interface SUWindowController : NSWindowController // We use this instead of plain old NSWindowController initWithWindowNibName so that we'll be able to find the right path when running in a bundle loaded from another app. -- (instancetype)initWithHost:(SUHost *)host windowNibName:(NSString *)nibName; +- (instancetype)initWithWindowNibName:(NSString *)nibName; @end #endif diff --git a/Frameworks/Sparkle/Sparkle/SUWindowController.m b/Frameworks/Sparkle/Sparkle/SUWindowController.m index 874f76535..553231130 100644 --- a/Frameworks/Sparkle/Sparkle/SUWindowController.m +++ b/Frameworks/Sparkle/Sparkle/SUWindowController.m @@ -11,16 +11,9 @@ @implementation SUWindowController -- (instancetype)initWithHost:(SUHost *)host windowNibName:(NSString *)nibName +- (instancetype)initWithWindowNibName:(NSString *)nibName { - NSString *path = [[NSBundle bundleForClass:[self class]] pathForResource:nibName ofType:@"nib"]; - if (path == nil) // Slight hack to resolve issues with running Sparkle in debug configurations. - { - NSString *frameworkPath = [[[host bundle] sharedFrameworksPath] stringByAppendingPathComponent:@"Sparkle.framework"]; - NSBundle *framework = [NSBundle bundleWithPath:frameworkPath]; - path = [framework pathForResource:nibName ofType:@"nib"]; - } - self = [super initWithWindowNibPath:path owner:self]; + self = [super initWithWindowNibName:nibName owner:self]; return self; } diff --git a/Frameworks/Sparkle/Sparkle/Sparkle.h b/Frameworks/Sparkle/Sparkle/Sparkle.h index 555cb6f64..fe97bae17 100644 --- a/Frameworks/Sparkle/Sparkle/Sparkle.h +++ b/Frameworks/Sparkle/Sparkle/Sparkle.h @@ -18,5 +18,6 @@ #import #import #import +#import #endif diff --git a/Frameworks/Sparkle/Sparkle/Sparkle.pch b/Frameworks/Sparkle/Sparkle/Sparkle.pch index e30bf990c..ccd8c7591 100644 --- a/Frameworks/Sparkle/Sparkle/Sparkle.pch +++ b/Frameworks/Sparkle/Sparkle/Sparkle.pch @@ -10,6 +10,7 @@ #import #import "SUConstants.h" +#import "SUErrors.h" #define SULocalizedString(key, comment) NSLocalizedStringFromTableInBundle(key, @"Sparkle", [NSBundle bundleWithIdentifier:SUBundleIdentifier] ? [NSBundle bundleWithIdentifier:SUBundleIdentifier] : [NSBundle mainBundle], comment) diff --git a/Frameworks/Sparkle/Sparkle/en.lproj/SUUpdatePermissionPrompt.xib b/Frameworks/Sparkle/Sparkle/en.lproj/SUUpdatePermissionPrompt.xib index 149753b1e..c110598fb 100644 --- a/Frameworks/Sparkle/Sparkle/en.lproj/SUUpdatePermissionPrompt.xib +++ b/Frameworks/Sparkle/Sparkle/en.lproj/SUUpdatePermissionPrompt.xib @@ -44,7 +44,7 @@ DQ -