Repository: sparkle-project/Sparkle Branch: 2.x Commit: 59ff700f5178 Files: 577 Total size: 3.1 MB Directory structure: gitextract_utyt973q/ ├── .clang-format ├── .github/ │ ├── ISSUE_TEMPLATE/ │ │ ├── fixes-and-enhancements.md │ │ └── sparkle-doesn-t-work-in-my-app.md │ ├── pull_request_template.md │ └── workflows/ │ ├── ci.yml │ └── create-draft-release.yml ├── .gitignore ├── .gitmodules ├── .swiftlint.yml ├── Autoupdate/ │ ├── AgentConnection.h │ ├── AgentConnection.m │ ├── AppInstaller.h │ ├── AppInstaller.m │ ├── SPUDeltaArchive.h │ ├── SPUDeltaArchive.m │ ├── SPUDeltaArchiveProtocol.h │ ├── SPUDeltaCompressionMode.h │ ├── SPUInstallationInfo.h │ ├── SPUInstallationInfo.m │ ├── SPUInstallationInputData.h │ ├── SPUInstallationInputData.m │ ├── SPUMessageTypes.h │ ├── SPUMessageTypes.m │ ├── SPUSparkleDeltaArchive.h │ ├── SPUSparkleDeltaArchive.m │ ├── SPUXarDeltaArchive.h │ ├── SPUXarDeltaArchive.m │ ├── SUBinaryDeltaApply.h │ ├── SUBinaryDeltaApply.m │ ├── SUBinaryDeltaCommon.h │ ├── SUBinaryDeltaCommon.m │ ├── SUBinaryDeltaCreate.h │ ├── SUBinaryDeltaCreate.m │ ├── SUBinaryDeltaUnarchiver.h │ ├── SUBinaryDeltaUnarchiver.m │ ├── SUCodeSigningVerifier.h │ ├── SUCodeSigningVerifier.m │ ├── SUDiskImageUnarchiver.h │ ├── SUDiskImageUnarchiver.m │ ├── SUFlatPackageUnarchiver.h │ ├── SUFlatPackageUnarchiver.m │ ├── SUGuidedPackageInstaller.h │ ├── SUGuidedPackageInstaller.m │ ├── SUInstaller.h │ ├── SUInstaller.m │ ├── SUInstallerProtocol.h │ ├── SUPipedUnarchiver.h │ ├── SUPipedUnarchiver.m │ ├── SUPlainInstaller.h │ ├── SUPlainInstaller.m │ ├── SUSignatureVerifier.h │ ├── SUSignatureVerifier.m │ ├── SUStatusInfoProtocol.h │ ├── SUUnarchiver.h │ ├── SUUnarchiver.m │ ├── SUUnarchiverNotifier.h │ ├── SUUnarchiverNotifier.m │ ├── SUUnarchiverProtocol.h │ ├── StatusInfo.h │ ├── StatusInfo.m │ └── main.m ├── BinaryDelta/ │ ├── Bridging-Header.h │ └── main.swift ├── CHANGELOG ├── CODE_OF_CONDUCT.md ├── Carthage-dev.json ├── Configurations/ │ ├── CommandLineTool-Debug.xcconfig │ ├── CommandLineTool-Release.xcconfig │ ├── CommandLineTool-Shared.xcconfig │ ├── ConfigCommon.xcconfig │ ├── ConfigCommonCoverage.xcconfig │ ├── ConfigCommonDebug.xcconfig │ ├── ConfigCommonRelease.xcconfig │ ├── ConfigDownloader.xcconfig │ ├── ConfigDownloaderDebug.xcconfig │ ├── ConfigFramework.xcconfig │ ├── ConfigFrameworkDebug.xcconfig │ ├── ConfigFrameworkRelease.xcconfig │ ├── ConfigInstallerConnection.xcconfig │ ├── ConfigInstallerConnectionDebug.xcconfig │ ├── ConfigInstallerLauncher.xcconfig │ ├── ConfigInstallerLauncherDebug.xcconfig │ ├── ConfigInstallerProgress.xcconfig │ ├── ConfigInstallerStatus.xcconfig │ ├── ConfigInstallerStatusDebug.xcconfig │ ├── ConfigRelaunch.xcconfig │ ├── ConfigSparkleTool.xcconfig │ ├── ConfigSwift.xcconfig │ ├── ConfigSwiftDebug.xcconfig │ ├── ConfigSwiftRelease.xcconfig │ ├── ConfigTestApp.xcconfig │ ├── ConfigTestAppDebug.xcconfig │ ├── ConfigTestAppHelper.xcconfig │ ├── ConfigTestAppHelperDebug.xcconfig │ ├── ConfigUITest.xcconfig │ ├── ConfigUITestCoverage.xcconfig │ ├── ConfigUITestDebug.xcconfig │ ├── ConfigUITestRelease.xcconfig │ ├── ConfigUnitTest.xcconfig │ ├── ConfigUnitTestCoverage.xcconfig │ ├── ConfigUnitTestDebug.xcconfig │ ├── ConfigUnitTestRelease.xcconfig │ ├── bsdiff-Debug.xcconfig │ ├── bsdiff-Release.xcconfig │ ├── bsdiff-Shared.xcconfig │ ├── ed25519-Debug.xcconfig │ ├── ed25519-Release.xcconfig │ ├── ed25519-Shared.xcconfig │ ├── generate_latest_changes.py │ ├── link-tools.sh │ ├── make-release-package.sh │ ├── make-xcframework.sh │ ├── release-move-tag.sh │ ├── set-git-version-info.sh │ ├── strip-framework.sh │ └── update-carthage.py ├── Documentation/ │ ├── .gitignore │ ├── API_README.markdown │ ├── Design Practices.md │ ├── Installation.md │ └── Security.md ├── Downloader/ │ ├── Downloader.entitlements │ ├── Info.plist │ ├── SPUDownloader.h │ ├── SPUDownloader.m │ ├── SPUDownloaderDelegate.h │ ├── SPUDownloaderProtocol.h │ └── main.m ├── INSTALL ├── InstallerConnection/ │ ├── Info.plist │ ├── SUInstallerCommunicationProtocol.h │ ├── SUInstallerConnection.h │ ├── SUInstallerConnection.m │ ├── SUInstallerConnectionProtocol.h │ ├── SUXPCInstallerConnection.h │ ├── SUXPCInstallerConnection.m │ └── main.m ├── InstallerLauncher/ │ ├── Info.plist │ ├── SUInstallerLauncher+Private.h │ ├── SUInstallerLauncher.h │ ├── SUInstallerLauncher.m │ ├── SUInstallerLauncherProtocol.h │ ├── SUInstallerLauncherStatus.h │ └── main.m ├── InstallerStatus/ │ ├── Info.plist │ ├── SUInstallerStatus.h │ ├── SUInstallerStatus.m │ ├── SUInstallerStatusProtocol.h │ ├── SUXPCInstallerStatus.h │ ├── SUXPCInstallerStatus.m │ └── main.m ├── LICENSE ├── Makefile ├── Package.swift ├── README.markdown ├── Resources/ │ ├── AppIcon.icon/ │ │ └── icon.json │ ├── Images.xcassets/ │ │ └── AppIcon.appiconset/ │ │ └── Contents.json │ ├── ReleaseNotesColorStyle.css │ ├── SampleAppcast.xml │ └── Sparkle-Icon-2016.sketch ├── Sparkle/ │ ├── AppKitPrevention.h │ ├── Autoupdate/ │ │ ├── TerminationListener.h │ │ └── TerminationListener.m │ ├── Base.lproj/ │ │ └── Sparkle.strings │ ├── CheckLocalizations.swift │ ├── InstallerProgress/ │ │ ├── InstallerProgress-Info.plist │ │ ├── InstallerProgressAppController.h │ │ ├── InstallerProgressAppController.m │ │ ├── InstallerProgressDelegate.h │ │ ├── SPUInstallerAgentProtocol.h │ │ ├── SUInstallerAgentInitiationProtocol.h │ │ ├── ShowInstallerProgress.h │ │ ├── ShowInstallerProgress.m │ │ └── main.m │ ├── SPUAppcastItemState.h │ ├── SPUAppcastItemState.m │ ├── SPUAppcastItemStateResolver+Private.h │ ├── SPUAppcastItemStateResolver.h │ ├── SPUAppcastItemStateResolver.m │ ├── SPUAppcastSigningValidationStatus.h │ ├── SPUAutomaticUpdateDriver.h │ ├── SPUAutomaticUpdateDriver.m │ ├── SPUBasicUpdateDriver.h │ ├── SPUBasicUpdateDriver.m │ ├── SPUCoreBasedUpdateDriver.h │ ├── SPUCoreBasedUpdateDriver.m │ ├── SPUDownloadData.h │ ├── SPUDownloadData.m │ ├── SPUDownloadDataPrivate.h │ ├── SPUDownloadDriver.h │ ├── SPUDownloadDriver.m │ ├── SPUDownloadedUpdate.h │ ├── SPUDownloadedUpdate.m │ ├── SPUExtractSignedFeed.h │ ├── SPUExtractSignedFeed.m │ ├── SPUGentleUserDriverReminders.h │ ├── SPUInformationalUpdate.h │ ├── SPUInformationalUpdate.m │ ├── SPUInstallationType.h │ ├── SPUInstallerDriver.h │ ├── SPUInstallerDriver.m │ ├── SPULocalCacheDirectory.h │ ├── SPULocalCacheDirectory.m │ ├── SPUNoUpdateFoundInfo.h │ ├── SPUNoUpdateFoundInfo.m │ ├── SPUProbeInstallStatus.h │ ├── SPUProbeInstallStatus.m │ ├── SPUProbingUpdateDriver.h │ ├── SPUProbingUpdateDriver.m │ ├── SPUResumableUpdate.h │ ├── SPUScheduledUpdateDriver.h │ ├── SPUScheduledUpdateDriver.m │ ├── SPUSecureCoding.h │ ├── SPUSecureCoding.m │ ├── SPUSkippedUpdate.h │ ├── SPUSkippedUpdate.m │ ├── SPUStandardUpdaterController.h │ ├── SPUStandardUpdaterController.m │ ├── SPUStandardUserDriver+Private.h │ ├── SPUStandardUserDriver.h │ ├── SPUStandardUserDriver.m │ ├── SPUStandardUserDriverDelegate.h │ ├── SPUStandardVersionDisplay.h │ ├── SPUStandardVersionDisplay.m │ ├── SPUUIBasedUpdateDriver.h │ ├── SPUUIBasedUpdateDriver.m │ ├── SPUUpdateCheck.h │ ├── SPUUpdateDriver.h │ ├── SPUUpdatePermissionRequest.h │ ├── SPUUpdatePermissionRequest.m │ ├── SPUUpdater.h │ ├── SPUUpdater.m │ ├── SPUUpdaterCycle.h │ ├── SPUUpdaterCycle.m │ ├── SPUUpdaterDelegate.h │ ├── SPUUpdaterSettings+Debug.h │ ├── SPUUpdaterSettings.h │ ├── SPUUpdaterSettings.m │ ├── SPUUpdaterTimer.h │ ├── SPUUpdaterTimer.m │ ├── SPUUserAgent+Private.h │ ├── SPUUserAgent+Private.m │ ├── SPUUserDriver.h │ ├── SPUUserInitiatedUpdateDriver.h │ ├── SPUUserInitiatedUpdateDriver.m │ ├── SPUUserUpdateState+Private.h │ ├── SPUUserUpdateState.h │ ├── SPUUserUpdateState.m │ ├── SPUVerifierInformation.h │ ├── SPUVerifierInformation.m │ ├── SPUXPCServiceInfo.h │ ├── SPUXPCServiceInfo.m │ ├── SUAppcast+Private.h │ ├── SUAppcast.h │ ├── SUAppcast.m │ ├── SUAppcastDriver.h │ ├── SUAppcastDriver.m │ ├── SUAppcastItem+Private.h │ ├── SUAppcastItem.h │ ├── SUAppcastItem.m │ ├── SUApplicationInfo.h │ ├── SUApplicationInfo.m │ ├── SUConstants.h │ ├── SUConstants.m │ ├── SUErrors.h │ ├── SUExport.h │ ├── SUFileManager.h │ ├── SUFileManager.m │ ├── SUHost.h │ ├── SUHost.m │ ├── SUInstallerProtocol.h │ ├── SULegacyWebView.h │ ├── SULegacyWebView.m │ ├── SULocalizations.h │ ├── SULog+NSError.h │ ├── SULog+NSError.m │ ├── SULog.h │ ├── SULog.m │ ├── SUNormalization.h │ ├── SUNormalization.m │ ├── SUOperatingSystem.h │ ├── SUOperatingSystem.m │ ├── SUPhasedUpdateGroupInfo.h │ ├── SUPhasedUpdateGroupInfo.m │ ├── SUReleaseNotesCommon.h │ ├── SUReleaseNotesCommon.m │ ├── SUReleaseNotesView.h │ ├── SUSignatures.h │ ├── SUSignatures.m │ ├── SUStandardVersionComparator.h │ ├── SUStandardVersionComparator.m │ ├── SUStatus.xib │ ├── SUStatusController.h │ ├── SUStatusController.m │ ├── SUSystemProfiler.h │ ├── SUSystemProfiler.m │ ├── SUTextViewReleaseNotesView.h │ ├── SUTextViewReleaseNotesView.m │ ├── SUTouchBarButtonGroup.h │ ├── SUTouchBarButtonGroup.m │ ├── SUUpdateAlert.h │ ├── SUUpdateAlert.m │ ├── SUUpdateAlert.xib │ ├── SUUpdatePermissionPrompt.h │ ├── SUUpdatePermissionPrompt.m │ ├── SUUpdatePermissionPrompt.xib │ ├── SUUpdatePermissionResponse.h │ ├── SUUpdatePermissionResponse.m │ ├── SUUpdateValidator.h │ ├── SUUpdateValidator.m │ ├── SUUpdater.h │ ├── SUUpdater.m │ ├── SUUpdaterDelegate.h │ ├── SUVersionComparisonProtocol.h │ ├── SUVersionDisplayProtocol.h │ ├── SUWKWebView.h │ ├── SUWKWebView.m │ ├── Sparkle-Info.plist │ ├── Sparkle.h │ ├── Sparkle.private.modulemap │ ├── ar.lproj/ │ │ └── Sparkle.strings │ ├── ca.lproj/ │ │ └── Sparkle.strings │ ├── cs.lproj/ │ │ └── Sparkle.strings │ ├── da.lproj/ │ │ └── Sparkle.strings │ ├── de.lproj/ │ │ └── Sparkle.strings │ ├── el.lproj/ │ │ └── Sparkle.strings │ ├── es.lproj/ │ │ └── Sparkle.strings │ ├── fa.lproj/ │ │ └── Sparkle.strings │ ├── fi.lproj/ │ │ └── Sparkle.strings │ ├── fr.lproj/ │ │ └── Sparkle.strings │ ├── he.lproj/ │ │ └── Sparkle.strings │ ├── hr.lproj/ │ │ └── Sparkle.strings │ ├── hu.lproj/ │ │ └── Sparkle.strings │ ├── is.lproj/ │ │ └── Sparkle.strings │ ├── it.lproj/ │ │ └── Sparkle.strings │ ├── ja.lproj/ │ │ └── Sparkle.strings │ ├── ko.lproj/ │ │ └── Sparkle.strings │ ├── nb.lproj/ │ │ └── Sparkle.strings │ ├── nl.lproj/ │ │ └── Sparkle.strings │ ├── nn.lproj/ │ │ └── Sparkle.strings │ ├── pl.lproj/ │ │ └── Sparkle.strings │ ├── pt-BR.lproj/ │ │ └── Sparkle.strings │ ├── pt-PT.lproj/ │ │ └── Sparkle.strings │ ├── ro.lproj/ │ │ └── Sparkle.strings │ ├── ru.lproj/ │ │ └── Sparkle.strings │ ├── sk.lproj/ │ │ └── Sparkle.strings │ ├── sl.lproj/ │ │ └── Sparkle.strings │ ├── sv.lproj/ │ │ └── Sparkle.strings │ ├── th.lproj/ │ │ └── Sparkle.strings │ ├── tr.lproj/ │ │ └── Sparkle.strings │ ├── uk.lproj/ │ │ └── Sparkle.strings │ ├── vi.lproj/ │ │ └── Sparkle.strings │ ├── zh_CN.lproj/ │ │ └── Sparkle.strings │ ├── zh_HK.lproj/ │ │ └── Sparkle.strings │ └── zh_TW.lproj/ │ └── Sparkle.strings ├── Sparkle.podspec ├── Sparkle.xcodeproj/ │ ├── project.pbxproj │ ├── project.xcworkspace/ │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata/ │ │ ├── IDEWorkspaceChecks.plist │ │ └── swiftpm/ │ │ └── Package.resolved │ └── xcshareddata/ │ └── xcschemes/ │ ├── BinaryDelta.xcscheme │ ├── Distribution.xcscheme │ ├── Sparkle Test App.xcscheme │ ├── Sparkle.xcscheme │ ├── UITests.xcscheme │ ├── generate_appcast.xcscheme │ ├── generate_keys.xcscheme │ ├── sign_update.xcscheme │ └── sparkle-cli.xcscheme ├── TestAppHelper/ │ ├── Info.plist │ ├── TestAppHelper.h │ ├── TestAppHelper.m │ ├── TestAppHelperProtocol.h │ └── main.m ├── TestApplication/ │ ├── AppIcon.icon/ │ │ └── icon.json │ ├── Base.lproj/ │ │ ├── InfoPlist.strings │ │ └── MainMenu.xib │ ├── SUAdHocCodeSigning.h │ ├── SUAdHocCodeSigning.m │ ├── SUInstallUpdateViewController.h │ ├── SUInstallUpdateViewController.m │ ├── SUInstallUpdateViewController.xib │ ├── SUPopUpTitlebarUserDriver.h │ ├── SUPopUpTitlebarUserDriver.m │ ├── SUTestApplicationDelegate.h │ ├── SUTestApplicationDelegate.m │ ├── SUTestWebServer.h │ ├── SUTestWebServer.m │ ├── SUUpdateSettingsWindowController.h │ ├── SUUpdateSettingsWindowController.m │ ├── SUUpdateSettingsWindowController.xib │ ├── Sparkle-Test-App.entitlements │ ├── TestApplication-Info.plist │ ├── ar.lproj/ │ │ └── MainMenu.strings │ ├── ca.lproj/ │ │ └── MainMenu.strings │ ├── cs.lproj/ │ │ └── MainMenu.strings │ ├── da.lproj/ │ │ └── MainMenu.strings │ ├── de.lproj/ │ │ └── MainMenu.strings │ ├── el.lproj/ │ │ └── MainMenu.strings │ ├── en.lproj/ │ │ └── MainMenu.strings │ ├── es.lproj/ │ │ └── MainMenu.strings │ ├── fa.lproj/ │ │ └── MainMenu.strings │ ├── fi.lproj/ │ │ └── MainMenu.strings │ ├── fr.lproj/ │ │ └── MainMenu.strings │ ├── he.lproj/ │ │ └── MainMenu.strings │ ├── hr.lproj/ │ │ └── MainMenu.strings │ ├── hu.lproj/ │ │ └── MainMenu.strings │ ├── is.lproj/ │ │ └── MainMenu.strings │ ├── it.lproj/ │ │ └── MainMenu.strings │ ├── ja.lproj/ │ │ └── MainMenu.strings │ ├── ko.lproj/ │ │ └── MainMenu.strings │ ├── main.m │ ├── nb.lproj/ │ │ └── MainMenu.strings │ ├── nl.lproj/ │ │ └── MainMenu.strings │ ├── nn.lproj/ │ │ └── MainMenu.strings │ ├── pl.lproj/ │ │ └── MainMenu.strings │ ├── pt-BR.lproj/ │ │ └── MainMenu.strings │ ├── pt-PT.lproj/ │ │ └── MainMenu.strings │ ├── ro.lproj/ │ │ └── MainMenu.strings │ ├── ru.lproj/ │ │ └── MainMenu.strings │ ├── sk.lproj/ │ │ └── MainMenu.strings │ ├── sl.lproj/ │ │ └── MainMenu.strings │ ├── sparkletestcast.xml │ ├── sv.lproj/ │ │ └── MainMenu.strings │ ├── th.lproj/ │ │ └── MainMenu.strings │ ├── tr.lproj/ │ │ └── MainMenu.strings │ ├── uk.lproj/ │ │ └── MainMenu.strings │ ├── vi.lproj/ │ │ └── MainMenu.strings │ ├── zh_CN.lproj/ │ │ └── MainMenu.strings │ ├── zh_HK.lproj/ │ │ └── MainMenu.strings │ └── zh_TW.lproj/ │ └── MainMenu.strings ├── Tests/ │ ├── .swiftlint.yml │ ├── Resources/ │ │ ├── DevSignedAppVersion2.dmg │ │ ├── SUUpdateValidatorTest/ │ │ │ ├── Both.bundle/ │ │ │ │ └── Contents/ │ │ │ │ ├── Info.plist │ │ │ │ └── Resources/ │ │ │ │ └── test-pubkey.pem │ │ │ ├── CodeSignedBoth.bundle/ │ │ │ │ └── Contents/ │ │ │ │ ├── Info.plist │ │ │ │ ├── Resources/ │ │ │ │ │ └── test-pubkey.pem │ │ │ │ └── _CodeSignature/ │ │ │ │ ├── CodeDirectory │ │ │ │ ├── CodeRequirements │ │ │ │ ├── CodeRequirements-1 │ │ │ │ ├── CodeResources │ │ │ │ └── CodeSignature │ │ │ ├── CodeSignedBothNew.bundle/ │ │ │ │ └── Contents/ │ │ │ │ ├── Info.plist │ │ │ │ ├── Resources/ │ │ │ │ │ └── test-pubkey.pem │ │ │ │ └── _CodeSignature/ │ │ │ │ ├── CodeDirectory │ │ │ │ ├── CodeRequirements │ │ │ │ ├── CodeRequirements-1 │ │ │ │ ├── CodeResources │ │ │ │ └── CodeSignature │ │ │ ├── CodeSignedInvalid.bundle/ │ │ │ │ └── Contents/ │ │ │ │ ├── Info.plist │ │ │ │ ├── Resources/ │ │ │ │ │ └── test-pubkey.pem │ │ │ │ └── _CodeSignature/ │ │ │ │ ├── CodeDirectory │ │ │ │ ├── CodeRequirements │ │ │ │ ├── CodeRequirements-1 │ │ │ │ ├── CodeResources │ │ │ │ └── CodeSignature │ │ │ ├── CodeSignedInvalidOnly.bundle/ │ │ │ │ └── Contents/ │ │ │ │ ├── Info.plist │ │ │ │ └── _CodeSignature/ │ │ │ │ ├── CodeDirectory │ │ │ │ ├── CodeRequirements │ │ │ │ ├── CodeRequirements-1 │ │ │ │ ├── CodeResources │ │ │ │ └── CodeSignature │ │ │ ├── CodeSignedOldED.bundle/ │ │ │ │ └── Contents/ │ │ │ │ ├── Info.plist │ │ │ │ └── _CodeSignature/ │ │ │ │ ├── CodeDirectory │ │ │ │ ├── CodeRequirements │ │ │ │ ├── CodeRequirements-1 │ │ │ │ ├── CodeResources │ │ │ │ └── CodeSignature │ │ │ ├── CodeSignedOnly.bundle/ │ │ │ │ └── Contents/ │ │ │ │ ├── Info.plist │ │ │ │ └── _CodeSignature/ │ │ │ │ ├── CodeDirectory │ │ │ │ ├── CodeRequirements │ │ │ │ ├── CodeRequirements-1 │ │ │ │ ├── CodeResources │ │ │ │ └── CodeSignature │ │ │ ├── CodeSignedOnlyNew.bundle/ │ │ │ │ └── Contents/ │ │ │ │ ├── Info.plist │ │ │ │ └── _CodeSignature/ │ │ │ │ ├── CodeDirectory │ │ │ │ ├── CodeRequirements │ │ │ │ ├── CodeRequirements-1 │ │ │ │ ├── CodeResources │ │ │ │ └── CodeSignature │ │ │ ├── DSAOnly.bundle/ │ │ │ │ └── Contents/ │ │ │ │ ├── Info.plist │ │ │ │ └── Resources/ │ │ │ │ └── test-pubkey.pem │ │ │ ├── EDOnly.bundle/ │ │ │ │ └── Contents/ │ │ │ │ └── Info.plist │ │ │ ├── None.bundle/ │ │ │ │ └── Contents/ │ │ │ │ └── Info.plist │ │ │ └── resign-all.sh │ │ ├── SparkleTestCodeSignApp.aar │ │ ├── SparkleTestCodeSignApp.dmg │ │ ├── SparkleTestCodeSignApp.enc.aar │ │ ├── SparkleTestCodeSignApp.enc.dmg │ │ ├── SparkleTestCodeSignApp.enc.nolicense.dmg │ │ ├── SparkleTestCodeSignApp.tar.bz2 │ │ ├── SparkleTestCodeSignApp.tar.xz │ │ ├── SparkleTestCodeSign_apfs.dmg │ │ ├── SparkleTestCodeSign_apfs_lzma_aux_files_adhoc.dmg │ │ ├── SparkleTestCodeSign_pkg.dmg │ │ ├── signed-test-file.txt │ │ ├── test-dangerous-link.xml │ │ ├── test-links.xml │ │ ├── test-pubkey.pem │ │ ├── test-relative-urls.xml │ │ ├── test.pkg │ │ ├── testappcast.xml │ │ ├── testappcast_arm64HardwareRequirement.xml │ │ ├── testappcast_channels.xml │ │ ├── testappcast_info_updates.xml │ │ ├── testappcast_minimumAutoupdateVersion.xml │ │ ├── testappcast_minimumAutoupdateVersionSkipping.xml │ │ ├── testappcast_minimumAutoupdateVersionSkipping2.xml │ │ ├── testappcast_minimumUpdateVersion.xml │ │ ├── testappcast_phasedRollout.xml │ │ ├── testlocalizedreleasenotesappcast.xml │ │ ├── testnamespaces.xml │ │ └── testreleasenotes.html │ ├── SUAppcastTest.swift │ ├── SUBinaryDeltaTest.m │ ├── SUCodeSigningVerifierTest.m │ ├── SUFeedSignatureVerifierTest.swift │ ├── SUFileManagerTest.swift │ ├── SUInstallerTest.m │ ├── SUSignatureVerifierTest.m │ ├── SUSpotlightImporterTest.swift │ ├── SUUnarchiverTest.swift │ ├── SUUpdateValidatorTest.swift │ ├── SUUpdaterTest.m │ ├── SUVersionComparisonTest.m │ ├── Sparkle Unit Tests-Bridging-Header.h │ └── SparkleTests-Info.plist ├── UITests/ │ ├── .swiftlint.yml │ ├── SUTestApplicationTest.swift │ └── UITests-Info.plist ├── Vendor/ │ ├── bsdiff/ │ │ ├── bscommon.c │ │ ├── bscommon.h │ │ ├── bsdiff.c │ │ ├── bspatch.c │ │ ├── bspatch.h │ │ ├── sais.c │ │ └── sais.h │ └── ed25519-sparkle/ │ ├── alterations.txt │ ├── license.txt │ ├── readme.md │ └── src/ │ ├── add_scalar.c │ ├── ed25519.h │ ├── fe.c │ ├── fe.h │ ├── fixedint.h │ ├── ge.c │ ├── ge.h │ ├── key_exchange.c │ ├── keypair.c │ ├── precomp_data.h │ ├── sc.c │ ├── sc.h │ ├── seed.c │ ├── sha512.c │ ├── sha512.h │ ├── sign.c │ └── verify.c ├── bin/ │ └── old_dsa_scripts/ │ └── sign_update ├── common_cli/ │ ├── Secret.swift │ └── Signing.swift ├── generate_appcast/ │ ├── Appcast.swift │ ├── ArchiveItem.swift │ ├── Bridging-Header.h │ ├── FeedXML.swift │ ├── URL+Hashing.swift │ ├── Unarchive.swift │ └── main.swift ├── generate_keys/ │ ├── Bridging-Header.h │ └── main.swift ├── sign_update/ │ ├── Bridging-Header.h │ └── main.swift └── sparkle-cli/ ├── Info.plist ├── SPUCommandLineDriver.h ├── SPUCommandLineDriver.m ├── SPUCommandLineUserDriver.h ├── SPUCommandLineUserDriver.m └── main.m ================================================ FILE CONTENTS ================================================ ================================================ FILE: .clang-format ================================================ AccessModifierOffset: -4 AlignEscapedNewlinesLeft: true AlignTrailingComments: false AllowAllParametersOfDeclarationOnNextLine: false AllowShortBlocksOnASingleLine: false AllowShortFunctionsOnASingleLine: None AllowShortIfStatementsOnASingleLine: false AllowShortLoopsOnASingleLine: false AlwaysBreakBeforeMultilineStrings: false AlwaysBreakTemplateDeclarations: false BinPackParameters: true BreakBeforeBinaryOperators: true BreakBeforeBraces: Linux BreakBeforeTernaryOperators: true BreakConstructorInitializersBeforeComma: false ColumnLimit: 0 CommentPragmas: '^ IWYU pragma:' ConstructorInitializerAllOnOneLineOrOnePerLine: false ConstructorInitializerIndentWidth: 4 ContinuationIndentWidth: 4 Cpp11BracedListStyle: false #DerivePointerAlignment: false DisableFormat: false #ForEachMacros: foreach,Q_FOREACH IndentCaseLabels: true IndentFunctionDeclarationAfterType: true IndentWidth: 4 KeepEmptyLinesAtTheStartOfBlocks: false Language: Cpp MaxEmptyLinesToKeep: 1 NamespaceIndentation: All ObjCSpaceAfterProperty: true ObjCSpaceBeforeProtocolList: true #PointerAlignment: Right SpaceBeforeAssignmentOperators: true SpaceBeforeParens: ControlStatements SpaceInEmptyParentheses: false SpacesBeforeTrailingComments: 1 SpacesInAngles: false SpacesInCStyleCastParentheses: false SpacesInContainerLiterals: false SpacesInParentheses: false Standard: Cpp03 TabWidth: 4 UseTab: Never ================================================ FILE: .github/ISSUE_TEMPLATE/fixes-and-enhancements.md ================================================ --- name: Enhancements and Bug Fixes about: Found a bug? Want to implement a new feature? Something else? title: '' assignees: '' --- ## Summary [provide general summary to the issue or enhancement and its rationale] ## Possible Fix [not obligatory, but suggest fixes or reasons for the bug] ## Version [please specify versions of Sparkle this is applicable to] ================================================ FILE: .github/ISSUE_TEMPLATE/sparkle-doesn-t-work-in-my-app.md ================================================ --- name: Sparkle doesn't work in my app about: Problems with integration, unexpected errors using Sparkle title: '' assignees: '' --- ### Description of the problem ### Do you use Sandboxing in your app? ### Version of `Sparkle.framework` in the latest version of your app ### Version of `Sparkle.framework` in the old version of app that your users have (or N/A) ### Sparkle's output from Console.app ``` ``` ### Steps to reproduce the behavior [The more information provided and pasted verbatim, the easier it will be to diagnose an issue. If you can provide the affected application/binary and XML feed to reproduce an issue, share them] ================================================ FILE: .github/pull_request_template.md ================================================ (Insert summary of your pull request here) Fixes # (issue) ## Misc Checklist - [ ] My change requires a documentation update on [Sparkle's website repository](https://github.com/sparkle-project/sparkle-project.github.io) - [ ] My change requires changes to generate_appcast, generate_keys, or sign_update ## Testing I tested and verified my change by using one or multiple of these methods: - [ ] Sparkle Test App - [ ] Unit Tests - [ ] My own app - [ ] Other (please specify) (Describe all the cases that were tested) macOS version tested: [place version here] ================================================ FILE: .github/workflows/ci.yml ================================================ name: Build & Tests on: push: branches: [ 2.x, master ] pull_request: branches: [ 2.x, master ] jobs: build: strategy: matrix: xcode: ['xcode26.2', 'xcode16.4'] include: - xcode: 'xcode16.4' xcode-path: '/Applications/Xcode_16.4.app/Contents/Developer' upload-dist: false run-analyzer: false macos: 'macos-15' - xcode: 'xcode26.2' xcode-path: '/Applications/Xcode_26.2.app/Contents/Developer' upload-dist: true run-analyzer: true macos: 'macos-26' name: Build and Test Sparkle runs-on: ${{ matrix.macos }} permissions: pull-requests: write steps: - name: Checkout uses: actions/checkout@v6 with: submodules: true fetch-depth: 0 - name: Build Unit Tests env: DEVELOPER_DIR: ${{ matrix.xcode-path }} run: | xcodebuild build-for-testing -project Sparkle.xcodeproj -scheme Distribution -enableCodeCoverage YES -derivedDataPath build - name: Run Unit Tests env: DEVELOPER_DIR: ${{ matrix.xcode-path }} run: | xcodebuild test-without-building -project Sparkle.xcodeproj -scheme Distribution -enableCodeCoverage YES -derivedDataPath build - name: Build UI Tests env: DEVELOPER_DIR: ${{ matrix.xcode-path }} run: | xcodebuild build-for-testing -project Sparkle.xcodeproj -scheme UITests -configuration Debug -derivedDataPath build - name: Run UI Tests env: DEVELOPER_DIR: ${{ matrix.xcode-path }} run: | xcodebuild test-without-building -project Sparkle.xcodeproj -scheme UITests -configuration Debug -derivedDataPath build - name: Analyze Sparkle if: ${{ matrix.run-analyzer && github.event_name == 'pull_request' }} env: DEVELOPER_DIR: ${{ matrix.xcode-path }} run: | xcodebuild analyze -project Sparkle.xcodeproj -quiet -scheme Sparkle -configuration Release -derivedDataPath analyze > analyze_output.txt - name: Find Analyzed Warnings if: ${{ success() && matrix.run-analyzer && github.event_name == 'pull_request' }} id: findwarnings env: DEVELOPER_DIR: ${{ matrix.xcode-path }} run: | if grep -q "warning:" analyze_output.txt; then echo "analyzestatus=0" >> $GITHUB_OUTPUT else echo "analyzestatus=1" >> $GITHUB_OUTPUT fi - name: Extract Analyzed Warnings if: ${{ success() && matrix.run-analyzer && github.event_name == 'pull_request' && steps.findwarnings.outputs.analyzestatus == '0' }} id: warnings run: | { echo 'content<> $GITHUB_OUTPUT - name: Post Analyzed Warnings if: ${{ success() && matrix.run-analyzer && github.event_name == 'pull_request' && steps.findwarnings.outputs.analyzestatus == '0' }} uses: mshick/add-pr-comment@v2 with: allow-repeats: false message: "``` ${{ steps.warnings.outputs.content }} ```" - name: Build Release Distribution env: DEVELOPER_DIR: ${{ matrix.xcode-path }} run: | xcodebuild build -project Sparkle.xcodeproj -scheme Distribution -configuration Release -derivedDataPath build - name: Archive Test Results if: failure() uses: actions/upload-artifact@v4 with: name: build-logs path: | build/Logs ~/Library/Logs/DiagnosticReports - name: Upload Distribution if: ${{ success() && matrix.upload-dist }} uses: actions/upload-artifact@v4 with: name: Sparkle-distribution-${{ matrix.xcode }}.tar.xz path: build/Build/Products/Release/sparkle_dist.tar.xz ================================================ FILE: .github/workflows/create-draft-release.yml ================================================ name: "Create Draft Release" env: BUILDDIR: "build" DEVELOPER_DIR: "/Applications/Xcode_26.2.app/Contents/Developer" on: workflow_dispatch: inputs: marketingVersion: description: "Marketing Version" required: true default: "" prereleaseSuffix: description: "Pre-release Suffix" required: false default: "" buildVersion: description: "Product Build" required: true default: "" concurrency: group: publish-release-${{ github.ref }} cancel-in-progress: true jobs: release: name: "Publish binaries for release" runs-on: macos-26 steps: - name: "Checkout sources" uses: actions/checkout@v6 with: token: ${{ secrets.BOT_PERSONAL_ACCESS_TOKEN }} submodules: true fetch-depth: 0 - name: "Extract latest changes from CHANGELOG" run: | ./Configurations/generate_latest_changes.py > latest-changes.txt - name: "Overwrite project versions in project" run: | IFS='.' read major minor patch <<< "${{ github.event.inputs.marketingVersion }}" sed -E -i '' "s/SPARKLE_VERSION_MAJOR =.+/SPARKLE_VERSION_MAJOR = $major/g" ./Configurations/ConfigCommon.xcconfig sed -E -i '' "s/SPARKLE_VERSION_MINOR =.+/SPARKLE_VERSION_MINOR = $minor/g" ./Configurations/ConfigCommon.xcconfig sed -E -i '' "s/SPARKLE_VERSION_PATCH =.+/SPARKLE_VERSION_PATCH = $patch/g" ./Configurations/ConfigCommon.xcconfig if [[ ! -z "${{ github.event.inputs.prereleaseSuffix }}" ]]; then sed -E -i '' "s/SPARKLE_VERSION_SUFFIX =.*/SPARKLE_VERSION_SUFFIX = ${{ github.event.inputs.prereleaseSuffix }}/g" ./Configurations/ConfigCommon.xcconfig else sed -E -i '' "s/SPARKLE_VERSION_SUFFIX =.*/SPARKLE_VERSION_SUFFIX =/g" ./Configurations/ConfigCommon.xcconfig fi sed -E -i '' "s/CURRENT_PROJECT_VERSION =.+/CURRENT_PROJECT_VERSION = ${{ github.event.inputs.buildVersion }}/g" ./Configurations/ConfigCommon.xcconfig git add ./Configurations/ConfigCommon.xcconfig - name: "Determine if this is a pre-release version" run: | if [[ ! -z "${{ github.event.inputs.prereleaseSuffix }}" ]]; then echo "PRERELEASE_VERSION=true" >> $GITHUB_ENV else echo "PRERELEASE_VERSION=false" >> $GITHUB_ENV fi - name: "Set up git and create tag" run: | git config user.name Sparkle-Bot git config user.email sparkle.project.bot@gmail.com git tag "${{ github.event.inputs.marketingVersion }}${{ github.event.inputs.prereleaseSuffix }}" - name: "Build release distribution" run: make release env: GITHUB_ACTOR: ${{ github.actor }} GITHUB_REPOSITORY: ${{ github.repository }} GITHUB_TOKEN: ${{ secrets.BOT_PERSONAL_ACCESS_TOKEN }} - name: "Push the updated package description" env: GITHUB_TOKEN: ${{ secrets.BOT_PERSONAL_ACCESS_TOKEN }} run: git push - name: "Draft a release" uses: softprops/action-gh-release@v2 with: draft: true prerelease: ${{ env.PRERELEASE_VERSION }} target_commitish: ${{ github.ref_name }} name: "${{ github.event.inputs.marketingVersion }}${{ github.event.inputs.prereleaseSuffix }}" tag_name: "${{ github.event.inputs.marketingVersion }}${{ github.event.inputs.prereleaseSuffix }}" fail_on_unmatched_files: true token: ${{ secrets.BOT_PERSONAL_ACCESS_TOKEN }} body_path: latest-changes.txt files: | build/Build/Products/Release/Sparkle-*.tar.xz build/Build/Products/Release/Sparkle-for-Swift-Package-Manager.zip ================================================ FILE: .gitignore ================================================ build/ *.pbxuser !default.pbxuser *.mode1v3 !default.mode1v3 *.mode2v3 !default.mode2v3 *.perspectivev3 !default.perspectivev3 xcuserdata *.xccheckout *.moved-aside DerivedData *.xcuserstate .DS_Store .AppleDouble .LSOverride # Icon must end with two \r Icon # Thumbnails ._* # Files that might appear on external disk .Spotlight-V100 .Trashes # With Carthage, if Sparkle is included as a Git submodule (e.g. with the # --use-submodules option), the main module's .gitignore can't reach all the # way inside the submodule to ignore the added Carthage/Build symlink, and Git # keeps complaining about untracked changes in the submodule forever, which # is very annoying. To avoid that issue, ignore here in Sparkle instead. Carthage/Build # Directories potentially created on remote AFP share .AppleDB .AppleDesktop Network Trash Folder Temporary Items .apdisk # Swift Package Manager /.build .swiftpm ================================================ FILE: .gitmodules ================================================ ================================================ FILE: .swiftlint.yml ================================================ excluded: - Vendor disabled_rules: - opening_brace - empty_parentheses_with_trailing_closure - function_body_length - line_length - cyclomatic_complexity - large_tuple # Rule-specific config trailing_comma: mandatory_comma: true force_try: severity: warning force_cast: severity: warning identifier_name: min_length: warning: 2 ================================================ FILE: Autoupdate/AgentConnection.h ================================================ // // AgentConnection.h // Sparkle // // Created by Mayur Pawashe on 7/17/16. // Copyright © 2016 Sparkle Project. All rights reserved. // #import NS_ASSUME_NONNULL_BEGIN @protocol AgentConnectionDelegate - (void)agentConnectionDidInitiate; - (void)agentConnectionDidInvalidate; @end @protocol SPUInstallerAgentProtocol; SPU_OBJC_DIRECT_MEMBERS @interface AgentConnection : NSObject - (instancetype)initWithHostBundleIdentifier:(NSString *)bundleIdentifier delegate:(id)delegate; - (void)startListener; - (void)invalidate; @property (nonatomic, readonly, nullable) id agent; @property (nonatomic, readonly) BOOL connected; @property (nonatomic, nullable) NSError *invalidationError; @end NS_ASSUME_NONNULL_END ================================================ FILE: Autoupdate/AgentConnection.m ================================================ // // AgentConnection.m // Sparkle // // Created by Mayur Pawashe on 7/17/16. // Copyright © 2016 Sparkle Project. All rights reserved. // #import "AgentConnection.h" #import "SPUMessageTypes.h" #import "SPUInstallerAgentProtocol.h" #import "SUInstallerAgentInitiationProtocol.h" #import "SUCodeSigningVerifier.h" #import "SULog.h" #include "AppKitPrevention.h" @interface AgentConnection () @end @implementation AgentConnection { NSXPCListener *_xpcListener; NSXPCConnection *_activeConnection; __weak id _delegate; } @synthesize agent = _agent; @synthesize connected = _connected; @synthesize invalidationError = _invalidationError; - (instancetype)initWithHostBundleIdentifier:(NSString *)bundleIdentifier delegate:(id)delegate { self = [super init]; if (self != nil) { // Agents should always be the one that connect to daemons due to how mach bootstraps work // For this reason, we are the ones that are creating a listener, not the agent _xpcListener = [[NSXPCListener alloc] initWithMachServiceName:SPUProgressAgentServiceNameForBundleIdentifier(bundleIdentifier)]; _xpcListener.delegate = self; _delegate = delegate; } return self; } - (void)startListener { [_xpcListener resume]; } - (void)invalidate { _delegate = nil; [_activeConnection invalidate]; // Don't need to set _activeConnection to nil, we don't expect new connections [_xpcListener invalidate]; _xpcListener = nil; } - (BOOL)listener:(NSXPCListener *)__unused listener shouldAcceptNewConnection:(NSXPCConnection *)newConnection { if (_activeConnection != nil) { SULog(SULogLevelError, @"Error: Rejecting new connection for agent due already having an active connection"); [newConnection invalidate]; return NO; } // Hardening but not critical for security NSError *validationError = nil; SUValidateConnectionStatus validationStatus = [SUCodeSigningVerifier validateConnection:newConnection error:&validationError]; switch (validationStatus) { case SUValidateConnectionStatusSetCodeSigningRequirementSuccess: break; case SUValidateConnectionStatusSetNoRequirementSuccess: break; case SUValidateConnectionStatusAPIFailure: case SUValidateConnectionStatusCodeSigningRequirementFailure: case SUValidateConectionNoSupportedValidationMethodFailure: SULog(SULogLevelError, @"Error: Rejecting new connection for agent due to failing validation of XPC connection with status %lu and error: %@", validationStatus, validationError.localizedDescription); [newConnection invalidate]; return NO; } newConnection.exportedInterface = [NSXPCInterface interfaceWithProtocol:@protocol(SUInstallerAgentInitiationProtocol)]; newConnection.exportedObject = self; newConnection.remoteObjectInterface = [NSXPCInterface interfaceWithProtocol:@protocol(SPUInstallerAgentProtocol)]; _activeConnection = newConnection; __weak __typeof__(self) weakSelf = self; newConnection.interruptionHandler = ^{ dispatch_async(dispatch_get_main_queue(), ^{ __typeof__(self) strongSelf = weakSelf; if (strongSelf != nil) { [strongSelf->_activeConnection invalidate]; } }); }; newConnection.invalidationHandler = ^{ dispatch_async(dispatch_get_main_queue(), ^{ __typeof__(self) strongSelf = weakSelf; if (strongSelf != nil) { [strongSelf->_delegate agentConnectionDidInvalidate]; } }); }; _agent = newConnection.remoteObjectProxy; [newConnection resume]; return YES; } - (void)connectionDidInitiateWithReply:(void (^)(void))acknowledgement { dispatch_async(dispatch_get_main_queue(), ^{ self->_connected = YES; [self->_delegate agentConnectionDidInitiate]; }); if (acknowledgement != NULL) { acknowledgement(); } } - (void)connectionWillInvalidateWithError:(NSError *)error { dispatch_async(dispatch_get_main_queue(), ^{ self->_invalidationError = error; }); } @end ================================================ FILE: Autoupdate/AppInstaller.h ================================================ // // AppInstaller.h // Sparkle // // Created by Mayur Pawashe on 3/7/16. // Copyright © 2016 Sparkle Project. All rights reserved. // #import #import "SUUnarchiverProtocol.h" NS_ASSUME_NONNULL_BEGIN SPU_OBJC_DIRECT_MEMBERS @interface AppInstaller : NSObject - (instancetype)initWithHostBundleIdentifier:(NSString *)hostBundleIdentifier homeDirectory:(NSString *)homeDirectory userName:(NSString *)userName; - (void)start; - (void)cleanupAndExitWithStatus:(int)status error:(NSError * _Nullable)error __attribute__((noreturn)); @end NS_ASSUME_NONNULL_END ================================================ FILE: Autoupdate/AppInstaller.m ================================================ // // AppInstaller.m // Sparkle // // Created by Mayur Pawashe on 3/7/16. // Copyright © 2016 Sparkle Project. All rights reserved. // #import "AppInstaller.h" #import "SUInstaller.h" #import "SUUpdateValidator.h" #import "SULog.h" #import "SULog+NSError.h" #import "SUHost.h" #import "SULocalizations.h" #import "SUStandardVersionComparator.h" #import "SPUMessageTypes.h" #import "SPUSecureCoding.h" #import "SPUInstallationInputData.h" #import "SUUnarchiver.h" #import "SUFileManager.h" #import "SPUInstallationInfo.h" #import "SUAppcastItem.h" #import "SUErrors.h" #import "SUInstallerCommunicationProtocol.h" #import "AgentConnection.h" #import "SPUInstallerAgentProtocol.h" #import "SPUInstallationType.h" #import "SPULocalCacheDirectory.h" #import "SPUVerifierInformation.h" #import "SUConstants.h" #import "SUCodeSigningVerifier.h" #import #include "AppKitPrevention.h" #define FIRST_UPDATER_MESSAGE_TIMEOUT 18ull #define RETRIEVE_PROCESS_IDENTIFIER_TIMEOUT 8ull /** * Show display progress UI after a delay from starting the final part of the installation. * This should be long enough so that we don't show progress for very fast installations, but * short enough so that we don't leave the user wondering why nothing is happening. */ static const NSTimeInterval SUDisplayProgressTimeDelay = 0.7; @interface AppInstaller () @end @implementation AppInstaller { NSXPCListener* _xpcListener; // Must be synchronized with _newConnectionLock // Set from new connection handler, and also set/read from main thread NSXPCConnection *_activeConnection; id _communicator; AgentConnection *_agentConnection; SUUpdateValidator *_updateValidator; NSString *_hostBundleIdentifier; NSString *_homeDirectory; NSString *_userName; SUHost *_host; NSString *_updateDirectoryPath; NSString *_extractionDirectory; NSString *_downloadName; NSString *_decryptionPassword; SUSignatures *_signatures; NSString *_relaunchPath; NSString *_installationType; SPUVerifierInformation *_verifierInformation; id _installer; dispatch_queue_t _installerQueue; os_unfair_lock _newConnectionLock; #if SPARKLE_BUILD_PACKAGE_SUPPORT // Must be synchronized with _newConnectionLock // Set from new connection handler, read from main thread BOOL _connectionCodeSigningValidationSkipped; #endif BOOL _shouldRelaunch; BOOL _shouldShowUI; BOOL _receivedUpdaterPong; BOOL _willCompleteInstallation; BOOL _receivedInstallationData; BOOL _finishedValidation; BOOL _agentInitiatedConnection; // Setting _performedStage1Installation on main thread must be synchronzied with reading it from new connection handler BOOL _performedStage1Installation; BOOL _performedStage2Installation; BOOL _performedStage3Installation; BOOL _targetTerminated; } - (instancetype)initWithHostBundleIdentifier:(NSString *)hostBundleIdentifier homeDirectory:(NSString *)homeDirectory userName:(NSString *)userName { if (!(self = [super init])) { return nil; } _newConnectionLock = OS_UNFAIR_LOCK_INIT; _hostBundleIdentifier = [hostBundleIdentifier copy]; _homeDirectory = [homeDirectory copy]; _userName = [userName copy]; _xpcListener = [[NSXPCListener alloc] initWithMachServiceName:SPUInstallerServiceNameForBundleIdentifier(hostBundleIdentifier)]; _xpcListener.delegate = self; _agentConnection = [[AgentConnection alloc] initWithHostBundleIdentifier:hostBundleIdentifier delegate:self]; return self; } - (BOOL)listener:(NSXPCListener *)__unused listener shouldAcceptNewConnection:(NSXPCConnection *)newConnection { os_unfair_lock_lock(&_newConnectionLock); { if (_activeConnection != nil) { os_unfair_lock_unlock(&_newConnectionLock); SULog(SULogLevelError, @"Error: Rejecting multiple XPC connections for installer..."); [newConnection invalidate]; return NO; } #if SPARKLE_BUILD_PACKAGE_SUPPORT BOOL connectionCodeSigningValidationSkipped = NO; #endif // It's safe to allow any connections once stage 1 installation is complete // This is to allow general updaters to resume the installation. if (!_performedStage1Installation) { BOOL passesValidation; NSError *validationError = nil; SUValidateConnectionStatus status = [SUCodeSigningVerifier validateConnection:newConnection error:&validationError]; switch (status) { case SUValidateConnectionStatusSetCodeSigningRequirementSuccess: passesValidation = YES; break; case SUValidateConnectionStatusSetNoRequirementSuccess: passesValidation = YES; #if SPARKLE_BUILD_PACKAGE_SUPPORT connectionCodeSigningValidationSkipped = YES; #endif break; case SUValidateConnectionStatusAPIFailure: case SUValidateConnectionStatusCodeSigningRequirementFailure: case SUValidateConectionNoSupportedValidationMethodFailure: passesValidation = NO; break; } if (!passesValidation) { os_unfair_lock_unlock(&_newConnectionLock); SULog(SULogLevelError, @"Error: Rejecting new connection for installer due to failing validation of XPC connection with status %lu and error: %@", status, validationError.localizedDescription); [newConnection invalidate]; return NO; } } #if SPARKLE_BUILD_PACKAGE_SUPPORT _connectionCodeSigningValidationSkipped = connectionCodeSigningValidationSkipped; #endif _activeConnection = newConnection; } os_unfair_lock_unlock(&_newConnectionLock); newConnection.exportedInterface = [NSXPCInterface interfaceWithProtocol:@protocol(SUInstallerCommunicationProtocol)]; newConnection.exportedObject = self; newConnection.remoteObjectInterface = [NSXPCInterface interfaceWithProtocol:@protocol(SUInstallerCommunicationProtocol)]; __weak __typeof__(self) weakSelf = self; newConnection.interruptionHandler = ^{ dispatch_async(dispatch_get_main_queue(), ^{ __typeof__(self) strongSelf = weakSelf; if (strongSelf != nil) { os_unfair_lock_lock(&strongSelf->_newConnectionLock); NSXPCConnection *activeConnection = strongSelf->_activeConnection; os_unfair_lock_unlock(&strongSelf->_newConnectionLock); [activeConnection invalidate]; } }); }; newConnection.invalidationHandler = ^{ dispatch_async(dispatch_get_main_queue(), ^{ __typeof__(self) strongSelf = weakSelf; if (strongSelf != nil) { if (strongSelf->_activeConnection != nil && !strongSelf->_willCompleteInstallation) { [strongSelf cleanupAndExitWithStatus:EXIT_FAILURE error:[NSError errorWithDomain:SUSparkleErrorDomain code:SPUInstallerError userInfo:@{ NSLocalizedDescriptionKey: @"Invalidation on remote port being called, and installation is not close enough to completion!" }]]; } strongSelf->_communicator = nil; os_unfair_lock_lock(&strongSelf->_newConnectionLock); strongSelf->_activeConnection = nil; os_unfair_lock_unlock(&strongSelf->_newConnectionLock); } }); }; // _communicator is used only on main thread dispatch_async(dispatch_get_main_queue(), ^{ self->_communicator = newConnection.remoteObjectProxy; [newConnection resume]; }); return YES; } - (void)start { [_xpcListener resume]; [_agentConnection startListener]; dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(FIRST_UPDATER_MESSAGE_TIMEOUT * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ if (!self->_receivedInstallationData) { [self cleanupAndExitWithStatus:EXIT_FAILURE error:[NSError errorWithDomain:SUSparkleErrorDomain code:SPUInstallerError userInfo:@{ NSLocalizedDescriptionKey: @"Timeout: installation data was never received" }]]; } if (!self->_agentConnection.connected) { [self cleanupAndExitWithStatus:EXIT_FAILURE error:[NSError errorWithDomain:SUSparkleErrorDomain code:SPUInstallerError userInfo:@{ NSLocalizedDescriptionKey: @"Timeout: agent connection was never initiated" }]]; } }); } - (void)extractAndInstallUpdate SPU_OBJC_DIRECT { [_communicator handleMessageWithIdentifier:SPUExtractionStarted data:[NSData data]]; NSString *archivePath = [_updateDirectoryPath stringByAppendingPathComponent:_downloadName]; id unarchiver = [SUUnarchiver unarchiverForPath:archivePath extractionDirectory:_extractionDirectory updatingHostBundlePath:_host.bundlePath decryptionPassword:_decryptionPassword expectingInstallationType:_installationType]; NSError *prevalidationError = nil; BOOL success = NO; if (!unarchiver) { prevalidationError = [NSError errorWithDomain:SUSparkleErrorDomain code:SUUnarchivingError userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"No valid unarchiver was found for %@", archivePath] }]; success = NO; } else { NSError *fileAttributesError = nil; NSDictionary *archiveFileAttributes = [NSFileManager.defaultManager attributesOfItemAtPath:archivePath error:&fileAttributesError]; if (archiveFileAttributes == nil) { SULog(SULogLevelError, @"Failed to retrieve file attributes from archive: %@.", fileAttributesError); } else { _verifierInformation.actualContentLength = (uint64_t)(archiveFileAttributes.fileSize); } _updateValidator = [[SUUpdateValidator alloc] initWithDownloadPath:archivePath signatures:_signatures host:_host verifierInformation:_verifierInformation]; // More uncommon archives types (.aar, .yaa) need SUVerifyUpdateBeforeExtraction BOOL verifyBeforeExtraction = [_host boolForInfoDictionaryKey:SUVerifyUpdateBeforeExtractionKey]; if (!verifyBeforeExtraction && unarchiver.needsVerifyBeforeExtractionKey) { prevalidationError = [NSError errorWithDomain:SUSparkleErrorDomain code:SUValidationError userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Extracting %@ archives require setting %@ to YES in the old app. Please visit https://sparkle-project.org/documentation/customization/ for more information.", archivePath.pathExtension, SUVerifyUpdateBeforeExtractionKey] }]; success = NO; } else { // Delta, package updates, and apps with SUVerifyUpdateBeforeExtraction will require validation before extraction // Otherwise normal application updates are a bit more lenient allowing developers to change one of apple dev ID or EdDSA keys after extraction BOOL archiveTypeMustValidateBeforeExtraction = [[unarchiver class] mustValidateBeforeExtraction]; BOOL needsPrevalidation = verifyBeforeExtraction || archiveTypeMustValidateBeforeExtraction || ![_installationType isEqualToString:SPUInstallationTypeApplication]; if (needsPrevalidation) { // EdDSA signing is required, so host must have public keys if (![_updateValidator validateHostHasPublicKeys:&prevalidationError]) { success = NO; } else { // Falling back on code signing for prevalidation requires SUVerifyUpdateBeforeExtraction // and that update is a regular app update, and not a delta update BOOL fallbackOnCodeSigning = (verifyBeforeExtraction && !archiveTypeMustValidateBeforeExtraction && [_installationType isEqualToString:SPUInstallationTypeApplication]); success = [_updateValidator validateDownloadPathWithFallbackOnCodeSigning:fallbackOnCodeSigning error:&prevalidationError]; } } else { success = YES; } } } if (!success) { [self unarchiverDidFailWithError:prevalidationError]; } else { [unarchiver unarchiveWithCompletionBlock:^(NSError * _Nullable error) { if (error != nil) { [self unarchiverDidFailWithError:[NSError errorWithDomain:SUSparkleErrorDomain code:SUUnarchivingError userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Failed to unarchive %@", archivePath], NSUnderlyingErrorKey: (NSError * _Nonnull)error }]]; } else { [self->_communicator handleMessageWithIdentifier:SPUValidationStarted data:[NSData data]]; NSError *validationError = nil; BOOL validationSuccess = [self->_updateValidator validateWithUpdateDirectory:self->_extractionDirectory error:&validationError]; if (!validationSuccess) { [self cleanupAndExitWithStatus:EXIT_FAILURE error:[NSError errorWithDomain:SUSparkleErrorDomain code:SPUInstallerError userInfo:@{ NSLocalizedDescriptionKey: @"Update validation was a failure", NSUnderlyingErrorKey: validationError }]]; } else { [self->_communicator handleMessageWithIdentifier:SPUInstallationStartedStage1 data:[NSData data]]; self->_finishedValidation = YES; if (self->_agentInitiatedConnection) { [self retrieveProcessIdentifierAndStartInstallation]; } } } } progressBlock:^(double progress) { if (sizeof(progress) == sizeof(uint64_t)) { uint64_t progressValue = CFSwapInt64HostToLittle(*(uint64_t *)&progress); NSData *data = [NSData dataWithBytes:&progressValue length:sizeof(progressValue)]; [self->_communicator handleMessageWithIdentifier:SPUExtractedArchiveWithProgress data:data]; } } waitForCleanup:NO]; } } - (void)clearUpdateDirectory SPU_OBJC_DIRECT { if (_updateDirectoryPath != nil) { NSError *theError = nil; if (![[[SUFileManager alloc] init] removeItemAtURL:[NSURL fileURLWithPath:_updateDirectoryPath] error:&theError]) { SULog(SULogLevelError, @"Couldn't remove update folder: %@.", theError); } _updateDirectoryPath = nil; } } - (void)unarchiverDidFailWithError:(NSError *)error SPU_OBJC_DIRECT { SULog(SULogLevelError, @"Failed to unarchive file"); SULogError(error); // No longer need update validator until next possible extraction (eg: if initial delta update fails) _updateValidator = nil; // Client could try update again with different inputs // Eg: one common case is if a delta update fails, client may want to fall back to regular update // We really only need to set updateDirectoryPath to nil since that's the field we check if we've received installation data, // but may as well set other fields to nil too [self clearUpdateDirectory]; _downloadName = nil; _extractionDirectory = nil; _decryptionPassword = nil; _signatures = nil; _relaunchPath = nil; _host = nil; NSData *archivedError = SPUArchiveRootObjectSecurely(error); [_communicator handleMessageWithIdentifier:SPUArchiveExtractionFailed data:archivedError != nil ? archivedError : [NSData data]]; } - (void)agentConnectionDidInitiate { _agentInitiatedConnection = YES; if (_finishedValidation) { [self retrieveProcessIdentifierAndStartInstallation]; } } - (void)agentConnectionDidInvalidate { if (!_finishedValidation || !_agentInitiatedConnection || !_targetTerminated) { NSMutableDictionary *userInfo = [NSMutableDictionary dictionaryWithDictionary:@{ NSLocalizedDescriptionKey: @"Error: Agent connection invalidated before installation began" }]; NSError *agentError = _agentConnection.invalidationError; if (agentError != nil) { userInfo[NSUnderlyingErrorKey] = agentError; } [self cleanupAndExitWithStatus:EXIT_FAILURE error:[NSError errorWithDomain:SUSparkleErrorDomain code:SPUInstallerError userInfo:userInfo]]; } } - (void)retrieveProcessIdentifierAndStartInstallation SPU_OBJC_DIRECT { // We use the relaunch path for the bundle to listen for termination instead of the host path // For a plug-in this makes a big difference; we want to wait until the app hosting the plug-in terminates // Otherwise for an app, the relaunch path and host path should be identical __block BOOL receivedResponse = NO; [_agentConnection.agent registerApplicationBundlePath:_relaunchPath reply:^(BOOL targetTerminated) { dispatch_async(dispatch_get_main_queue(), ^{ receivedResponse = YES; if (!targetTerminated) { [self->_agentConnection.agent listenForTerminationWithCompletion:^{ dispatch_async(dispatch_get_main_queue(), ^{ self->_targetTerminated = YES; if (self->_performedStage1Installation) { [self finishInstallationAfterHostTermination]; } }); }]; } else { self->_targetTerminated = YES; } [self startInstallation]; }); }]; dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(RETRIEVE_PROCESS_IDENTIFIER_TIMEOUT * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ if (!receivedResponse) { [self cleanupAndExitWithStatus:EXIT_FAILURE error:[NSError errorWithDomain:SUSparkleErrorDomain code:SPUInstallerError userInfo:@{ NSLocalizedDescriptionKey: @"Timeout error: failed to retrieve process identifier from agent" }]]; } }); } - (void)handleMessageWithIdentifier:(int32_t)identifier data:(NSData *)data { if (identifier == SPUInstallationData && _updateDirectoryPath == nil) { dispatch_async(dispatch_get_main_queue(), ^{ // Mark that we have received the installation data // Do not rely on eg: self->_updateDirectoryPath != nil because we may set it to nil again if an early stage fails (i.e, archive extraction) self->_receivedInstallationData = YES; SPUInstallationInputData *installationData = (data != nil) ? (SPUInstallationInputData *)SPUUnarchiveRootObjectSecurely(data, [SPUInstallationInputData class]) : nil; if (installationData == nil) { [self cleanupAndExitWithStatus:EXIT_FAILURE error:[NSError errorWithDomain:SUSparkleErrorDomain code:SPUInstallerError userInfo:@{ NSLocalizedDescriptionKey: @"Error: Failed to unarchive input installation data" }]]; return; } NSString *installationType = installationData.installationType; if (!SPUValidInstallationType(installationType)) { [self cleanupAndExitWithStatus:EXIT_FAILURE error:[NSError errorWithDomain:SUSparkleErrorDomain code:SPUInstallerError userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Error: Received invalid installation type: %@", installationType] }]]; return; } NSBundle *hostBundle = [NSBundle bundleWithPath:installationData.hostBundlePath]; NSString *bundleIdentifier = hostBundle.bundleIdentifier; if (bundleIdentifier == nil || ![bundleIdentifier isEqualToString:self->_hostBundleIdentifier]) { [self cleanupAndExitWithStatus:EXIT_FAILURE error:[NSError errorWithDomain:SUSparkleErrorDomain code:SPUInstallerError userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Error: Failed to match host bundle identifiers %@ and %@", self->_hostBundleIdentifier, bundleIdentifier] }]]; return; } // This will be important later if (installationData.relaunchPath == nil) { [self cleanupAndExitWithStatus:EXIT_FAILURE error:[NSError errorWithDomain:SUSparkleErrorDomain code:SPUInstallerError userInfo:@{ NSLocalizedDescriptionKey: @"Error: Failed to obtain relaunch path from installation data" }]]; return; } // This installation path is specific to sparkle and the bundle identifier NSString *rootCacheInstallationPath = [[SPULocalCacheDirectory cachePathForBundleIdentifier:bundleIdentifier] stringByAppendingPathComponent:@"Installation"]; [SPULocalCacheDirectory removeOldItemsInDirectory:rootCacheInstallationPath]; NSString *cacheInstallationPath = [SPULocalCacheDirectory createUniqueDirectoryInDirectory:rootCacheInstallationPath]; if (cacheInstallationPath == nil) { [self cleanupAndExitWithStatus:EXIT_FAILURE error:[NSError errorWithDomain:SUSparkleErrorDomain code:SPUInstallerError userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Error: Failed to create installation cache directory in %@", rootCacheInstallationPath] }]]; return; } // Resolve the bookmark data for the downloaded update // See "Share file access between processes with URL bookmarks" in https://developer.apple.com/documentation/security/app_sandbox/accessing_files_from_the_macos_app_sandbox BOOL isStale = NO; NSError *bookmarkError = nil; NSURL *downloadURL = [NSURL URLByResolvingBookmarkData:installationData.updateURLBookmarkData options:NSURLBookmarkResolutionWithoutUI relativeToURL:nil bookmarkDataIsStale:&isStale error:&bookmarkError]; if (downloadURL == nil) { [self cleanupAndExitWithStatus:EXIT_FAILURE error:[NSError errorWithDomain:SUSparkleErrorDomain code:SPUInstallerError userInfo:@{ NSLocalizedDescriptionKey: @"Error: Failed to resolve bookmark data from downloaded update", NSUnderlyingErrorKey: bookmarkError }]]; return; } // Validate the download URL before moving it { NSArray *downloadURLPathComponents = downloadURL.URLByResolvingSymlinksInPath.pathComponents; if (downloadURLPathComponents == nil) { [self cleanupAndExitWithStatus:EXIT_FAILURE error:[NSError errorWithDomain:SUSparkleErrorDomain code:SPUInstallerError userInfo:@{ NSLocalizedDescriptionKey: @"Error: Failed to retrieve path components from download URL" }]]; return; } if ([downloadURLPathComponents containsObject:@".."]) { [self cleanupAndExitWithStatus:EXIT_FAILURE error:[NSError errorWithDomain:SUSparkleErrorDomain code:SPUInstallerError userInfo:@{ NSLocalizedDescriptionKey: @"Error: download URL path components contains '..' which is unsafe" }]]; return; } if (![downloadURLPathComponents containsObject:@SPARKLE_BUNDLE_IDENTIFIER] || ![downloadURLPathComponents containsObject:@"PersistentDownloads"]) { [self cleanupAndExitWithStatus:EXIT_FAILURE error:[NSError errorWithDomain:SUSparkleErrorDomain code:SPUInstallerError userInfo:@{ NSLocalizedDescriptionKey: @"Error: download URL path components does not contain PersistentDownloads or "@SPARKLE_BUNDLE_IDENTIFIER }]]; return; } } if (!isStale) { SULog(SULogLevelError, @"Error: bookmark data for update download is stale.. but still continuing."); } NSString *originalDownloadName = downloadURL.lastPathComponent; if (originalDownloadName == nil) { [self cleanupAndExitWithStatus:EXIT_FAILURE error:[NSError errorWithDomain:SUSparkleErrorDomain code:SPUInstallerError userInfo:@{ NSLocalizedDescriptionKey: @"Error: Failed to retrieve download name from download URL" }]]; return; } // Randomize the download name if possible // This adds better security if there are any vulnerabilities in extracting/executing archives // which allow writing in unexpected locations. For zip/tar/dmg archives we may also extract them before // performing signing validation (due to key rotation). NSString *downloadName; NSString *randomizedUUIDString = [[NSUUID UUID] UUIDString]; if (randomizedUUIDString != nil) { // Find the real path extension of the download name // We cannot use -[NSString pathExtension] because it may not give us the full path extension // E.g. for "foo.tar.xz" we need "tar.xz", not "xz" NSString *downloadPathExtension; NSRange pathExtensionDelimiterRange = [originalDownloadName rangeOfString:@"."]; if (pathExtensionDelimiterRange.location == NSNotFound) { downloadPathExtension = @""; } else { downloadPathExtension = [originalDownloadName substringFromIndex:pathExtensionDelimiterRange.location + 1]; } NSString *randomizedDownloadName = [randomizedUUIDString stringByAppendingPathExtension:downloadPathExtension]; if (randomizedDownloadName != nil) { downloadName = randomizedDownloadName; } else { downloadName = originalDownloadName; } } else { downloadName = originalDownloadName; } // Move the download archive to somewhere where probably only we will be touching it // This prevents eg: if a bug exists in the updater that removes files we are trying to install // When this tool is ran as root, we are moving it into a directory that only root will have access to NSURL *downloadDestinationURL = [[NSURL fileURLWithPath:cacheInstallationPath] URLByAppendingPathComponent:downloadName]; NSError *moveError = nil; if (![[[SUFileManager alloc] init] moveItemAtURL:downloadURL toURL:downloadDestinationURL error:&moveError]) { [self cleanupAndExitWithStatus:EXIT_FAILURE error:[NSError errorWithDomain:SUSparkleErrorDomain code:SPUInstallerError userInfo:@{ NSLocalizedDescriptionKey: @"Error: Failed to move download archive to new location", NSUnderlyingErrorKey: moveError }]]; return; } // Make sure the downloaded archive we moved over is a regular file and not a symbolic link placed by an attacker NSError *attributesError = nil; NSString *downloadDestinationPath = downloadDestinationURL.path; if (downloadDestinationPath == nil) { [self cleanupAndExitWithStatus:EXIT_FAILURE error:[NSError errorWithDomain:SUSparkleErrorDomain code:SPUInstallerError userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Error: Failed to retrieve download archive path from %@", downloadDestinationURL] }]]; return; } NSDictionary *archiveAttributes = [[NSFileManager defaultManager] attributesOfItemAtPath:downloadDestinationPath error:&attributesError]; if (archiveAttributes == nil) { [self cleanupAndExitWithStatus:EXIT_FAILURE error:[NSError errorWithDomain:SUSparkleErrorDomain code:SPUInstallerError userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Error: Failed to retrieve download archive attributes from %@", downloadDestinationPath] }]]; return; } if (![(NSString *)archiveAttributes[NSFileType] isEqualToString:NSFileTypeRegular]) { [self cleanupAndExitWithStatus:EXIT_FAILURE error:[NSError errorWithDomain:SUSparkleErrorDomain code:SPUInstallerError userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Error: Received bad archive file type: %@", archiveAttributes[NSFileType]] }]]; return; } NSString *extractionDirectory = [SPULocalCacheDirectory createUniqueDirectoryInDirectory:cacheInstallationPath]; if (extractionDirectory == nil) { [self cleanupAndExitWithStatus:EXIT_FAILURE error:[NSError errorWithDomain:SUSparkleErrorDomain code:SPUInstallerError userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Error: Failed to create installation extraction directory in %@", cacheInstallationPath] }]]; return; } // Carry these properties separately rather than using the SUInstallationInputData object // Some of our properties may slightly differ than our input and we don't want to make the mistake of using one of those self->_installationType = installationType; self->_relaunchPath = installationData.relaunchPath; self->_downloadName = downloadName; self->_signatures = installationData.signatures; self->_updateDirectoryPath = cacheInstallationPath; self->_extractionDirectory = extractionDirectory; self->_decryptionPassword = installationData.decryptionPassword; self->_host = [[SUHost alloc] initWithBundle:hostBundle]; self->_verifierInformation = [[SPUVerifierInformation alloc] initWithExpectedVersion:installationData.expectedVersion expectedContentLength:installationData.expectedContentLength]; [self extractAndInstallUpdate]; }); } else if (identifier == SPUSentUpdateAppcastItemData) { SUAppcastItem *updateItem = (data != nil) ? (SUAppcastItem *)SPUUnarchiveRootObjectSecurely(data, [SUAppcastItem class]) : nil; if (updateItem != nil) { SPUInstallationInfo *installationInfo = [[SPUInstallationInfo alloc] initWithAppcastItem:updateItem]; NSData *archivedData = SPUArchiveRootObjectSecurely(installationInfo); if (archivedData != nil) { [_agentConnection.agent registerInstallationInfoData:archivedData]; } } } else if (identifier == SPUResumeInstallationToStage2 && data.length == sizeof(uint8_t) * 2) { // Because anyone can ask us to resume the installation, it may be wise to think about backwards compatibility here if IPC changes uint8_t relaunch = *((const uint8_t *)data.bytes); uint8_t showsUI = *((const uint8_t *)data.bytes + 1); dispatch_async(dispatch_get_main_queue(), ^{ // This flag has an impact on showing UI progress during installations self->_shouldShowUI = (BOOL)showsUI; // Don't test if the application was alive initially, leave that to the progress agent if we decide to relaunch self->_shouldRelaunch = (BOOL)relaunch; if (self->_performedStage1Installation) { // Resume the installation if we aren't done with stage 2 yet, and remind the client we are prepared to relaunch dispatch_async(self->_installerQueue, ^{ if (!self->_performedStage2Installation) { [self performStage2Installation]; } else if (!self->_performedStage3Installation) { // If we already performed the 2nd stage, re-purpose this request to re-try sending another termination signal dispatch_async(dispatch_get_main_queue(), ^{ // Don't check if the target is already terminated, leave that to the progress agent // We could be slightly off if there were multiple instances running [self->_agentConnection.agent sendTerminationSignal]; }); } }); } }); } else if (identifier == SPUCancelInstallation) { dispatch_async(dispatch_get_main_queue(), ^{ [self cleanupAndExitWithStatus:0 error:nil]; }); } else if (identifier == SPUUpdaterAlivePong) { _receivedUpdaterPong = YES; } } - (void)startInstallation SPU_OBJC_DIRECT { _willCompleteInstallation = YES; dispatch_queue_attr_t queuePriority = dispatch_queue_attr_make_with_qos_class(DISPATCH_QUEUE_SERIAL, QOS_CLASS_USER_INITIATED, 0); _installerQueue = dispatch_queue_create("org.sparkle-project.sparkle.installer", queuePriority); #if SPARKLE_BUILD_PACKAGE_SUPPORT os_unfair_lock_lock(&_newConnectionLock); BOOL connectionCodeSigningValidationSkipped = self->_connectionCodeSigningValidationSkipped; os_unfair_lock_unlock(&_newConnectionLock); #else BOOL connectionCodeSigningValidationSkipped = NO; #endif dispatch_async(_installerQueue, ^{ NSError *installerError = nil; id installer = [SUInstaller installerForHost:self->_host expectedInstallationType:self->_installationType updateDirectory:self->_extractionDirectory connectionCodeSigningValidationSkipped:connectionCodeSigningValidationSkipped homeDirectory:self->_homeDirectory userName:self->_userName error:&installerError]; if (installer == nil) { dispatch_async(dispatch_get_main_queue(), ^{ [self cleanupAndExitWithStatus:EXIT_FAILURE error:[NSError errorWithDomain:SUSparkleErrorDomain code:SPUInstallerError userInfo:@{ NSLocalizedDescriptionKey: @"Error: Failed to create installer instance", NSUnderlyingErrorKey: installerError }]]; }); return; } NSError *firstStageError = nil; if (![installer performInitialInstallation:&firstStageError]) { self->_installer = nil; dispatch_async(dispatch_get_main_queue(), ^{ [self cleanupAndExitWithStatus:EXIT_FAILURE error:[NSError errorWithDomain:SUSparkleErrorDomain code:SPUInstallerError userInfo:@{ NSLocalizedDescriptionKey: @"Error: Failed to start installer", NSUnderlyingErrorKey: firstStageError }]]; }); return; } dispatch_async(dispatch_get_main_queue(), ^{ self->_installer = installer; os_unfair_lock_lock(&self->_newConnectionLock); self->_performedStage1Installation = YES; os_unfair_lock_unlock(&self->_newConnectionLock); uint8_t targetTerminated = (uint8_t)self->_targetTerminated; NSData *sendData = [NSData dataWithBytes:&targetTerminated length:sizeof(targetTerminated)]; [self->_communicator handleMessageWithIdentifier:SPUInstallationFinishedStage1 data:sendData]; if (self->_targetTerminated) { // Stage 2 can still be run before we finish installation // if the updater requests for it before the app is terminated [self finishInstallationAfterHostTermination]; } }); }); } - (void)performStage2Installation SPU_OBJC_DIRECT { _performedStage2Installation = YES; dispatch_async(dispatch_get_main_queue(), ^{ uint8_t targetTerminated = (uint8_t)self->_targetTerminated; NSData *sendData = [NSData dataWithBytes:&targetTerminated length:sizeof(targetTerminated)]; [self->_communicator handleMessageWithIdentifier:SPUInstallationFinishedStage2 data:sendData]; // Don't check if the target is already terminated, leave that to the progress agent // We could be slightly off if there were multiple instances running [self->_agentConnection.agent sendTerminationSignal]; }); } - (void)finishInstallationAfterHostTermination SPU_OBJC_DIRECT { assert(self->_targetTerminated); // Show our installer progress UI tool if only after a certain amount of time passes __block BOOL shouldShowUIProgress = YES; if (self->_shouldShowUI) { // Ask the updater if it is still alive // If they are, we will receive a pong response back // Reset if we received a pong just to be on the safe side self->_receivedUpdaterPong = NO; [self->_communicator handleMessageWithIdentifier:SPUUpdaterAlivePing data:[NSData data]]; dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(SUDisplayProgressTimeDelay * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ // Make sure we're still eligible for showing the installer progress // Also if the updater process is still alive, showing the progress should not be our duty // if the communicator object is nil, the updater definitely isn't alive. However, if it is not nil, // this does not necessarily mean the updater is alive, so we should also check if we got a recent response back from the updater if (shouldShowUIProgress && (!self->_receivedUpdaterPong || self->_communicator == nil)) { [self->_agentConnection.agent showProgress]; } }); } dispatch_async(self->_installerQueue, ^{ if (!self->_performedStage2Installation) { [self performStage2Installation]; } if (!self->_performedStage2Installation) { // We failed and we're going to exit shortly return; } NSError *thirdStageError = nil; if (![self->_installer performFinalInstallationProgressBlock:nil error:&thirdStageError]) { [self->_installer performCleanup]; self->_installer = nil; dispatch_async(dispatch_get_main_queue(), ^{ [self cleanupAndExitWithStatus:EXIT_FAILURE error:[NSError errorWithDomain:SUSparkleErrorDomain code:SPUInstallerError userInfo:@{ NSLocalizedDescriptionKey: @"Failed to finalize installation", NSUnderlyingErrorKey: thirdStageError }]]; }); return; } self->_performedStage3Installation = YES; dispatch_async(dispatch_get_main_queue(), ^{ // Make sure to stop our displayed progress before we move onto cleanup & relaunch // This will also stop the agent from broadcasting the status info service, which we want to do before // we relaunch the app because the relaunched app could check the service upon launch.. [self->_agentConnection.agent stopProgress]; shouldShowUIProgress = NO; [self->_communicator handleMessageWithIdentifier:SPUInstallationFinishedStage3 data:[NSData data]]; if (self->_shouldRelaunch) { // This will also signal to the agent that it will terminate soon [self->_agentConnection.agent relaunchApplication]; } [self->_installer performCleanup]; [self cleanupAndExitWithStatus:EXIT_SUCCESS error:nil]; }); }); } - (void)cleanupAndExitWithStatus:(int)status error:(NSError * _Nullable)error __attribute__((noreturn)) { if (error != nil) { SULogError(error); NSData *errorData = SPUArchiveRootObjectSecurely((NSError * _Nonnull)error); if (errorData != nil) { [_communicator handleMessageWithIdentifier:SPUInstallerError data:errorData]; } } // It's nice to tell the other end we're invalidating os_unfair_lock_lock(&_newConnectionLock); NSXPCConnection *activeConnection = _activeConnection; _activeConnection = nil; os_unfair_lock_unlock(&_newConnectionLock); [activeConnection invalidate]; [_xpcListener invalidate]; _xpcListener = nil; [_agentConnection invalidate]; _agentConnection = nil; [self clearUpdateDirectory]; exit(status); } @end ================================================ FILE: Autoupdate/SPUDeltaArchive.h ================================================ // // SPUDeltaArchive.h // Sparkle // // Created by Mayur Pawashe on 12/29/21. // Copyright © 2021 Sparkle Project. All rights reserved. // #import @protocol SPUDeltaArchiveProtocol; @class SPUDeltaArchiveHeader; NS_ASSUME_NONNULL_BEGIN // Opens patch file for reading and decodes the archive header id SPUDeltaArchiveReadPatchAndHeader(NSString *patchFile, SPUDeltaArchiveHeader * _Nullable __autoreleasing * _Nullable outHeader); NS_ASSUME_NONNULL_END ================================================ FILE: Autoupdate/SPUDeltaArchive.m ================================================ // // SPUDeltaArchive.m // Sparkle // // Created by Mayur Pawashe on 12/29/21. // Copyright © 2021 Sparkle Project. All rights reserved. // #import "SPUDeltaArchive.h" #import "SPUDeltaArchiveProtocol.h" #import "SPUSparkleDeltaArchive.h" #import "SPUXarDeltaArchive.h" #import "SUBinaryDeltaCommon.h" #include "AppKitPrevention.h" SPUDeltaCompressionMode SPUDeltaCompressionModeDefault = (SPUDeltaCompressionMode)UINT8_MAX; id SPUDeltaArchiveReadPatchAndHeader(NSString *patchFile, SPUDeltaArchiveHeader * _Nullable __autoreleasing * _Nullable outHeader) { id sparkleArchive = [[SPUSparkleDeltaArchive alloc] initWithPatchFileForReading:patchFile]; SPUDeltaArchiveHeader *header = [sparkleArchive readHeader]; if (header == nil) { #if SPARKLE_BUILD_LEGACY_DELTA_SUPPORT NSError *archiveError = sparkleArchive.error; if (archiveError != nil && [archiveError.domain isEqualToString:SPARKLE_DELTA_ARCHIVE_ERROR_DOMAIN] && archiveError.code == SPARKLE_DELTA_ARCHIVE_ERROR_CODE_BAD_MAGIC) { // Retry with XAR archive if the magic value is unexpected [sparkleArchive close]; id xarArchive = [[SPUXarDeltaArchive alloc] initWithPatchFileForReading:patchFile]; SPUDeltaArchiveHeader *xarHeader = [xarArchive readHeader]; if (outHeader != NULL) { *outHeader = xarHeader; } return xarArchive; } else #endif { if (outHeader != NULL) { *outHeader = nil; } return sparkleArchive; } } else { if (outHeader != NULL) { *outHeader = header; } return sparkleArchive; } } @implementation SPUDeltaArchiveItem @synthesize relativeFilePath = _relativeFilePath; @synthesize itemFilePath = _itemFilePath; @synthesize clonedRelativePath = _clonedRelativePath; @synthesize sourcePath = _sourcePath; @synthesize commands = _commands; #if SPARKLE_BUILD_LEGACY_DELTA_SUPPORT @synthesize xarContext = _xarContext; #endif @synthesize mode = _mode; @synthesize codedDataLength = _codedDataLength; - (instancetype)initWithRelativeFilePath:(NSString *)relativeFilePath commands:(SPUDeltaItemCommands)commands mode:(uint16_t)mode { self = [super init]; if (self != nil) { _relativeFilePath = [relativeFilePath copy]; _commands = commands; _mode = mode; } return self; } @end @implementation SPUDeltaArchiveHeader { unsigned char _beforeTreeHash[BINARY_DELTA_HASH_LENGTH]; unsigned char _afterTreeHash[BINARY_DELTA_HASH_LENGTH]; } @synthesize compression = _compression; @synthesize compressionLevel = _compressionLevel; @synthesize fileSystemCompression = _fileSystemCompression; @synthesize majorVersion = _majorVersion; @synthesize minorVersion = _minorVersion; @synthesize bundleCreationDate = _bundleCreationDate; - (instancetype)initWithCompression:(SPUDeltaCompressionMode)compression compressionLevel:(uint8_t)compressionLevel fileSystemCompression:(bool)fileSystemCompression majorVersion:(uint16_t)majorVersion minorVersion:(uint16_t)minorVersion beforeTreeHash:(const unsigned char *)beforeTreeHash afterTreeHash:(const unsigned char *)afterTreeHash bundleCreationDate:(nullable NSDate *)bundleCreationDate { self = [super init]; if (self != nil) { _compression = compression; _compressionLevel = compressionLevel; _fileSystemCompression = fileSystemCompression; _majorVersion = majorVersion; _minorVersion = minorVersion; memcpy(_beforeTreeHash, beforeTreeHash, sizeof(_beforeTreeHash)); memcpy(_afterTreeHash, afterTreeHash, sizeof(_afterTreeHash)); _bundleCreationDate = bundleCreationDate; } return self; } - (unsigned char *)beforeTreeHash { return _beforeTreeHash; } - (unsigned char *)afterTreeHash { return _afterTreeHash; } @end ================================================ FILE: Autoupdate/SPUDeltaArchiveProtocol.h ================================================ // // SPUDeltaArchiveProtocol.h // Autoupdate // // Created by Mayur Pawashe on 12/28/21. // Copyright © 2021 Sparkle Project. All rights reserved. // #import #import "SPUDeltaCompressionMode.h" NS_ASSUME_NONNULL_BEGIN // Attributes for an item we extract/write to the archive // Note: BinaryDiff cannot coexist together with Delete typedef NS_ENUM(uint8_t, SPUDeltaItemCommands) { SPUDeltaItemCommandEndMarker = 0, SPUDeltaItemCommandDelete = (1u << 0), SPUDeltaItemCommandExtract = (1u << 1), SPUDeltaItemCommandModifyPermissions = (1u << 2), SPUDeltaItemCommandBinaryDiff = (1u << 3), SPUDeltaItemCommandClone = (1u << 4), }; // Represents header for our archive SPU_OBJC_DIRECT_MEMBERS @interface SPUDeltaArchiveHeader : NSObject - (instancetype)initWithCompression:(SPUDeltaCompressionMode)compression compressionLevel:(uint8_t)compressionLevel fileSystemCompression:(bool)fileSystemCompression majorVersion:(uint16_t)majorVersion minorVersion:(uint16_t)minorVersion beforeTreeHash:(const unsigned char *)beforeTreeHash afterTreeHash:(const unsigned char *)afterTreeHash bundleCreationDate:(nullable NSDate *)bundleCreationDate; @property (nonatomic, readonly) SPUDeltaCompressionMode compression; @property (nonatomic, readonly) uint8_t compressionLevel; @property (nonatomic, readonly) bool fileSystemCompression; @property (nonatomic, readonly) uint16_t majorVersion; @property (nonatomic, readonly) uint16_t minorVersion; @property (nonatomic, readonly) unsigned char *beforeTreeHash; @property (nonatomic, readonly) unsigned char *afterTreeHash; @property (nonatomic, readonly, nullable) NSDate *bundleCreationDate; @end // Represents an item we read or write to in our delta archive SPU_OBJC_DIRECT_MEMBERS @interface SPUDeltaArchiveItem : NSObject - (instancetype)initWithRelativeFilePath:(NSString *)relativeFilePath commands:(SPUDeltaItemCommands)commands mode:(uint16_t)mode; // The relative file path of the item, eg /Contents/MacOS/Foo @property (nonatomic, readonly) NSString *relativeFilePath; // For extraction, the path where the item will be extracted. For creation, the path to the item to add to the archive. @property (nonatomic, nullable) NSString *itemFilePath; // The relative file path of the originating item for clones. This may be null. // For example, if /Contents/Resources/hello/foo.txt moves to /Contents/Resources/hello2/foo.txt, the former is the relative path @property (nonatomic, nullable) NSString *clonedRelativePath; // The source path of the item to extract file metadata from such as file size. @property (nonatomic, nullable) NSString *sourcePath; // The commands that describe the actions to take for this item. @property (nonatomic, readonly) SPUDeltaItemCommands commands; // Provided change in permissions for item or tracking file mode for the item @property (nonatomic) uint16_t mode; // Private properties #if SPARKLE_BUILD_LEGACY_DELTA_SUPPORT // xar_file context for Xar delta archiver @property (nonatomic, nullable) const void *xarContext; #endif // Tracking length of item's data in data section, when encoding items and when extracting items @property (nonatomic) uint64_t codedDataLength; @end // A protocol for reading and writing binary delta patches // Operations must be done in order. The header must first be read or written before any other operations. // For reading, file items cannot be extracted out of order. @protocol SPUDeltaArchiveProtocol @property (nonatomic, readonly, class) BOOL maySupportSafeExtraction; // If non-nil, there was an error with reading or writing data from the archive @property (nonatomic, readonly, nullable) NSError *error; // Closes file for reading/writing, called in -dealloc if it's not called manually - (void)close; // For reading // Retrieves metadata for the archive including major/minor version and expected bundle hashes - (nullable SPUDeltaArchiveHeader *)readHeader; // Enumerate through items in the patch file and read the path, attributes, permissions (if permission attribute is available), and way to stop enumeration - (void)enumerateItems:(void (^)(SPUDeltaArchiveItem *item, BOOL *stop))itemHandler; // Extract a file item from the patch file to a destination file // The item's physical file path must be set as a destination - (BOOL)extractItem:(SPUDeltaArchiveItem *)item; // ------------ // For writing // Set metadata for archive including major/minor version and expected bundle hashes - (void)writeHeader:(SPUDeltaArchiveHeader *)header; // Add item to patch file // Physical file path must be provided if there is an extract or binary delta attribute // Permissions are used only if there is a modify permissions attribute - (void)addItem:(SPUDeltaArchiveItem *)item; // Finishes encoding items after having added all of them - (void)finishEncodingItems; @end NS_ASSUME_NONNULL_END ================================================ FILE: Autoupdate/SPUDeltaCompressionMode.h ================================================ // // SPUDeltaCompressionMode.h // Sparkle // // Created by Mayur Pawashe on 1/3/22. // Copyright © 2022 Sparkle Project. All rights reserved. // #import // Compression mode to use during patch creation typedef NS_ENUM(uint8_t, SPUDeltaCompressionMode) { SPUDeltaCompressionModeNone = 0, SPUDeltaCompressionModeBzip2, SPUDeltaCompressionModeLZMA, SPUDeltaCompressionModeLZFSE, SPUDeltaCompressionModeLZ4, SPUDeltaCompressionModeZLIB }; // For Swift access extern SPUDeltaCompressionMode SPUDeltaCompressionModeDefault; ================================================ FILE: Autoupdate/SPUInstallationInfo.h ================================================ // // SPUInstallationInfo.h // Sparkle // // Created by Mayur Pawashe on 4/10/16. // Copyright © 2016 Sparkle Project. All rights reserved. // #import NS_ASSUME_NONNULL_BEGIN @class SUAppcastItem; SPU_OBJC_DIRECT_MEMBERS @interface SPUInstallationInfo : NSObject - (instancetype)initWithAppcastItem:(SUAppcastItem *)appcastItem; @property (nonatomic, readonly) SUAppcastItem *appcastItem; @property (nonatomic) BOOL systemDomain; @end NS_ASSUME_NONNULL_END ================================================ FILE: Autoupdate/SPUInstallationInfo.m ================================================ // // SPUInstallationInfo.m // Sparkle // // Created by Mayur Pawashe on 4/10/16. // Copyright © 2016 Sparkle Project. All rights reserved. // #import "SPUInstallationInfo.h" #import "SUAppcastItem.h" #include "AppKitPrevention.h" static NSString *SUAppcastItemKey = @"SUAppcastItem"; static NSString *SUCanSilentlyInstallKey = @"SUCanSilentlyInstall"; static NSString *SUSystemDomainKey = @"SUSystemDomain"; @implementation SPUInstallationInfo @synthesize appcastItem = _appcastItem; @synthesize systemDomain = _systemDomain; - (instancetype)initWithAppcastItem:(SUAppcastItem *)appcastItem systemDomain:(BOOL)systemDomain { self = [super init]; if (self != nil) { _appcastItem = appcastItem; _systemDomain = systemDomain; } return self; } - (instancetype)initWithAppcastItem:(SUAppcastItem *)appcastItem { return [self initWithAppcastItem:appcastItem systemDomain:NO]; } - (nullable instancetype)initWithCoder:(NSCoder *)decoder { SUAppcastItem *appcastItem = [decoder decodeObjectOfClass:[SUAppcastItem class] forKey:SUAppcastItemKey]; if (appcastItem == nil) { return nil; } BOOL systemDomain = [decoder decodeBoolForKey:SUSystemDomainKey]; return [self initWithAppcastItem:appcastItem systemDomain:systemDomain]; } - (void)encodeWithCoder:(NSCoder *)coder { [coder encodeObject:_appcastItem forKey:SUAppcastItemKey]; [coder encodeBool:_systemDomain forKey:SUSystemDomainKey]; // Installation types can always be silently installed for newer versions of Sparkle // Still encode this key to maintain backwards compatibility with older Sparkle clients [coder encodeBool:YES forKey:SUCanSilentlyInstallKey]; } + (BOOL)supportsSecureCoding { return YES; } @end ================================================ FILE: Autoupdate/SPUInstallationInputData.h ================================================ // // SPUInstallationInputData.h // Sparkle // // Created by Mayur Pawashe on 3/24/16. // Copyright © 2016 Sparkle Project. All rights reserved. // #import @class SUSignatures; NS_ASSUME_NONNULL_BEGIN SPU_OBJC_DIRECT_MEMBERS @interface SPUInstallationInputData : NSObject /* * relaunchPath - path to application bundle to relaunch and listen for termination * hostBundlePath - path to host bundle to update & replace * updateDirectoryPath - path to update directory (i.e, temporary directory containing the new update archive) * downloadName - name of update archive in update directory * signatures - signatures for the update that came from the appcast item * decryptionPassword - optional decryption password for dmg archives * expectedVersion - optional expected version of the new update * expectedContentLength - optional expected content length of the new download archive */ - (instancetype)initWithRelaunchPath:(NSString *)relaunchPath hostBundlePath:(NSString *)hostBundlePath updateURLBookmarkData:(NSData *)updateURLBookmarkData installationType:(NSString *)installationType signatures:(SUSignatures * _Nullable)signatures decryptionPassword:(nullable NSString *)decryptionPassword expectedVersion:(NSString *)expectedVersion expectedContentLength:(uint64_t)expectedContentLength; @property (nonatomic, copy, readonly) NSString *relaunchPath; @property (nonatomic, copy, readonly) NSString *hostBundlePath; @property (nonatomic, copy, readonly) NSData *updateURLBookmarkData; @property (nonatomic, copy, readonly) NSString *installationType; @property (nonatomic, readonly, nullable) SUSignatures *signatures; // nullable because although not using signatures is deprecated, it's still supported @property (nonatomic, copy, readonly, nullable) NSString *decryptionPassword; @property (nonatomic, copy, readonly, nullable) NSString *expectedVersion; @property (nonatomic, readonly) uint64_t expectedContentLength; @end NS_ASSUME_NONNULL_END ================================================ FILE: Autoupdate/SPUInstallationInputData.m ================================================ // // SPUInstallationInputData.m // Sparkle // // Created by Mayur Pawashe on 3/24/16. // Copyright © 2016 Sparkle Project. All rights reserved. // #import "SPUInstallationInputData.h" #import "SPUInstallationType.h" #import "SUSignatures.h" #include "AppKitPrevention.h" static NSString *SURelaunchPathKey = @"SURelaunchPath"; static NSString *SUHostBundlePathKey = @"SUHostBundlePath"; static NSString *SUUpdateURLBookmarkDataKey = @"SUUpdateURLBookmarkData"; static NSString *SUSignaturesKey = @"SUSignatures"; static NSString *SUDecryptionPasswordKey = @"SUDecryptionPassword"; static NSString *SUInstallationTypeKey = @"SUInstallationType"; static NSString *SUExpectedVersionKey = @"SUExpectedVersion"; static NSString *SUExpectedContentLength = @"SUExpectedContentLength"; @implementation SPUInstallationInputData @synthesize relaunchPath = _relaunchPath; @synthesize hostBundlePath = _hostBundlePath; @synthesize updateURLBookmarkData = _updateURLBookmarkData; @synthesize signatures = _signatures; @synthesize decryptionPassword = _decryptionPassword; @synthesize installationType = _installationType; @synthesize expectedVersion = _expectedVersion; @synthesize expectedContentLength = _expectedContentLength; - (instancetype)initWithRelaunchPath:(NSString *)relaunchPath hostBundlePath:(NSString *)hostBundlePath updateURLBookmarkData:(NSData *)updateURLBookmarkData installationType:(NSString *)installationType signatures:(SUSignatures * _Nullable)signatures decryptionPassword:(nullable NSString *)decryptionPassword expectedVersion:(nonnull NSString *)expectedVersion expectedContentLength:(uint64_t)expectedContentLength { self = [super init]; if (self != nil) { _relaunchPath = [relaunchPath copy]; _hostBundlePath = [hostBundlePath copy]; _updateURLBookmarkData = updateURLBookmarkData; _installationType = [installationType copy]; assert(SPUValidInstallationType(_installationType)); _signatures = signatures; _decryptionPassword = [decryptionPassword copy]; _expectedVersion = [expectedVersion copy]; _expectedContentLength = expectedContentLength; } return self; } - (nullable instancetype)initWithCoder:(NSCoder *)decoder { NSString *relaunchPath = [decoder decodeObjectOfClass:[NSString class] forKey:SURelaunchPathKey]; if (relaunchPath == nil) { return nil; } NSString *hostBundlePath = [decoder decodeObjectOfClass:[NSString class] forKey:SUHostBundlePathKey]; if (hostBundlePath == nil) { return nil; } NSData *updateURLBookmarkData = [decoder decodeObjectOfClass:[NSData class] forKey:SUUpdateURLBookmarkDataKey]; if (updateURLBookmarkData == nil) { return nil; } NSString *installationType = [decoder decodeObjectOfClass:[NSString class] forKey:SUInstallationTypeKey]; if (!SPUValidInstallationType(installationType)) { return nil; } SUSignatures *signatures = [decoder decodeObjectOfClass:[SUSignatures class] forKey:SUSignaturesKey]; if (signatures == nil) { return nil; } NSString *decryptionPassword = [decoder decodeObjectOfClass:[NSString class] forKey:SUDecryptionPasswordKey]; NSString *expectedVersion = [decoder decodeObjectOfClass:[NSString class] forKey:SUExpectedVersionKey]; uint64_t expectedContentLength = (uint64_t)[decoder decodeInt64ForKey:SUExpectedContentLength]; return [self initWithRelaunchPath:relaunchPath hostBundlePath:hostBundlePath updateURLBookmarkData:updateURLBookmarkData installationType:installationType signatures:signatures decryptionPassword:decryptionPassword expectedVersion:expectedVersion expectedContentLength:expectedContentLength]; } - (void)encodeWithCoder:(NSCoder *)coder { [coder encodeObject:_relaunchPath forKey:SURelaunchPathKey]; [coder encodeObject:_hostBundlePath forKey:SUHostBundlePathKey]; [coder encodeObject:_updateURLBookmarkData forKey:SUUpdateURLBookmarkDataKey]; [coder encodeObject:_installationType forKey:SUInstallationTypeKey]; [coder encodeObject:_signatures forKey:SUSignaturesKey]; if (_decryptionPassword != nil) { [coder encodeObject:_decryptionPassword forKey:SUDecryptionPasswordKey]; } if (_expectedVersion != nil) { [coder encodeObject:_expectedVersion forKey:SUExpectedVersionKey]; } [coder encodeInt64:(int64_t)_expectedContentLength forKey:SUExpectedContentLength]; } + (BOOL)supportsSecureCoding { return YES; } @end ================================================ FILE: Autoupdate/SPUMessageTypes.h ================================================ // // SPUMessageTypes.h // Sparkle // // Created by Mayur Pawashe on 3/11/16. // Copyright © 2016 Sparkle Project. All rights reserved. // #import NS_ASSUME_NONNULL_BEGIN // Order matters; higher stages have higher values. typedef NS_ENUM(int32_t, SPUInstallerMessageType) { SPUInstallerNotStarted = 0, SPUExtractionStarted = 1, SPUExtractedArchiveWithProgress = 2, SPUArchiveExtractionFailed = 3, SPUValidationStarted = 4, SPUInstallationStartedStage1 = 5, SPUInstallationFinishedStage1 = 6, SPUInstallationFinishedStage2 = 7, SPUInstallationFinishedStage3 = 8, SPUUpdaterAlivePing = 9, SPUInstallerError = 10 }; typedef NS_ENUM(int32_t, SPUUpdaterMessageType) { SPUInstallationData = 0, SPUSentUpdateAppcastItemData = 1, SPUResumeInstallationToStage2 = 2, SPUUpdaterAlivePong = 3, SPUCancelInstallation = 4 }; BOOL SPUInstallerMessageTypeIsLegal(SPUInstallerMessageType oldMessageType, SPUInstallerMessageType newMessageType); // Used by framework to communicate to installer (Autoupdate) NSString *SPUInstallerServiceNameForBundleIdentifier(NSString *bundleIdentifier); // Used by framework to communicate to progress agent tool (Updater) NSString *SPUStatusInfoServiceNameForBundleIdentifier(NSString *bundleIdentifier); // Used by progress agent tool to communicate to installer (Autoupdate) NSString *SPUProgressAgentServiceNameForBundleIdentifier(NSString *bundleIdentifier); NS_ASSUME_NONNULL_END ================================================ FILE: Autoupdate/SPUMessageTypes.m ================================================ // // SPUMessageTypes.m // Sparkle // // Created by Mayur Pawashe on 3/11/16. // Copyright © 2016 Sparkle Project. All rights reserved. // #import "SPUMessageTypes.h" #include "AppKitPrevention.h" // Tags added to the bundle identifier which is used as Mach service names // These should be very short because of length restrictions #define SPARKLE_INSTALLER_TAG @"-spki" #define SPARKLE_STATUS_TAG @"-spks" #define SPARKLE_PROGRESS_TAG @"-spkp" // macOS 10.8 couldn't handle service names that are >= 64 characters, // but 10.9 raised this to >= 128 characters #define MAX_SERVICE_NAME_LENGTH 127u BOOL SPUInstallerMessageTypeIsLegal(SPUInstallerMessageType oldMessageType, SPUInstallerMessageType newMessageType) { BOOL legal; switch (newMessageType) { case SPUInstallerNotStarted: legal = (oldMessageType == SPUInstallerNotStarted); break; case SPUExtractionStarted: legal = (oldMessageType == SPUInstallerNotStarted); break; case SPUExtractedArchiveWithProgress: case SPUArchiveExtractionFailed: legal = (oldMessageType == SPUExtractionStarted || oldMessageType == SPUExtractedArchiveWithProgress); break; case SPUValidationStarted: legal = (oldMessageType == SPUExtractionStarted || oldMessageType == SPUExtractedArchiveWithProgress); break; case SPUInstallationStartedStage1: legal = (oldMessageType == SPUValidationStarted); break; case SPUInstallationFinishedStage1: legal = (oldMessageType == SPUInstallationStartedStage1); break; case SPUInstallationFinishedStage2: legal = (oldMessageType == SPUInstallationFinishedStage1); break; case SPUInstallationFinishedStage3: legal = (oldMessageType == SPUInstallationFinishedStage2); break; case SPUInstallerError: case SPUUpdaterAlivePing: // Having this state being dependent on other installation states would make the complicate our logic // So just always allow these type of messages legal = YES; break; } return legal; } static NSString *SPUServiceNameWithTag(NSString *tagName, NSString *bundleIdentifier) { NSString *serviceName = [bundleIdentifier stringByAppendingString:tagName]; NSUInteger length = MIN(serviceName.length, MAX_SERVICE_NAME_LENGTH); // If the service name is too long, cut off the beginning rather than cutting off the end // This should lead to a more unique name return [serviceName substringFromIndex:serviceName.length - length]; } NSString *SPUInstallerServiceNameForBundleIdentifier(NSString *bundleIdentifier) { return SPUServiceNameWithTag(SPARKLE_INSTALLER_TAG, bundleIdentifier); } NSString *SPUStatusInfoServiceNameForBundleIdentifier(NSString *bundleIdentifier) { return SPUServiceNameWithTag(SPARKLE_STATUS_TAG, bundleIdentifier); } NSString *SPUProgressAgentServiceNameForBundleIdentifier(NSString *bundleIdentifier) { return SPUServiceNameWithTag(SPARKLE_PROGRESS_TAG, bundleIdentifier); } ================================================ FILE: Autoupdate/SPUSparkleDeltaArchive.h ================================================ // // SPUSparkleDeltaArchive.h // Sparkle // // Created by Mayur Pawashe on 12/30/21. // Copyright © 2021 Sparkle Project. All rights reserved. // #import #import "SPUDeltaArchiveProtocol.h" #import "SPUDeltaCompressionMode.h" NS_ASSUME_NONNULL_BEGIN #define SPARKLE_DELTA_ARCHIVE_ERROR_DOMAIN @"Sparkle Delta Archive" #define SPARKLE_DELTA_ARCHIVE_ERROR_CODE_BAD_MAGIC 1 #define SPARKLE_DELTA_ARCHIVE_ERROR_CODE_BAD_COMPRESSION_VALUE 2 #define SPARKLE_DELTA_ARCHIVE_ERROR_CODE_BAD_CHUNK_SIZE 3 #define SPARKLE_DELTA_ARCHIVE_ERROR_CODE_BAD_CLONE_LOOKUP 4 #define SPARKLE_DELTA_ARCHIVE_ERROR_CODE_TOO_MANY_FILES 5 #define SPARKLE_DELTA_ARCHIVE_ERROR_CODE_LINK_TOO_LONG 6 /* Modern container format for binary delta archives. Delta archive format has four sections which are the header, the relative file path table, the commands, and the data blobs. The relative file path table records all the file paths the archive needs to know about. The commands are the operations to be recorded (eg: extract, clone, binary diff, delete). The commands may additionally have more metadata such as file permission modes, relative path indexes in the case of clones, or file sizes for the data blobs. The data blobs contain all file data from extract and binary diff outputs. The implementation design of this archive is such that we do not seek backwards or skip ahead to fetch data. We go through the archive when writing or reading from it in a single pass. -- UNCOMPRESSED -- [ HEADER (part 1) ] magic (length: 4) compression (length: 1) metadata (See format below. length: 1) -- METADATA -- compressionLevel (bits [0, 4]) (bits [5, 6] reserved) fileSystemCompression (bit 7) -- COMPRESSED -- [ HEADER (part 2)] majorVersion (length: 2) minorVersion (length: 2) beforeTreeHash (length: 40) afterTreeHash (length: 40) [ RELATIVE_FILE_PATH_TABLE ] sizeOfRelativeFilePathTable (length: 8 bytes) List of null terminated path strings joined together (N paths) [ COMMANDS ] [ Command ] Set of command types for entry (length: 1 byte) Additional metadata for command (see below section) (M commands where M <= N paths) (Indexes for commands refer to indexes to relative file path table, excluding extraneous trailing entries in relative path table used for clones) (Last command must be a ZERO COMMAND (a null terminating command)) [COMMAND METADATA] [COMMAND: DELETE] N/A: no additional data [COMMAND: MODIFY_PERMISSIONS] fileMode (length: 2) [COMMAND: CLONE_FILE] sourceRelativePathIndex (length: 4) [COMMAND: EXTRACT_FILE] dataLength (length: 8 for regular files, 2 for symbolic links, 0 for directories) [COMMAND: BINARY_DIFF_FILE] dataLength (length: 8) Note: These command types may be OR'd or combined. In those cases, different commands that have the same attribute (eg dataLength) are not repeated. This list of metadata is also in order in what will be encoded/decoded first. EXTRACT_FILE and BINARY_DIFF_FILE cannot be combined. CLONE_FILE and EXTRACT_FILE cannot be combined. MODIFY_PERMISSIONS can be combined with either CLONE_FILE, EXTRACT_FILE, BINARY_DIFF_FILE, or just by itself. CLONE_FILE can be combined with BINARY_DIFF_FILE or just by itself. DELETE can be combined with EXTRACT_FILE, or just by itself. It is only combined with EXTRACT_FILE when the file type (regular, directory, or symlink) changes. [ DATA_BLOBS ] All raw binary data joined together (P number of blobs where P <= M commands) (Indexes for data blobs correspond to indexes for a filtered list of applicable commands (EXTRACT_FILE, BINARY_DIFF_FILE) that have data content) */ SPU_OBJC_DIRECT_MEMBERS @interface SPUSparkleDeltaArchive : NSObject - (instancetype)initWithPatchFileForWriting:(NSString *)patchFile; - (instancetype)initWithPatchFileForReading:(NSString *)patchFile; @end NS_ASSUME_NONNULL_END ================================================ FILE: Autoupdate/SPUSparkleDeltaArchive.m ================================================ // // SPUSparkleDeltaArchive.m // Sparkle // // Created by Mayur Pawashe on 12/30/21. // Copyright © 2021 Sparkle Project. All rights reserved. // #import "SPUSparkleDeltaArchive.h" #import #import #import "SUBinaryDeltaCommon.h" #import #if SPARKLE_BUILD_BZIP2_DELTA_SUPPORT #import #endif #include "AppKitPrevention.h" #define SPARKLE_DELTA_FORMAT_MAGIC "spk!" #define PARTIAL_IO_CHUNK_SIZE 16384 // this must be >= PATH_MAX #define COMPRESSION_BUFFER_SIZE 65536 #define SPARKLE_BZIP2_ERROR_DOMAIN @"Sparkle BZIP2" #define SPARKLE_COMPRESSION_ERROR_DOMAIN @"Sparkle Compression" typedef struct { uint8_t compressionLevel : 4; uint8_t reserved : 3; bool fileSystemCompression : 1; } SparkleDeltaArchiveMetadata; @implementation SPUSparkleDeltaArchive { FILE *_file; #if SPARKLE_BUILD_BZIP2_DELTA_SUPPORT BZFILE *_bzipFile; #endif NSString *_patchFile; NSError *_error; void *_partialChunkBuffer; void *_compressionBuffer; NSMutableArray *_writableItems; compression_stream _compressionStream; SPUDeltaCompressionMode _compression; BOOL _initializedCompressionStream; BOOL _writeMode; } @synthesize error = _error; + (BOOL)maySupportSafeExtraction { return YES; } - (instancetype)initWithPatchFileForWriting:(NSString *)patchFile { self = [super init]; if (self != nil) { _patchFile = [patchFile copy]; _writableItems = [NSMutableArray array]; _writeMode = YES; } return self; } - (instancetype)initWithPatchFileForReading:(NSString *)patchFile { self = [super init]; if (self != nil) { _patchFile = [patchFile copy]; } return self; } - (void)dealloc { [self close]; } - (void)close { #if SPARKLE_BUILD_BZIP2_DELTA_SUPPORT if (_bzipFile != NULL) { if (!_writeMode) { int bzerror = 0; BZ2_bzReadClose(&bzerror, _bzipFile); } _bzipFile = NULL; } else #endif if (_initializedCompressionStream) { compression_stream_destroy(&_compressionStream); _initializedCompressionStream = NO; } if (_file != NULL) { fclose(_file); _file = NULL; } free(_partialChunkBuffer); _partialChunkBuffer = NULL; free(_compressionBuffer); _compressionBuffer = NULL; } - (BOOL)createBuffers SPU_OBJC_DIRECT { _partialChunkBuffer = calloc(1, PARTIAL_IO_CHUNK_SIZE); if (_partialChunkBuffer == NULL) { _error = [NSError errorWithDomain:NSPOSIXErrorDomain code:errno userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Failed to calloc() %d bytes for partial chunk buffer.", PARTIAL_IO_CHUNK_SIZE] }]; return NO; } if (_initializedCompressionStream) { _compressionBuffer = calloc(1, COMPRESSION_BUFFER_SIZE); if (_compressionBuffer == NULL) { _error = [NSError errorWithDomain:NSPOSIXErrorDomain code:errno userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Failed to calloc() %d bytes for compression buffer.", COMPRESSION_BUFFER_SIZE] }]; return NO; } } return YES; } - (BOOL)_readBuffer:(void *)buffer length:(int32_t)length SPU_OBJC_DIRECT { if (_error != nil) { return NO; } switch (_compression) { case SPUDeltaCompressionModeNone: { if (fread(buffer, (size_t)length, 1, _file) < 1) { _error = [NSError errorWithDomain:NSPOSIXErrorDomain code:errno userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Failed to read %d uncompressed bytes from archive.", length] }]; return NO; } else { return YES; } } case SPUDeltaCompressionModeBzip2: { #if SPARKLE_BUILD_BZIP2_DELTA_SUPPORT int bzerror = 0; int bytesRead = BZ2_bzRead(&bzerror, _bzipFile, buffer, length); switch (bzerror) { case BZ_OK: case BZ_STREAM_END: if (bytesRead < length) { _error = [NSError errorWithDomain:SPARKLE_BZIP2_ERROR_DOMAIN code:0 userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Only %d out of %d expected bytes were read from the bz2 archive.", bytesRead, length] }]; return NO; } else { return YES; } case BZ_IO_ERROR: _error = [NSError errorWithDomain:NSPOSIXErrorDomain code:errno userInfo:@{ NSLocalizedDescriptionKey: @"Encountered unexpected IO error when reading compressed bytes from bz2 archive." }]; return NO; default: _error = [NSError errorWithDomain:SPARKLE_BZIP2_ERROR_DOMAIN code:bzerror userInfo:@{ NSLocalizedDescriptionKey: @"Encountered unexpected error when reading compressed bytes from bz2 archive." }]; return NO; } #else return NO; #endif } case SPUDeltaCompressionModeLZMA: case SPUDeltaCompressionModeLZFSE: case SPUDeltaCompressionModeLZ4: case SPUDeltaCompressionModeZLIB: { FILE *file = _file; void *compressionBuffer = _compressionBuffer; _compressionStream.dst_ptr = (uint8_t *)buffer; _compressionStream.dst_size = (size_t)length; while (_compressionStream.dst_size > 0) { // Go through the current incomplete chunk before reading another one if (_compressionStream.src_size == 0) { size_t bytesRead = fread(compressionBuffer, 1, COMPRESSION_BUFFER_SIZE, file); if (bytesRead < COMPRESSION_BUFFER_SIZE) { if (feof(file) == 0) { _error = [NSError errorWithDomain:NSPOSIXErrorDomain code:errno userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Failed to read %d compressed raw bytes from archive.", length] }]; return NO; } } // Reset source buffer _compressionStream.src_ptr = (const uint8_t *)compressionBuffer; _compressionStream.src_size = bytesRead; } compression_status status = compression_stream_process(&_compressionStream, feof(file) != 0 ? COMPRESSION_STREAM_FINALIZE : 0); if (status == COMPRESSION_STATUS_ERROR) { _error = [NSError errorWithDomain:SPARKLE_COMPRESSION_ERROR_DOMAIN code:COMPRESSION_STATUS_ERROR userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Failed to read %d compressed bytes.", length] }]; return NO; } if (status == COMPRESSION_STATUS_END && _compressionStream.dst_size > 0) { // We're expecting more bytes but we can't read any more bytes _error = [NSError errorWithDomain:SPARKLE_DELTA_ARCHIVE_ERROR_DOMAIN code:-1 userInfo:@{ NSLocalizedDescriptionKey: @"Failed to decompress and read bytes because we reached EOF" }]; return NO; } } return YES; } } } static compression_algorithm compressionAlgorithmForMode(SPUDeltaCompressionMode compressionMode) { switch (compressionMode) { case SPUDeltaCompressionModeLZMA: return COMPRESSION_LZMA; case SPUDeltaCompressionModeLZFSE: return COMPRESSION_LZFSE; case SPUDeltaCompressionModeLZ4: return COMPRESSION_LZ4; case SPUDeltaCompressionModeZLIB: return COMPRESSION_ZLIB; case SPUDeltaCompressionModeNone: case SPUDeltaCompressionModeBzip2: assert(false); } assert(false); } - (nullable SPUDeltaArchiveHeader *)readHeader { NSString *patchFile = _patchFile; char patchFilePath[PATH_MAX + 1] = {0}; if (![patchFile getFileSystemRepresentation:patchFilePath maxLength:sizeof(patchFilePath) - 1]) { _error = [NSError errorWithDomain:NSCocoaErrorDomain code:NSFileReadInvalidFileNameError userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Failed to open and represent as a file system representation: %@", patchFile] }]; return nil; } FILE *file = fopen(patchFilePath, "rb"); if (file == NULL) { _error = [NSError errorWithDomain:NSPOSIXErrorDomain code:errno userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Failed to open patch file for writing value due to io error: %@", patchFile] }]; return nil; } _file = file; char magic[5] = {0}; if (fread(magic, sizeof(magic) - 1, 1, file) < 1) { _error = [NSError errorWithDomain:NSPOSIXErrorDomain code:errno userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Failed to read magic value from patch file: %@", patchFile] }]; return nil; } if (strncmp(magic, SPARKLE_DELTA_FORMAT_MAGIC, sizeof(magic) - 1) != 0) { _error = [NSError errorWithDomain:SPARKLE_DELTA_ARCHIVE_ERROR_DOMAIN code:SPARKLE_DELTA_ARCHIVE_ERROR_CODE_BAD_MAGIC userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Patch file does not have '%@' magic value", @SPARKLE_DELTA_FORMAT_MAGIC] }]; return nil; } SPUDeltaCompressionMode compression = SPUDeltaCompressionModeNone; if (fread(&compression, sizeof(compression), 1, file) < 1) { _error = [NSError errorWithDomain:NSPOSIXErrorDomain code:errno userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Failed to read compression value from patch file: %@", patchFile] }]; return nil; } _compression = compression; SparkleDeltaArchiveMetadata metadata = {0}; if (fread(&metadata, sizeof(metadata), 1, file) < 1) { _error = [NSError errorWithDomain:NSPOSIXErrorDomain code:errno userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Failed to read compression level value from patch file: %@", patchFile] }]; return nil; } switch (compression) { case SPUDeltaCompressionModeNone: break; case SPUDeltaCompressionModeBzip2: { #if SPARKLE_BUILD_BZIP2_DELTA_SUPPORT int bzerror = 0; BZFILE *bzipFile = BZ2_bzReadOpen(&bzerror, file, 0, 0, NULL, 0); if (bzipFile == NULL) { switch (bzerror) { case BZ_IO_ERROR: _error = [NSError errorWithDomain:NSPOSIXErrorDomain code:errno userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Failed to open patch as bz2 file due to io error: %@", patchFile] }]; break; default: _error = [NSError errorWithDomain:NSPOSIXErrorDomain code:bzerror userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Failed to open patch as bz2 file: %@", patchFile] }]; break; } return nil; } _bzipFile = bzipFile; break; #else _error = [NSError errorWithDomain:SPARKLE_BZIP2_ERROR_DOMAIN code:-1 userInfo:@{ NSLocalizedDescriptionKey: @"Failed to open patch as bz2 file because bzip2 support is disabled" }]; return nil; #endif } case SPUDeltaCompressionModeLZMA: case SPUDeltaCompressionModeLZFSE: case SPUDeltaCompressionModeLZ4: case SPUDeltaCompressionModeZLIB: { if (compression_stream_init(&_compressionStream, COMPRESSION_STREAM_DECODE, compressionAlgorithmForMode(compression)) != COMPRESSION_STATUS_OK) { _error = [NSError errorWithDomain:SPARKLE_COMPRESSION_ERROR_DOMAIN code:COMPRESSION_STATUS_ERROR userInfo:@{ NSLocalizedDescriptionKey: @"Failed to open compression stream for reading" }]; return nil; } _initializedCompressionStream = YES; break; } default: _error = [NSError errorWithDomain:SPARKLE_DELTA_ARCHIVE_ERROR_DOMAIN code:SPARKLE_DELTA_ARCHIVE_ERROR_CODE_BAD_COMPRESSION_VALUE userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Compression value read %d is not recognized.", compression] }]; return nil; } if (![self createBuffers]) { return nil; } uint16_t majorVersion = 0; if (![self _readBuffer:&majorVersion length:sizeof(majorVersion)]) { return nil; } uint16_t minorVersion = 0; if (![self _readBuffer:&minorVersion length:sizeof(minorVersion)]) { return nil; } unsigned char beforeTreeHash[BINARY_DELTA_HASH_LENGTH] = {0}; if (![self _readBuffer:beforeTreeHash length:sizeof(beforeTreeHash)]) { return nil; } unsigned char afterTreeHash[BINARY_DELTA_HASH_LENGTH] = {0}; if (![self _readBuffer:afterTreeHash length:sizeof(afterTreeHash)]) { return nil; } NSDate *bundleCreationDate; if (majorVersion >= SUBinaryDeltaMajorVersion4) { double bundleCreationTimeInterval = 0; if (![self _readBuffer:&bundleCreationTimeInterval length:sizeof(bundleCreationTimeInterval)]) { return nil; } bundleCreationDate = (bundleCreationTimeInterval != 0.0) ? [NSDate dateWithTimeIntervalSinceReferenceDate:bundleCreationTimeInterval] : nil; } else { bundleCreationDate = nil; } return [[SPUDeltaArchiveHeader alloc] initWithCompression:compression compressionLevel:metadata.compressionLevel fileSystemCompression:metadata.fileSystemCompression majorVersion:majorVersion minorVersion:minorVersion beforeTreeHash:beforeTreeHash afterTreeHash:afterTreeHash bundleCreationDate:bundleCreationDate]; } - (NSArray *)_readRelativeFilePaths SPU_OBJC_DIRECT { if (_error != nil) { return nil; } uint64_t filePathSectionSize = 0; if (![self _readBuffer:&filePathSectionSize length:sizeof(filePathSectionSize)]) { return nil; } if (filePathSectionSize == 0) { // Nothing has actually changed if there are no entries return @[]; } char *fileTableData = (char *)calloc(1, filePathSectionSize); if (fileTableData == NULL) { _error = [NSError errorWithDomain:NSPOSIXErrorDomain code:errno userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Failed to calloc() %llu bytes for relative file paths.", filePathSectionSize] }]; return nil; } { // Read all the paths in chunks uint64_t bytesLeftoverToCopy = filePathSectionSize; while (bytesLeftoverToCopy > 0) { uint64_t currentBlockSize = (bytesLeftoverToCopy >= PARTIAL_IO_CHUNK_SIZE) ? PARTIAL_IO_CHUNK_SIZE : bytesLeftoverToCopy; if (![self _readBuffer:fileTableData + (filePathSectionSize - bytesLeftoverToCopy) length:(int32_t)currentBlockSize]) { free(fileTableData); return nil; } bytesLeftoverToCopy -= currentBlockSize; } } // Read all relative file paths separated by null terminators NSFileManager *fileManager = [NSFileManager defaultManager]; NSMutableArray *relativeFilePaths = [[NSMutableArray alloc] init]; uint64_t currentStartIndex = 0; for (uint64_t index = 0; index < filePathSectionSize; index++) { if (fileTableData[index] == '\0') { NSString *relativePath = [fileManager stringWithFileSystemRepresentation:&fileTableData[currentStartIndex] length:index - currentStartIndex]; if (relativePath == nil) { _error = [NSError errorWithDomain:NSCocoaErrorDomain code:NSFileReadInvalidFileNameError userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Relative path cannot be decoded as a file system representation: %@", relativePath] }]; free(fileTableData); return nil; } currentStartIndex = index + 1; [relativeFilePaths addObject:relativePath]; } } free(fileTableData); return relativeFilePaths; } - (void)enumerateItems:(void (^)(SPUDeltaArchiveItem * _Nonnull, BOOL * _Nonnull))itemHandler { // Parse all relative file paths NSArray *relativeFilePaths = [self _readRelativeFilePaths]; if (relativeFilePaths == nil) { return; } if (relativeFilePaths.count == 0) { // No diff changes return; } if (relativeFilePaths.count > UINT32_MAX) { // Very unlikely but we should guard against this // Clones rely on 32-bit indexes _error = [NSError errorWithDomain:SPARKLE_DELTA_ARCHIVE_ERROR_DOMAIN code:SPARKLE_DELTA_ARCHIVE_ERROR_CODE_TOO_MANY_FILES userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"There are too many file entries to apply a patch for (more than %u)", UINT32_MAX] }]; return; } // Parse through all commands NSMutableArray *archiveItems = [[NSMutableArray alloc] init]; { uint64_t currentItemIndex = 0; while (YES) { SPUDeltaItemCommands commands = (SPUDeltaItemCommands)0; if (![self _readBuffer:&commands length:sizeof(commands)]) { break; } // Test if we're done if (commands == SPUDeltaItemCommandEndMarker) { break; } // Check if we need to decode additional data uint16_t decodedMode = 0; uint64_t decodedDataLength = 0; NSString *clonedRelativePath = nil; if ((commands & SPUDeltaItemCommandClone) != 0) { if ((commands & SPUDeltaItemCommandModifyPermissions) != 0) { // Decode file permission changes for clone if (![self _readBuffer:&decodedMode length:sizeof(decodedMode)]) { break; } } // Decode relative file path for original source file uint32_t cloneRelativePathIndex = 0; if (![self _readBuffer:&cloneRelativePathIndex length:sizeof(cloneRelativePathIndex)]) { break; } if ((NSUInteger)cloneRelativePathIndex >= relativeFilePaths.count) { _error = [NSError errorWithDomain:SPARKLE_DELTA_ARCHIVE_ERROR_DOMAIN code:SPARKLE_DELTA_ARCHIVE_ERROR_CODE_BAD_CLONE_LOOKUP userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Index %u is past relative path table bounds of length %lu", cloneRelativePathIndex, (unsigned long)relativeFilePaths.count] }]; break; } clonedRelativePath = relativeFilePaths[cloneRelativePathIndex]; if ((commands & SPUDeltaItemCommandBinaryDiff) != 0) { if (![self _readBuffer:&decodedDataLength length:sizeof(decodedDataLength)]) { break; } } } else if ((commands & SPUDeltaItemCommandBinaryDiff) != 0) { // Decode file permission changes if available if ((commands & SPUDeltaItemCommandModifyPermissions) != 0) { if (![self _readBuffer:&decodedMode length:sizeof(decodedMode)]) { break; } } // Decode data length if (![self _readBuffer:&decodedDataLength length:sizeof(decodedDataLength)]) { break; } } else if ((commands & SPUDeltaItemCommandExtract) != 0) { // Decode permissions/mode if (![self _readBuffer:&decodedMode length:sizeof(decodedMode)]) { break; } // Decode data length // Length doesn't matter for directory names (we already track the name in the relative path) if (S_ISREG(decodedMode)) { if (![self _readBuffer:&decodedDataLength length:sizeof(decodedDataLength)]) { break; } } else if (S_ISLNK(decodedMode)) { uint16_t decodedLinkLength = 0; if (![self _readBuffer:&decodedLinkLength length:sizeof(decodedLinkLength)]) { break; } decodedDataLength = decodedLinkLength; } } else if ((commands & SPUDeltaItemCommandModifyPermissions) != 0) { // Decode file permissions if (![self _readBuffer:&decodedMode length:sizeof(decodedMode)]) { break; } } SPUDeltaArchiveItem *archiveItem = [[SPUDeltaArchiveItem alloc] initWithRelativeFilePath:relativeFilePaths[currentItemIndex] commands:commands mode:decodedMode]; archiveItem.codedDataLength = decodedDataLength; archiveItem.clonedRelativePath = clonedRelativePath; [archiveItems addObject:archiveItem]; currentItemIndex++; } } if (_error != nil) { return; } // Feed items back to caller BOOL exitedEarly = NO; for (SPUDeltaArchiveItem *item in archiveItems) { itemHandler(item, &exitedEarly); if (exitedEarly) { break; } } } - (BOOL)extractItem:(SPUDeltaArchiveItem *)item { NSString *itemFilePath = item.itemFilePath; assert(itemFilePath != nil); SPUDeltaItemCommands commands = item.commands; assert((commands & SPUDeltaItemCommandExtract) != 0 || (commands & SPUDeltaItemCommandBinaryDiff) != 0); uint16_t mode = item.mode; NSFileManager *fileManager = [NSFileManager defaultManager]; if ((commands & SPUDeltaItemCommandBinaryDiff) != 0 || S_ISREG(mode) || S_ISLNK(mode)) { // Handle regular files // Binary diffs are always on regular files only uint64_t decodedLength = item.codedDataLength; if ((commands & SPUDeltaItemCommandBinaryDiff) != 0 || S_ISREG(mode)) { // Regular files [fileManager removeItemAtPath:itemFilePath error:NULL]; char itemFilePathString[PATH_MAX + 1] = {0}; if (![itemFilePath getFileSystemRepresentation:itemFilePathString maxLength:sizeof(itemFilePathString) - 1]) { _error = [NSError errorWithDomain:NSCocoaErrorDomain code:NSFileReadInvalidFileNameError userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Path to extract cannot be decoded and expressed as a file system representation: %@", itemFilePath] }]; return NO; } FILE *outputFile = fopen(itemFilePathString, "wb"); if (outputFile == NULL) { _error = [NSError errorWithDomain:NSPOSIXErrorDomain code:errno userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Failed to fopen() %@", itemFilePath] }]; return NO; } if (decodedLength > 0) { // Write out archive contents to file in chunks uint64_t bytesLeftoverToCopy = decodedLength; while (bytesLeftoverToCopy > 0) { uint64_t currentBlockSize = (bytesLeftoverToCopy >= PARTIAL_IO_CHUNK_SIZE) ? PARTIAL_IO_CHUNK_SIZE : bytesLeftoverToCopy; void *tempBuffer = _partialChunkBuffer; if (![self _readBuffer:tempBuffer length:(int32_t)currentBlockSize]) { break; } if (fwrite(tempBuffer, currentBlockSize, 1, outputFile) < 1) { _error = [NSError errorWithDomain:NSPOSIXErrorDomain code:errno userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Failed to fwrite() %llu bytes during extraction.", currentBlockSize] }]; break; } bytesLeftoverToCopy -= currentBlockSize; } } fclose(outputFile); if (_error != nil) { return NO; } if ((commands & SPUDeltaItemCommandExtract) != 0 && chmod(itemFilePathString, mode) != 0) { _error = [NSError errorWithDomain:NSPOSIXErrorDomain code:errno userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Failed to chmod() mode %d on %@", mode, itemFilePath] }]; return NO; } } else { // Link files if (PARTIAL_IO_CHUNK_SIZE < decodedLength) { // Something is seriously wrong _error = [NSError errorWithDomain:SPARKLE_DELTA_ARCHIVE_ERROR_DOMAIN code:SPARKLE_DELTA_ARCHIVE_ERROR_CODE_BAD_CHUNK_SIZE userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"PARTIAL_IO_CHUNK_SIZE (%d) < decodedLength (%llu)", PARTIAL_IO_CHUNK_SIZE, decodedLength] }]; return NO; } if (decodedLength > PATH_MAX) { // Link is too long _error = [NSError errorWithDomain:SPARKLE_DELTA_ARCHIVE_ERROR_DOMAIN code:SPARKLE_DELTA_ARCHIVE_ERROR_CODE_LINK_TOO_LONG userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Decoded length for link (%llu) is too long.", decodedLength] }]; return NO; } char buffer[PATH_MAX + 1] = {0}; if (![self _readBuffer:buffer length:(int32_t)decodedLength]) { return NO; } NSString *destinationPath = [fileManager stringWithFileSystemRepresentation:buffer length:decodedLength]; if (destinationPath == nil) { _error = [NSError errorWithDomain:NSCocoaErrorDomain code:NSFileReadInvalidFileNameError userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Destination path for link %@ cannot be created in a file system representation: %@",itemFilePath, destinationPath] }]; return NO; } [fileManager removeItemAtPath:itemFilePath error:NULL]; NSError *createLinkError = nil; if (![fileManager createSymbolicLinkAtPath:itemFilePath withDestinationPath:destinationPath error:&createLinkError]) { _error = createLinkError; return NO; } char itemFilePathString[PATH_MAX + 1] = {0}; if (![itemFilePath getFileSystemRepresentation:itemFilePathString maxLength:sizeof(itemFilePathString) - 1]) { _error = [NSError errorWithDomain:NSCocoaErrorDomain code:NSFileReadInvalidFileNameError userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Link path to extract cannot be decoded and expressed as a file system representation: %@", itemFilePath] }]; return NO; } // We shouldn't fail if setting permissions on symlinks fail // Apple filesystems have file permissions for symbolic links but other linux file systems don't // So this may have no effect on some file systems over the network lchmod(itemFilePathString, mode); } } else if (S_ISDIR(mode)) { NSError *createDirectoryError = nil; if (![fileManager createDirectoryAtPath:itemFilePath withIntermediateDirectories:NO attributes:@{NSFilePosixPermissions: @(mode)} error:&createDirectoryError]) { _error = createDirectoryError; return NO; } } return YES; } - (BOOL)_writeBuffer:(void *)buffer length:(int32_t)length SPU_OBJC_DIRECT { if (_error != nil) { return NO; } switch (_compression) { case SPUDeltaCompressionModeNone: { BOOL success = (fwrite(buffer, (size_t)length, 1, _file) == 1); if (!success) { _error = [NSError errorWithDomain:NSPOSIXErrorDomain code:errno userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Failed to write %d uncompressed bytes.", length] }]; } return success; } case SPUDeltaCompressionModeBzip2: { #if SPARKLE_BUILD_BZIP2_DELTA_SUPPORT int bzerror = 0; BZ2_bzWrite(&bzerror, _bzipFile, buffer, length); switch (bzerror) { case BZ_OK: return YES; case BZ_IO_ERROR: _error = [NSError errorWithDomain:NSPOSIXErrorDomain code:errno userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Failed to write %d compressed bz2 bytes due to io error.", length] }]; return NO; default: _error = [NSError errorWithDomain:SPARKLE_BZIP2_ERROR_DOMAIN code:bzerror userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Failed to write %d compressed bz2 bytes.", length] }]; return NO; } #else return NO; #endif } case SPUDeltaCompressionModeLZMA: case SPUDeltaCompressionModeLZFSE: case SPUDeltaCompressionModeLZ4: case SPUDeltaCompressionModeZLIB: { _compressionStream.src_ptr = (const uint8_t *)buffer; _compressionStream.src_size = (size_t)length; FILE *file = _file; void *compressionBuffer = _compressionBuffer; while (_compressionStream.src_size > 0) { // Reset destination buffer _compressionStream.dst_ptr = (uint8_t *)compressionBuffer; _compressionStream.dst_size = COMPRESSION_BUFFER_SIZE; if (compression_stream_process(&_compressionStream, 0) == COMPRESSION_STATUS_ERROR) { _error = [NSError errorWithDomain:SPARKLE_COMPRESSION_ERROR_DOMAIN code:COMPRESSION_STATUS_ERROR userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Failed to write %d compressed bytes.", length] }]; return NO; } size_t compressedBytesWritten = (size_t)(_compressionStream.dst_ptr - (uint8_t *)compressionBuffer); if (compressedBytesWritten > 0 && fwrite(compressionBuffer, compressedBytesWritten, 1, file) < 1) { _error = [NSError errorWithDomain:NSPOSIXErrorDomain code:errno userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Failed to write %zu compressed bytes.", compressedBytesWritten] }]; return NO; } } return YES; } } } - (void)writeHeader:(SPUDeltaArchiveHeader *)header { char patchFilePath[PATH_MAX + 1] = {0}; if (![_patchFile getFileSystemRepresentation:patchFilePath maxLength:sizeof(patchFilePath) - 1]) { _error = [NSError errorWithDomain:NSCocoaErrorDomain code:NSFileReadInvalidFileNameError userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Failed to open header and represent as a file system representation: %@", _patchFile] }]; return; } FILE *file = fopen(patchFilePath, "wb"); if (file == NULL) { _error = [NSError errorWithDomain:NSPOSIXErrorDomain code:errno userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Failed to open patch file for writing: %@", _patchFile] }]; return; } _file = file; char magic[] = SPARKLE_DELTA_FORMAT_MAGIC; if (fwrite(magic, sizeof(magic) - 1, 1, file) < 1) { _error = [NSError errorWithDomain:NSPOSIXErrorDomain code:errno userInfo:@{ NSLocalizedDescriptionKey: @"Failed to write magic value due to io error" }]; return; } SPUDeltaCompressionMode compression = (header.compression == SPUDeltaCompressionModeDefault) ? SPUDeltaCompressionModeLZMA : header.compression; _compression = compression; if (fwrite(&compression, sizeof(compression), 1, file) < 1) { _error = [NSError errorWithDomain:NSPOSIXErrorDomain code:errno userInfo:@{ NSLocalizedDescriptionKey: @"Failed to write compression value due to io error" }]; return; } // We only support configuring compression level for bzip2 uint8_t compressionLevel = 0; switch (compression) { case SPUDeltaCompressionModeBzip2: #if SPARKLE_BUILD_BZIP2_DELTA_SUPPORT // Only 1 - 9 are valid, 0 is a special case for using default 9 if (header.compressionLevel <= 0 || header.compressionLevel > 9) { compressionLevel = 9; } else { compressionLevel = header.compressionLevel; } break; #else _error = [NSError errorWithDomain:SPARKLE_BZIP2_ERROR_DOMAIN code:-1 userInfo:@{ NSLocalizedDescriptionKey: @"Failed to write bzip2 patch because bzip2 support is disabled" }]; return; #endif // Some supported formats below have a documented level even though it's not customizable // Let's record them in the archive case SPUDeltaCompressionModeLZMA: compressionLevel = 6; break; case SPUDeltaCompressionModeZLIB: compressionLevel = 5; break; // These formats don't have any documented level or aren't applicable case SPUDeltaCompressionModeLZ4: case SPUDeltaCompressionModeLZFSE: case SPUDeltaCompressionModeNone: compressionLevel = 0; } SparkleDeltaArchiveMetadata metadata = {.compressionLevel = compressionLevel, .fileSystemCompression = header.fileSystemCompression}; if (fwrite(&metadata, sizeof(metadata), 1, file) < 1) { _error = [NSError errorWithDomain:NSPOSIXErrorDomain code:errno userInfo:@{ NSLocalizedDescriptionKey: @"Failed to write metadata value due to io error" }]; return; } switch (compression) { case SPUDeltaCompressionModeNone: break; case SPUDeltaCompressionModeBzip2: { #if SPARKLE_BUILD_BZIP2_DELTA_SUPPORT int bzerror = 0; // Compression level can be 1 - 9 int blockSize100k = (int)compressionLevel; BZFILE *bzipFile = BZ2_bzWriteOpen(&bzerror, file, blockSize100k, 0, 0); if (bzipFile == NULL) { switch (bzerror) { case BZ_IO_ERROR: _error = [NSError errorWithDomain:NSPOSIXErrorDomain code:errno userInfo:@{ NSLocalizedDescriptionKey: @"Failed to open bz2 stream for writing due to io error" }]; break; default: _error = [NSError errorWithDomain:SPARKLE_BZIP2_ERROR_DOMAIN code:bzerror userInfo:@{ NSLocalizedDescriptionKey: @"Failed to open bz2 stream for writing" }]; break; } return; } _bzipFile = bzipFile; #endif break; } case SPUDeltaCompressionModeLZMA: case SPUDeltaCompressionModeLZFSE: case SPUDeltaCompressionModeLZ4: case SPUDeltaCompressionModeZLIB: { if (compression_stream_init(&_compressionStream, COMPRESSION_STREAM_ENCODE, compressionAlgorithmForMode(compression)) != COMPRESSION_STATUS_OK) { _error = [NSError errorWithDomain:SPARKLE_COMPRESSION_ERROR_DOMAIN code:COMPRESSION_STATUS_ERROR userInfo:@{ NSLocalizedDescriptionKey: @"Failed to open compression stream for writing" }]; return; } _initializedCompressionStream = YES; break; } } if (![self createBuffers]) { return; } uint16_t majorVersion = header.majorVersion; [self _writeBuffer:&majorVersion length:sizeof(majorVersion)]; uint16_t minorVersion = header.minorVersion; [self _writeBuffer:&minorVersion length:sizeof(minorVersion)]; [self _writeBuffer:header.beforeTreeHash length:BINARY_DELTA_HASH_LENGTH]; [self _writeBuffer:header.afterTreeHash length:BINARY_DELTA_HASH_LENGTH]; if (majorVersion >= SUBinaryDeltaMajorVersion4) { NSDate *bundleCreationDate = header.bundleCreationDate; // If bundleCreationDate == nil, we will write out a 0 time interval double timeInterval = bundleCreationDate.timeIntervalSinceReferenceDate; [self _writeBuffer:&timeInterval length:sizeof(timeInterval)]; } } - (void)addItem:(SPUDeltaArchiveItem *)item { [_writableItems addObject:item]; } - (void)finishEncodingItems { if (_error != nil) { return; } NSMutableArray *writableItems = _writableItems; // Build relative path table for tracking file clones NSMutableDictionary *relativePathToIndexTable = [NSMutableDictionary dictionary]; uint32_t currentRelativePathIndex = 0; for (SPUDeltaArchiveItem *item in writableItems) { NSString *relativePath = item.relativeFilePath; assert(relativePath != nil); relativePathToIndexTable[relativePath] = @(currentRelativePathIndex); currentRelativePathIndex++; } // Clone commands reference relative file paths in this table but sometimes there may not // be an entry if extraction for an original item was skipped. Fill out any missing file path entries. // For example, if A.app has Contents/A and B.app has Contents/A and Contents/B, // where A and B's contents are the same and A is the same in both apps, normally we would not record Contents/A because its extraction was skipped. However now B is a clone of A so we need a record for A. NSMutableArray *newClonedPathEntries = [NSMutableArray array]; for (SPUDeltaArchiveItem *item in writableItems) { NSString *clonedRelativePath = item.clonedRelativePath; if (clonedRelativePath != nil && relativePathToIndexTable[clonedRelativePath] == nil) { [newClonedPathEntries addObject:clonedRelativePath]; relativePathToIndexTable[clonedRelativePath] = @(currentRelativePathIndex); currentRelativePathIndex++; } } if (relativePathToIndexTable.count > UINT32_MAX) { // Very unlikely but we should guard against this // Clones rely on 32-bit indexes _error = [NSError errorWithDomain:SPARKLE_DELTA_ARCHIVE_ERROR_DOMAIN code:SPARKLE_DELTA_ARCHIVE_ERROR_CODE_TOO_MANY_FILES userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"There are too many file entries to create a patch for (more than %u)", UINT32_MAX] }]; return; } // Compute length of path section to write uint64_t totalPathLength = 0; for (SPUDeltaArchiveItem *item in writableItems) { NSString *relativePath = item.relativeFilePath; char relativePathString[PATH_MAX + 1] = {0}; if (![relativePath getFileSystemRepresentation:relativePathString maxLength:sizeof(relativePathString) - 1]) { _error = [NSError errorWithDomain:NSCocoaErrorDomain code:NSFileReadInvalidFileNameError userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Relative path cannot be retrieved and expressed as a file system representation: %@", relativePath] }]; break; } totalPathLength += strlen(relativePathString) + 1; } for (NSString *clonedPathEntry in newClonedPathEntries) { char relativePathString[PATH_MAX + 1] = {0}; if (![clonedPathEntry getFileSystemRepresentation:relativePathString maxLength:sizeof(relativePathString) - 1]) { _error = [NSError errorWithDomain:NSCocoaErrorDomain code:NSFileReadInvalidFileNameError userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Relative path for clone cannot be retrieved and expressed as a file system representation: %@", clonedPathEntry] }]; break; } totalPathLength += strlen(relativePathString) + 1; } if (_error != nil) { return; } // Write total expected length of path section if (![self _writeBuffer:&totalPathLength length:sizeof(totalPathLength)]) { return; } // Write all of the relative paths for (SPUDeltaArchiveItem *item in writableItems) { NSString *relativePath = item.relativeFilePath; char pathBuffer[PATH_MAX + 1] = {0}; if (![relativePath getFileSystemRepresentation:pathBuffer maxLength:PATH_MAX]) { _error = [NSError errorWithDomain:NSCocoaErrorDomain code:NSFileReadInvalidFileNameError userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Relative path cannot be encoded and expressed as a file system representation: %@", relativePath] }]; break; } if (![self _writeBuffer:pathBuffer length:(int32_t)strlen(pathBuffer) + 1]) { break; } } for (NSString *clonedPathEntry in newClonedPathEntries) { char pathBuffer[PATH_MAX + 1] = {0}; if (![clonedPathEntry getFileSystemRepresentation:pathBuffer maxLength:PATH_MAX]) { _error = [NSError errorWithDomain:NSCocoaErrorDomain code:NSFileReadInvalidFileNameError userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Relative path for clone cannot be encoded and expressed as a file system representation: %@", clonedPathEntry] }]; break; } if (![self _writeBuffer:pathBuffer length:(int32_t)strlen(pathBuffer) + 1]) { break; } } if (_error != nil) { return; } // Encode the items for (SPUDeltaArchiveItem *item in writableItems) { // Store commands SPUDeltaItemCommands commands = item.commands; if (![self _writeBuffer:&commands length:sizeof(commands)]) { break; } // Check if we need to encode additional data if ((commands & SPUDeltaItemCommandClone) != 0) { // Store any desired file permissions changes for the clone // Clones can be binary diffs from other sources too. Since we are creating a // new file in that case (rather than a copy) we want to store file mode as well if ((commands & SPUDeltaItemCommandModifyPermissions) != 0) { NSString *sourcePath = item.sourcePath; assert(sourcePath != nil); char sourcePathString[PATH_MAX + 1] = {0}; if (![sourcePath getFileSystemRepresentation:sourcePathString maxLength:sizeof(sourcePathString) - 1]) { _error = [NSError errorWithDomain:NSCocoaErrorDomain code:NSFileReadInvalidFileNameError userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Path cannot be decoded and expressed as a file system representation while encoding cloned binary diff item: %@", sourcePath] }]; break; } struct stat fileInfo = {0}; if (lstat(sourcePathString, &fileInfo) != 0) { _error = [NSError errorWithDomain:NSPOSIXErrorDomain code:errno userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Failed to lstat() on %@", sourcePath] }]; break; } uint16_t extractMode = fileInfo.st_mode; uint16_t encodedMode; if ((commands & SPUDeltaItemCommandModifyPermissions) != 0) { encodedMode = (extractMode & ~PERMISSION_FLAGS) | item.mode; } else { encodedMode = extractMode; } item.mode = extractMode; // Store file mode (including desired permissions) if (![self _writeBuffer:&encodedMode length:sizeof(encodedMode)]) { break; } } // Store index to relative path table NSString *clonedRelativePath = item.clonedRelativePath; assert(clonedRelativePath != nil); NSNumber *relativePathIndex = relativePathToIndexTable[clonedRelativePath]; if (relativePathIndex == nil) { // We have quite a problem here _error = [NSError errorWithDomain:SPARKLE_DELTA_ARCHIVE_ERROR_DOMAIN code:SPARKLE_DELTA_ARCHIVE_ERROR_CODE_BAD_CLONE_LOOKUP userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Relative file path index for %@ could not be located", clonedRelativePath] }]; break; } uint32_t relativePathCIndex = relativePathIndex.unsignedIntValue; if (![self _writeBuffer:&relativePathCIndex length:sizeof(relativePathCIndex)]) { break; } if ((commands & SPUDeltaItemCommandBinaryDiff) != 0) { NSString *itemPath = item.itemFilePath; assert(itemPath != nil); char itemFilePathString[PATH_MAX + 1] = {0}; if (![itemPath getFileSystemRepresentation:itemFilePathString maxLength:sizeof(itemFilePathString) - 1]) { _error = [NSError errorWithDomain:NSCocoaErrorDomain code:NSFileReadInvalidFileNameError userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Path cannot be decoded and expressed as a file system representation while encoding cloned binary diff item: %@", itemPath] }]; break; } struct stat fileInfo = {0}; if (lstat(itemFilePathString, &fileInfo) != 0) { _error = [NSError errorWithDomain:NSPOSIXErrorDomain code:errno userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Failed to lstat() on %@", itemPath] }]; break; } uint64_t dataLength = (uint64_t)fileInfo.st_size; if (![self _writeBuffer:&dataLength length:sizeof(dataLength)]) { break; } item.codedDataLength = dataLength; } } else if ((commands & SPUDeltaItemCommandExtract) != 0 || (commands & SPUDeltaItemCommandBinaryDiff) != 0) { uint16_t extractMode = 0; if ((commands & SPUDeltaItemCommandExtract) != 0 || (commands & SPUDeltaItemCommandModifyPermissions) != 0) { NSString *sourcePath = item.sourcePath; assert(sourcePath != nil); char sourceFilePathString[PATH_MAX + 1] = {0}; if (![sourcePath getFileSystemRepresentation:sourceFilePathString maxLength:sizeof(sourceFilePathString) - 1]) { _error = [NSError errorWithDomain:NSCocoaErrorDomain code:NSFileReadInvalidFileNameError userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Path cannot be decoded and expressed as a file system representation while encoding items: %@", sourcePath] }]; break; } struct stat sourceFileInfo = {0}; if (lstat(sourceFilePathString, &sourceFileInfo) != 0) { _error = [NSError errorWithDomain:NSPOSIXErrorDomain code:errno userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Failed to lstat() on %@", sourcePath] }]; break; } // For symbolic links we always default to 0755 when adding new items // Symbolic link permissions can get more easily lost when moving to other (linux) filesystems, // so we only support the macOS default if (S_ISLNK(sourceFileInfo.st_mode)) { extractMode = (sourceFileInfo.st_mode & ~PERMISSION_FLAGS) | VALID_SYMBOLIC_LINK_PERMISSIONS; } else { extractMode = sourceFileInfo.st_mode; } uint16_t encodedMode; if ((commands & SPUDeltaItemCommandModifyPermissions) != 0) { encodedMode = (extractMode & ~PERMISSION_FLAGS) | item.mode; } else { encodedMode = extractMode; } item.mode = extractMode; // Store file mode (including desired permissions) if (![self _writeBuffer:&encodedMode length:sizeof(encodedMode)]) { break; } } // Store data length // Length doesn't matter for directory names (we already track the name in the relative path) NSString *itemPath = item.itemFilePath; assert(itemPath != nil); char itemFilePathString[PATH_MAX + 1] = {0}; if (![itemPath getFileSystemRepresentation:itemFilePathString maxLength:sizeof(itemFilePathString) - 1]) { _error = [NSError errorWithDomain:NSCocoaErrorDomain code:NSFileReadInvalidFileNameError userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Path cannot be decoded and expressed as a file system representation while encoding items: %@", itemPath] }]; break; } struct stat itemFileInfo = {0}; if (lstat(itemFilePathString, &itemFileInfo) != 0) { _error = [NSError errorWithDomain:NSPOSIXErrorDomain code:errno userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Failed to lstat() on %@", itemPath] }]; break; } if ((commands & SPUDeltaItemCommandBinaryDiff) != 0 || S_ISREG(extractMode)) { uint64_t dataLength = (uint64_t)itemFileInfo.st_size; if (![self _writeBuffer:&dataLength length:sizeof(dataLength)]) { break; } item.codedDataLength = dataLength; } else if (S_ISLNK(extractMode)) { off_t fileSize = itemFileInfo.st_size; if (fileSize > UINT16_MAX) { _error = [NSError errorWithDomain:SPARKLE_DELTA_ARCHIVE_ERROR_DOMAIN code:SPARKLE_DELTA_ARCHIVE_ERROR_CODE_LINK_TOO_LONG userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Link path has a destination that is too long: %lld bytes", fileSize] }]; break; } uint16_t dataLength = (uint16_t)fileSize; if (![self _writeBuffer:&dataLength length:sizeof(dataLength)]) { break; } item.codedDataLength = dataLength; } } else if ((commands & SPUDeltaItemCommandModifyPermissions) != 0) { // Store file permissions uint16_t mode = item.mode; if (![self _writeBuffer:&mode length:sizeof(mode)]) { break; } } } if (_error != nil) { return; } // Encode end command marker SPUDeltaItemCommands endCommand = SPUDeltaItemCommandEndMarker; if (![self _writeBuffer:&endCommand length:sizeof(endCommand)]) { return; } // Encode all of our file contents void *tempBuffer = _partialChunkBuffer; for (SPUDeltaArchiveItem *item in writableItems) { SPUDeltaItemCommands commands = item.commands; if ((commands & SPUDeltaItemCommandExtract) != 0 || (commands & SPUDeltaItemCommandBinaryDiff) != 0) { NSString *itemPath = item.itemFilePath; assert(itemPath != nil); mode_t extractMode = item.mode; if ((commands & SPUDeltaItemCommandBinaryDiff) != 0 || S_ISREG(extractMode)) { // Write out file contents to archive in chunks uint64_t totalItemSize = item.codedDataLength; if (totalItemSize > 0) { char itemFilePathString[PATH_MAX + 1] = {0}; if (![itemPath getFileSystemRepresentation:itemFilePathString maxLength:sizeof(itemFilePathString) - 1]) { _error = [NSError errorWithDomain:NSCocoaErrorDomain code:NSFileReadInvalidFileNameError userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Path to finish encoding cannot be decoded and expressed as a file system representation: %@", itemPath] }]; break; } FILE *inputFile = fopen(itemFilePathString, "rb"); if (inputFile == NULL) { _error = [NSError errorWithDomain:NSPOSIXErrorDomain code:errno userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Failed to open file for reading while encoding items: %@", itemPath] }]; break; } uint64_t bytesLeftoverToCopy = totalItemSize; while (bytesLeftoverToCopy > 0) { uint64_t currentBlockSize = (bytesLeftoverToCopy >= PARTIAL_IO_CHUNK_SIZE) ? PARTIAL_IO_CHUNK_SIZE : bytesLeftoverToCopy; if (fread(tempBuffer, currentBlockSize, 1, inputFile) < 1) { _error = [NSError errorWithDomain:NSPOSIXErrorDomain code:errno userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Failed to read %llu chunk bytes while encoding items", currentBlockSize] }]; break; } if (![self _writeBuffer:tempBuffer length:(int32_t)currentBlockSize]) { break; } bytesLeftoverToCopy -= currentBlockSize; } fclose(inputFile); if (_error != nil) { break; } } } else if (S_ISLNK(extractMode)) { char itemFilePathString[PATH_MAX + 1] = {0}; if (![itemPath getFileSystemRepresentation:itemFilePathString maxLength:sizeof(itemFilePathString) - 1]) { _error = [NSError errorWithDomain:NSCocoaErrorDomain code:NSFileReadInvalidFileNameError userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Link path to finish encoding cannot be decoded and expressed as a file system representation: %@", itemPath] }]; break; } char linkDestination[PATH_MAX + 1] = {0}; ssize_t linkDestinationLength = readlink(itemFilePathString, linkDestination, PATH_MAX); if (linkDestinationLength < 0) { _error = [NSError errorWithDomain:NSPOSIXErrorDomain code:errno userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Failed to readlink() file at %@", itemPath] }]; break; } if (![self _writeBuffer:linkDestination length:(int32_t)strlen(linkDestination)]) { break; } } } } // Close up and write final data to compressed streams #if SPARKLE_BUILD_BZIP2_DELTA_SUPPORT if (_bzipFile != NULL) { int bzerror = 0; BZ2_bzWriteClose64(&bzerror, _bzipFile, 0, NULL, NULL, NULL, NULL); if (bzerror == BZ_IO_ERROR) { _error = [NSError errorWithDomain:NSPOSIXErrorDomain code:errno userInfo:@{ NSLocalizedDescriptionKey: @"Failed to write and close bzip2 file due to IO error" }]; return; } } else #endif if (_initializedCompressionStream) { void *compressionBuffer = _compressionBuffer; FILE *file = _file; _compressionStream.src_size = 0; while (_compressionStream.dst_size > 0) { _compressionStream.dst_ptr = (uint8_t *)compressionBuffer; _compressionStream.dst_size = COMPRESSION_BUFFER_SIZE; compression_status status = compression_stream_process(&_compressionStream, COMPRESSION_STREAM_FINALIZE); if (status == COMPRESSION_STATUS_ERROR) { _error = [NSError errorWithDomain:SPARKLE_COMPRESSION_ERROR_DOMAIN code:COMPRESSION_STATUS_ERROR userInfo:@{ NSLocalizedDescriptionKey: @"Failed to write final bits of Compression based file" }]; return; } size_t compressedBytesToWrite = (size_t)(_compressionStream.dst_ptr - (uint8_t *)compressionBuffer); if (compressedBytesToWrite > 0) { if (fwrite(compressionBuffer, compressedBytesToWrite, 1, file) < 1) { _error = [NSError errorWithDomain:NSPOSIXErrorDomain code:errno userInfo:@{ NSLocalizedDescriptionKey: @"Failed to write and close Compression based file due to io error" }]; return; } } if (status == COMPRESSION_STATUS_END) { // We're done break; } } } } @end ================================================ FILE: Autoupdate/SPUXarDeltaArchive.h ================================================ // // SPUXarDeltaArchive.h // Autoupdate // // Created by Mayur Pawashe on 12/28/21. // Copyright © 2021 Sparkle Project. All rights reserved. // #if SPARKLE_BUILD_LEGACY_DELTA_SUPPORT #import #import "SPUDeltaArchiveProtocol.h" #import "SPUDeltaCompressionMode.h" NS_ASSUME_NONNULL_BEGIN // Legacy container format for binary delta archives SPU_OBJC_DIRECT_MEMBERS @interface SPUXarDeltaArchive : NSObject - (instancetype)initWithPatchFileForWriting:(NSString *)patchFile SPU_OBJC_DIRECT; - (instancetype)initWithPatchFileForReading:(NSString *)patchFile SPU_OBJC_DIRECT; @end NS_ASSUME_NONNULL_END #endif ================================================ FILE: Autoupdate/SPUXarDeltaArchive.m ================================================ // // SPUXarDeltaArchive.m // Autoupdate // // Created by Mayur Pawashe on 12/28/21. // Copyright © 2021 Sparkle Project. All rights reserved. // #if SPARKLE_BUILD_LEGACY_DELTA_SUPPORT #import "SPUXarDeltaArchive.h" #include #include "SUBinaryDeltaCommon.h" #import #import #include "AppKitPrevention.h" #if __MAC_OS_X_VERSION_MAX_ALLOWED >= 120000 #define HAS_XAR_GET_SAFE_PATH 1 #else #define HAS_XAR_GET_SAFE_PATH 0 #endif #if HAS_XAR_GET_SAFE_PATH // This is preferred over xar_get_path (which is deprecated) when it's available // Don't use OS availability for this API extern char *xar_get_safe_path(xar_file_t f) __attribute__((weak_import)); #endif // Xar attribute keys #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" // Errors #define SPARKLE_DELTA_XAR_ARCHIVE_ERROR_DOMAIN @"Sparkle XAR Archive" #define SPARKLE_DELTA_XAR_ARCHIVE_ERROR_CODE_OPEN_FAILURE 1 #define SPARKLE_DELTA_XAR_ARCHIVE_ERROR_CODE_ADD_FAILURE 2 #define SPARKLE_DELTA_XAR_ARCHIVE_ERROR_CODE_EXTRACT_FAILURE 3 #define SPARKLE_DELTA_XAR_ARCHIVE_ERROR_CODE_UNSUPPORTED_COMPRESSION_FAILURE 4 @implementation SPUXarDeltaArchive { NSMutableDictionary *_fileTable; NSString *_patchFile; xar_t _x; int32_t _xarMode; } @synthesize error = _error; - (instancetype)initWithPatchFileForWriting:(NSString *)patchFile { self = [super init]; if (self != nil) { _patchFile = [patchFile copy]; _xarMode = WRITE; _fileTable = [NSMutableDictionary dictionary]; } return self; } - (instancetype)initWithPatchFileForReading:(NSString *)patchFile { self = [super init]; if (self != nil) { _patchFile = [patchFile copy]; _xarMode = READ; } return self; } - (void)dealloc { [self close]; } - (void)close { if (_x != NULL) { xar_close(_x); _x = NULL; } } // This indicates if safe extraction is available at compile time (SDK), but not if it's available at runtime. + (BOOL)maySupportSafeExtraction { return HAS_XAR_GET_SAFE_PATH; } - (nullable SPUDeltaArchiveHeader *)readHeader { NSString *patchFile = _patchFile; #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdeprecated-declarations" // Sparkle's XAR delta archives have been superseded by Sparkle's own format xar_t x = xar_open(patchFile.fileSystemRepresentation, READ); #pragma clang diagnostic pop if (x == NULL) { _error = [NSError errorWithDomain:SPARKLE_DELTA_XAR_ARCHIVE_ERROR_DOMAIN code:SPARKLE_DELTA_XAR_ARCHIVE_ERROR_CODE_OPEN_FAILURE userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Failed to xar_open() file for reading: %@", patchFile] }]; return nil; } _x = x; uint16_t majorDiffVersion = SUBinaryDeltaMajorVersionFirst; uint16_t minorDiffVersion = 0; NSString *expectedBeforeHash = nil; NSString *expectedAfterHash = 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_KEY) == 0) { { // available in version 2.0 or later const char *value = NULL; xar_subdoc_prop_get(subdoc, MAJOR_DIFF_VERSION_KEY, &value); if (value != NULL) { majorDiffVersion = (uint16_t)[@(value) intValue]; } } { // available in version 2.0 or later const char *value = NULL; xar_subdoc_prop_get(subdoc, MINOR_DIFF_VERSION_KEY, &value); if (value != NULL) { minorDiffVersion = (uint16_t)[@(value) intValue]; } } // available in version 2.0 or later { const char *value = NULL; xar_subdoc_prop_get(subdoc, BEFORE_TREE_SHA1_KEY, &value); if (value != NULL) { expectedBeforeHash = @(value); } } // available in version 2.0 or later { const char *value = NULL; xar_subdoc_prop_get(subdoc, AFTER_TREE_SHA1_KEY, &value); if (value != NULL) { expectedAfterHash = @(value); } } } } unsigned char rawExpectedBeforeHash[BINARY_DELTA_HASH_LENGTH] = {0}; getRawHashFromDisplayHash(rawExpectedBeforeHash, expectedBeforeHash); unsigned char rawExpectedAfterHash[BINARY_DELTA_HASH_LENGTH] = {0}; getRawHashFromDisplayHash(rawExpectedAfterHash, expectedAfterHash); // I wasn't able to figure out how to retrieve the compression options from xar, // so we will use default flags to indicate the info isn't available return [[SPUDeltaArchiveHeader alloc] initWithCompression:SPUDeltaCompressionModeDefault compressionLevel:0 fileSystemCompression:false majorVersion:majorDiffVersion minorVersion:minorDiffVersion beforeTreeHash:rawExpectedBeforeHash afterTreeHash:rawExpectedAfterHash bundleCreationDate:nil]; } - (void)writeHeader:(SPUDeltaArchiveHeader *)header { NSString *patchFile = _patchFile; #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdeprecated-declarations" // Sparkle's XAR delta archives have been superseded by Sparkle's own format xar_t x = xar_open(patchFile.fileSystemRepresentation, WRITE); #pragma clang diagnostic pop if (x == NULL) { _error = [NSError errorWithDomain:SPARKLE_DELTA_XAR_ARCHIVE_ERROR_DOMAIN code:SPARKLE_DELTA_XAR_ARCHIVE_ERROR_CODE_OPEN_FAILURE userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Failed to xar_open() file for writing: %@", patchFile] }]; return; } _x = x; SPUDeltaCompressionMode compression = (header.compression == SPUDeltaCompressionModeDefault ? SPUDeltaCompressionModeBzip2 : header.compression); uint8_t compressionLevel; // Only 1 - 9 are valid, 0 is special case to use default level 9 if (header.compressionLevel <= 0 || header.compressionLevel > 9) { compressionLevel = 9; } else { compressionLevel = header.compressionLevel; } switch (compression) { case SPUDeltaCompressionModeNone: xar_opt_set(x, XAR_OPT_COMPRESSION, XAR_OPT_VAL_NONE); break; case SPUDeltaCompressionModeBzip2: { xar_opt_set(x, XAR_OPT_COMPRESSION, "bzip2"); char buffer[256] = {0}; snprintf(buffer, sizeof(buffer) - 1, "%d", compressionLevel); xar_opt_set(x, XAR_OPT_COMPRESSIONARG, buffer); break; } case SPUDeltaCompressionModeLZMA: case SPUDeltaCompressionModeLZFSE: case SPUDeltaCompressionModeLZ4: case SPUDeltaCompressionModeZLIB: { _error = [NSError errorWithDomain:SPARKLE_DELTA_XAR_ARCHIVE_ERROR_DOMAIN code:SPARKLE_DELTA_XAR_ARCHIVE_ERROR_CODE_UNSUPPORTED_COMPRESSION_FAILURE userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Version 2 patches only support bzip2 compression."] }]; return; } } xar_subdoc_t attributes = xar_subdoc_new(x, BINARY_DELTA_ATTRIBUTES_KEY); xar_subdoc_prop_set(attributes, MAJOR_DIFF_VERSION_KEY, [[NSString stringWithFormat:@"%u", header.majorVersion] UTF8String]); xar_subdoc_prop_set(attributes, MINOR_DIFF_VERSION_KEY, [[NSString stringWithFormat:@"%u", header.minorVersion] UTF8String]); xar_subdoc_prop_set(attributes, BEFORE_TREE_SHA1_KEY, [displayHashFromRawHash(header.beforeTreeHash) UTF8String]); xar_subdoc_prop_set(attributes, AFTER_TREE_SHA1_KEY, [displayHashFromRawHash(header.afterTreeHash) UTF8String]); } static xar_file_t xarAddFile(NSMutableDictionary *fileTable, xar_t x, NSString *relativePath, NSString *filePath) { NSArray *rootRelativePathComponents = relativePath.pathComponents; // Relative path must at least have starting "/" component and one more path component if (rootRelativePathComponents.count < 2) { return NULL; } NSArray *relativePathComponents = [rootRelativePathComponents subarrayWithRange:NSMakeRange(1, rootRelativePathComponents.count - 1)]; NSUInteger relativePathComponentsCount = relativePathComponents.count; // Build parent files as needed until we get to our final file we want to add // So if we get "Contents/Resources/foo.txt", we will first add "Contents" parent, // then "Resources" parent, then "foo.txt" as the final entry we want to add // We store every file we add into a fileTable for easy referencing // Note if a diff has Contents/Resources/foo/ and Contents/Resources/foo/bar.txt, // due to sorting order we will add the foo directory first and won't end up with // misordering bugs xar_file_t lastParent = NULL; for (NSUInteger componentIndex = 0; componentIndex < relativePathComponentsCount; componentIndex++) { NSArray *subpathComponents = [relativePathComponents subarrayWithRange:NSMakeRange(0, componentIndex + 1)]; NSString *subpathKey = [subpathComponents componentsJoinedByString:@"/"]; xar_file_t cachedFile = (xar_file_t)[fileTable[subpathKey] pointerValue]; if (cachedFile != NULL) { lastParent = cachedFile; } else { xar_file_t newParent; BOOL atLastIndex = (componentIndex == relativePathComponentsCount - 1); NSString *lastPathComponent = subpathComponents.lastObject; if (atLastIndex && filePath != nil) { newParent = xar_add_frompath(x, lastParent, lastPathComponent.fileSystemRepresentation, filePath.fileSystemRepresentation); } else { newParent = xar_add_frombuffer(x, lastParent, lastPathComponent.fileSystemRepresentation, "", 1); } lastParent = newParent; fileTable[subpathKey] = [NSValue valueWithPointer:newParent]; } } return lastParent; } - (void)addItem:(SPUDeltaArchiveItem *)item { if (_error != nil) { return; } NSString *relativeFilePath = item.relativeFilePath; NSString *filePath = item.itemFilePath; SPUDeltaItemCommands commands = item.commands; uint16_t mode = item.mode; xar_file_t newFile = xarAddFile(_fileTable, _x, relativeFilePath, filePath); if (newFile == NULL) { _error = [NSError errorWithDomain:SPARKLE_DELTA_XAR_ARCHIVE_ERROR_DOMAIN code:SPARKLE_DELTA_XAR_ARCHIVE_ERROR_CODE_ADD_FAILURE userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Failed to add xar file entry: %@", relativeFilePath] }]; return; } if ((commands & SPUDeltaItemCommandDelete) != 0) { xar_prop_set(newFile, DELETE_KEY, "true"); } if ((commands & SPUDeltaItemCommandExtract) != 0) { xar_prop_set(newFile, EXTRACT_KEY, "true"); } if ((commands & SPUDeltaItemCommandBinaryDiff) != 0) { xar_prop_set(newFile, BINARY_DELTA_KEY, "true"); } if ((commands & SPUDeltaItemCommandModifyPermissions) != 0) { xar_prop_set(newFile, MODIFY_PERMISSIONS_KEY, [NSString stringWithFormat:@"%u", mode].UTF8String); } } - (void)finishEncodingItems { // Items are already encoded when they are extracted prior } - (void)enumerateItems:(void (^)(SPUDeltaArchiveItem *, BOOL *))itemHandler { if (_error != nil) { return; } BOOL exitedEarly = NO; xar_iter_t iter = xar_iter_new(); for (xar_file_t file = xar_file_first(_x, iter); file; file = xar_file_next(iter)) { char *pathCString; #if HAS_XAR_GET_SAFE_PATH #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wunguarded-availability-new" if (xar_get_safe_path != NULL) { pathCString = xar_get_safe_path(file); } #pragma clang diagnostic pop else #endif { #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdeprecated-declarations" pathCString = xar_get_path(file); #pragma clang diagnostic pop } if (pathCString == NULL) { continue; } NSString *relativePath = [[NSString alloc] initWithBytesNoCopy:pathCString length:strlen(pathCString) encoding:NSUTF8StringEncoding freeWhenDone:YES]; if (relativePath == nil) { free(pathCString); continue; } SPUDeltaItemCommands commands = (SPUDeltaItemCommands)0; { const char *value = NULL; if (xar_prop_get(file, DELETE_KEY, &value) == 0) { commands |= SPUDeltaItemCommandDelete; } } { const char *value = NULL; if (xar_prop_get(file, BINARY_DELTA_KEY, &value) == 0) { commands |= SPUDeltaItemCommandBinaryDiff; } } { const char *value = NULL; if (xar_prop_get(file, EXTRACT_KEY, &value) == 0) { commands |= SPUDeltaItemCommandExtract; } } uint16_t mode = 0; { const char *value = NULL; if (xar_prop_get(file, MODIFY_PERMISSIONS_KEY, &value) == 0) { commands |= SPUDeltaItemCommandModifyPermissions; mode = (uint16_t)[@(value) intValue]; } } SPUDeltaArchiveItem *item = [[SPUDeltaArchiveItem alloc] initWithRelativeFilePath:relativePath commands:commands mode:mode]; item.xarContext = file; itemHandler(item, &exitedEarly); if (exitedEarly) { break; } } xar_iter_free(iter); } - (BOOL)extractItem:(SPUDeltaArchiveItem *)item { if (_error != nil) { return NO; } assert(item.itemFilePath != nil); assert(item.xarContext != NULL); xar_file_t file = (xar_file_t)item.xarContext; if (xar_extract_tofile(_x, file, item.itemFilePath.fileSystemRepresentation) != 0) { _error = [NSError errorWithDomain:SPARKLE_DELTA_XAR_ARCHIVE_ERROR_DOMAIN code:SPARKLE_DELTA_XAR_ARCHIVE_ERROR_CODE_EXTRACT_FAILURE userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Failed to extract xar file entry to %@", item.itemFilePath] }]; return NO; } return YES; } @end #endif ================================================ FILE: Autoupdate/SUBinaryDeltaApply.h ================================================ // // SUBinaryDeltaApply.h // Sparkle // // Created by Mark Rowe on 2009-06-01. // Copyright 2009 Mark Rowe. All rights reserved. // #ifndef SUBINARYDELTAAPPLY_H #define SUBINARYDELTAAPPLY_H #import @class NSString; BOOL applyBinaryDelta(NSString *source, NSString *destination, NSString *patchFile, BOOL verbose, void (^progressCallback)(double), NSError * __autoreleasing *error); #endif ================================================ FILE: Autoupdate/SUBinaryDeltaApply.m ================================================ // // SUBinaryDeltaApply.m // Sparkle // // Created by Mark Rowe on 2009-06-01. // Copyright 2009 Mark Rowe. All rights reserved. // #import "SUBinaryDeltaApply.h" #import "SUBinaryDeltaCommon.h" #import "SPUDeltaArchiveProtocol.h" #import "SPUDeltaArchive.h" #import #import #include "bspatch.h" #include #include #import #include "AppKitPrevention.h" static BOOL applyBinaryDeltaToFile(NSString *patchFile, NSString *sourceFilePath, NSString *destinationFilePath) { const char *argv[] = {"/usr/bin/bspatch", [sourceFilePath fileSystemRepresentation], [destinationFilePath fileSystemRepresentation], [patchFile fileSystemRepresentation]}; BOOL success = (bspatch(4, argv) == 0); unlink([patchFile fileSystemRepresentation]); return success; } BOOL applyBinaryDelta(NSString *source, NSString *finalDestination, NSString *patchFile, BOOL verbose, void (^progressCallback)(double progress), NSError *__autoreleasing *error) { SPUDeltaArchiveHeader *header = nil; id archive = SPUDeltaArchiveReadPatchAndHeader(patchFile, &header); if (archive.error != nil) { if (error != NULL) { *error = archive.error; } return NO; } progressCallback(0/7.0); SUBinaryDeltaMajorVersion majorDiffVersion = (SUBinaryDeltaMajorVersion)header.majorVersion; uint16_t minorDiffVersion = header.minorVersion; unsigned char *expectedBeforeHash = header.beforeTreeHash; unsigned char *expectedAfterHash = header.afterTreeHash; if (majorDiffVersion < SUBinaryDeltaMajorVersionFirst) { if (error != NULL) { *error = [NSError errorWithDomain:NSCocoaErrorDomain code:NSFileReadCorruptFileError userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Unable to identify diff-version %u in delta. Giving up.", majorDiffVersion] }]; } return NO; } if (majorDiffVersion < SUBinaryDeltaMajorVersionFirstSupported) { if (error != NULL) { *error = [NSError errorWithDomain:NSCocoaErrorDomain code:NSFileReadCorruptFileError userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Applying version %u patches is no longer supported.", majorDiffVersion] }]; } return NO; } if (majorDiffVersion > SUBinaryDeltaMajorVersionLatest) { if (error != NULL) { *error = [NSError errorWithDomain:NSCocoaErrorDomain code:NSFileReadUnknownError userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"A later version is needed to apply this patch (on major version %u, but patch requests version %u).", SUBinaryDeltaMajorVersionLatest, majorDiffVersion] }]; } return NO; } // Reject patches that did not generate valid hierarchical xar container paths // These will not succeed to patch using recent versions of BinaryDelta if ([[archive class] maySupportSafeExtraction] && majorDiffVersion == SUBinaryDeltaMajorVersion2 && minorDiffVersion < 3) { if (error != NULL) { *error = [NSError errorWithDomain:NSCocoaErrorDomain code:NSFileReadCorruptFileError userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"This patch version (%u.%u) is too old and potentially unsafe to apply. Please re-generate the patch using the latest version of BinaryDelta or generate_appcast. New version %u.%u patches will still be compatible with older versions of Sparkle.", majorDiffVersion, minorDiffVersion, majorDiffVersion, latestMinorVersionForMajorVersion(majorDiffVersion)] }]; } return NO; } if (expectedBeforeHash == nil || expectedAfterHash == nil) { if (error != NULL) { *error = [NSError errorWithDomain:NSCocoaErrorDomain code:NSFileReadCorruptFileError userInfo:@{ NSLocalizedDescriptionKey: @"Unable to find before-sha1 or after-sha1 metadata in delta. Giving up." }]; } return NO; } if (verbose) { fprintf(stderr, "Applying version %u.%u patch...\n", majorDiffVersion, minorDiffVersion); fprintf(stderr, "Verifying source..."); } progressCallback(1/7.0); unsigned char beforeHash[BINARY_DELTA_HASH_LENGTH] = {0}; if (!getRawHashOfTreeWithVersion(beforeHash, source, majorDiffVersion)) { if (verbose) { fprintf(stderr, "\n"); } if (error != NULL) { *error = [NSError errorWithDomain:NSCocoaErrorDomain code:NSFileReadUnknownError userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Unable to calculate hash of tree %@", source] }]; } return NO; } if (memcmp(beforeHash, expectedBeforeHash, BINARY_DELTA_HASH_LENGTH) != 0) { if (verbose) { fprintf(stderr, "\n"); } if (error != NULL) { *error = [NSError errorWithDomain:NSCocoaErrorDomain code:NSFileReadUnknownError userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Source doesn't have expected hash (%@ != %@). Giving up.", displayHashFromRawHash(expectedBeforeHash), displayHashFromRawHash(beforeHash)] }]; } return NO; } if (verbose) { fprintf(stderr, "\nCopying files..."); } progressCallback(2/7.0); // Make a temporary destination path if necessary // If we want to apply file system compression after we're done applying, we'll need to use a different // temporary path NSString *destination; if (header.fileSystemCompression) { destination = [finalDestination.stringByDeletingLastPathComponent stringByAppendingPathComponent:[NSString stringWithFormat:@".tmp.%@", finalDestination.lastPathComponent]]; } else { destination = finalDestination; } if (!removeTree(destination)) { if (verbose) { fprintf(stderr, "\n"); } if (error != NULL) { *error = [NSError errorWithDomain:NSCocoaErrorDomain code:NSFileWriteUnknownError userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Failed to remove %@", destination] }]; } return NO; } progressCallback(3/7.0); NSFileManager *fileManager = [[NSFileManager alloc] init]; if (!copyTree(fileManager, source, destination)) { if (verbose) { fprintf(stderr, "\n"); } if (error != NULL) { *error = [NSError errorWithDomain:NSCocoaErrorDomain code:NSFileWriteUnknownError userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Failed to copy %@ to %@", source, destination] }]; } return NO; } // Preserve file creation date only for the root item if the date is recorded // (requires major version 4 or later) NSDate *bundleCreationDate = header.bundleCreationDate; if (bundleCreationDate != nil) { NSError *setFileCreationDateError = nil; if (![fileManager setAttributes:@{NSFileCreationDate: bundleCreationDate} ofItemAtPath:destination error:&setFileCreationDateError]) { fprintf(stderr, "\nWarning: failed to set file creation date: %s", setFileCreationDateError.localizedDescription.UTF8String); } } progressCallback(4/7.0); if (verbose) { fprintf(stderr, "\nPatching..."); } // Ensure error is cleared out in advance if (error != NULL) { *error = nil; } [archive enumerateItems:^(SPUDeltaArchiveItem *item, BOOL *stop) { NSString *relativePath = item.relativeFilePath; if ([relativePath.pathComponents containsObject:@".."]) { if (error != NULL) { *error = [NSError errorWithDomain:NSCocoaErrorDomain code:NSFileWriteUnknownError userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Relative path '%@' contains '..' path component", relativePath] }]; } *stop = YES; return; } NSString *sourceFilePath = [source stringByAppendingPathComponent:relativePath]; NSString *destinationFilePath = [destination stringByAppendingPathComponent:relativePath]; { NSString *destinationParentDirectory = destinationFilePath.stringByDeletingLastPathComponent; NSDictionary *destinationParentDirectoryAttributes = [fileManager attributesOfItemAtPath:destinationParentDirectory error:NULL]; // It is OK for the directory parent to not exist if it has already been removed if (destinationParentDirectoryAttributes != nil) { // But if it does exist, make sure the entry in the parent directory we're looking at is good // If it's inside a symlink, this is not good in any circumstance NSString *fileType = destinationParentDirectoryAttributes[NSFileType]; if ([fileType isEqualToString:NSFileTypeSymbolicLink]) { if (error != NULL) { *error = [NSError errorWithDomain:NSCocoaErrorDomain code:NSFileWriteUnknownError userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Failed to create patch because '%@' cannot be a symbolic link.", destinationParentDirectory] }]; } *stop = YES; return; } } } // Don't use -[NSFileManager fileExistsAtPath:] because it will follow symbolic links BOOL fileExisted = verbose && [fileManager attributesOfItemAtPath:destinationFilePath error:nil]; BOOL removedFile = NO; // Files that have no property set that we check for will get ignored // This is important because they aren't part of the delta, just part of the directory structure SPUDeltaItemCommands commands = item.commands; if ((commands & SPUDeltaItemCommandDelete) != 0) { if (!removeTree(destinationFilePath)) { if (verbose) { fprintf(stderr, "\n"); } if (error != NULL) { *error = [NSError errorWithDomain:NSCocoaErrorDomain code:NSFileWriteUnknownError userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"delete: failed to remove %@", destination] }]; } *stop = YES; return; } removedFile = YES; } if ((commands & SPUDeltaItemCommandClone) != 0 && (commands & SPUDeltaItemCommandBinaryDiff) == 0) { NSString *clonedRelativePath = item.clonedRelativePath; if ([clonedRelativePath.pathComponents containsObject:@".."]) { if (error != NULL) { *error = [NSError errorWithDomain:NSCocoaErrorDomain code:NSFileWriteUnknownError userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Relative path for clone '%@' contains '..' path component", clonedRelativePath] }]; } *stop = YES; return; } NSString *clonedOriginalPath = [source stringByAppendingPathComponent:clonedRelativePath]; // Ensure there isn't an item already at our destination [fileManager removeItemAtPath:destinationFilePath error:NULL]; NSError *copyError = nil; if (![fileManager copyItemAtPath:clonedOriginalPath toPath:destinationFilePath error:©Error]) { if (verbose) { fprintf(stderr, "\n"); } if (error != NULL) { *error = copyError; } *stop = YES; return; } if (verbose) { fprintf(stderr, "\n✂️ %s %s -> %s", VERBOSE_CLONED, [clonedRelativePath fileSystemRepresentation], [relativePath fileSystemRepresentation]); } } else if ((commands & SPUDeltaItemCommandBinaryDiff) != 0) { NSString *tempDiffFile = temporaryFilename(@"apply-binary-delta"); item.itemFilePath = tempDiffFile; if (![archive extractItem:item]) { if (verbose) { fprintf(stderr, "\n"); } if (error != NULL) { *error = [NSError errorWithDomain:NSCocoaErrorDomain code:NSFileWriteUnknownError userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Unable to extract diffed file to %@", tempDiffFile], NSUnderlyingErrorKey: (NSError * _Nonnull)archive.error }]; } *stop = YES; return; } NSString *sourceDiffFilePath; NSString *clonedRelativePath; if ((commands & SPUDeltaItemCommandClone) != 0) { clonedRelativePath = item.clonedRelativePath; if ([clonedRelativePath.pathComponents containsObject:@".."]) { if (error != NULL) { *error = [NSError errorWithDomain:NSCocoaErrorDomain code:NSFileWriteUnknownError userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Relative path for clone '%@' contains '..' path component", clonedRelativePath] }]; } *stop = YES; return; } sourceDiffFilePath = [source stringByAppendingPathComponent:clonedRelativePath]; } else { sourceDiffFilePath = sourceFilePath; clonedRelativePath = nil; } // Decide if we need to preserve original file permissions from the original file // applyBinaryDeltaToFile() normally preserves file permissions on the file it's replacing. // However this is not possible if the destination file we're patching is not writable. // We also need to preserve permissions for clones except when we'll be changing permissions later anyway. BOOL needsToCopyFilePermissions; if (![fileManager isWritableFileAtPath:destinationFilePath]) { // Remove the file non-writable we're patching that may cause issues [fileManager removeItemAtPath:destinationFilePath error:NULL]; // We will need to preserve permissions if there is no need to make permission changes later on needsToCopyFilePermissions = (commands & SPUDeltaItemCommandModifyPermissions) == 0; } else { needsToCopyFilePermissions = ((commands & SPUDeltaItemCommandClone) != 0) && ((commands & SPUDeltaItemCommandModifyPermissions) == 0); } if (!applyBinaryDeltaToFile(tempDiffFile, sourceDiffFilePath, destinationFilePath)) { if (verbose) { fprintf(stderr, "\n"); } if (error != NULL) { *error = [NSError errorWithDomain:NSCocoaErrorDomain code:NSFileWriteUnknownError userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Unable to patch %@ to destination %@", sourceFilePath, destinationFilePath] }]; } *stop = YES; return; } if (needsToCopyFilePermissions) { struct stat sourceFileInfo = {0}; if (lstat(sourceDiffFilePath.fileSystemRepresentation, &sourceFileInfo) != 0) { if (verbose) { fprintf(stderr, "\n"); } if (error != NULL) { *error = [NSError errorWithDomain:NSCocoaErrorDomain code:NSFileWriteUnknownError userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Unable to retrieve stat info from %@", sourceFilePath] }]; } *stop = YES; return; } if (chmod(destinationFilePath.fileSystemRepresentation, sourceFileInfo.st_mode) != 0) { if (verbose) { fprintf(stderr, "\n"); } if (error != NULL) { *error = [NSError errorWithDomain:NSCocoaErrorDomain code:NSFileWriteNoPermissionError userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Unable to modify permissions (%u) on file %@", sourceFileInfo.st_mode, destinationFilePath] }]; } *stop = YES; return; } } if (verbose) { if ((commands & SPUDeltaItemCommandClone) != 0) { fprintf(stderr, "\n🔨 %s %s -> %s", VERBOSE_PATCHED, [clonedRelativePath fileSystemRepresentation], [relativePath fileSystemRepresentation]); } else { fprintf(stderr, "\n🔨 %s %s", VERBOSE_PATCHED, [relativePath fileSystemRepresentation]); } } } else if ((commands & SPUDeltaItemCommandExtract) != 0) { // extract and permission modifications don't coexist item.itemFilePath = destinationFilePath; if (![archive extractItem:item]) { if (verbose) { fprintf(stderr, "\n"); } if (error != NULL) { *error = [NSError errorWithDomain:NSCocoaErrorDomain code:NSFileWriteUnknownError userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Unable to extract file to %@", destinationFilePath], NSUnderlyingErrorKey: (NSError * _Nonnull)archive.error }]; } *stop = YES; return; } if (verbose) { if (fileExisted) { fprintf(stderr, "\n✏️ %s %s", VERBOSE_UPDATED, [relativePath fileSystemRepresentation]); } else { fprintf(stderr, "\n✅ %s %s", VERBOSE_ADDED, [relativePath fileSystemRepresentation]); } } } else if (verbose && removedFile) { fprintf(stderr, "\n❌ %s %s", VERBOSE_DELETED, [relativePath fileSystemRepresentation]); } if ((commands & SPUDeltaItemCommandModifyPermissions) != 0) { mode_t mode = (mode_t)item.mode; if (!modifyPermissions(destinationFilePath, mode)) { if (verbose) { fprintf(stderr, "\n"); } if (error != NULL) { *error = [NSError errorWithDomain:NSCocoaErrorDomain code:NSFileWriteNoPermissionError userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Unable to modify permissions (%u) on file %@", mode, destinationFilePath] }]; } *stop = YES; return; } if (verbose) { fprintf(stderr, "\n👮 %s %s (0%o)", VERBOSE_MODIFIED, [relativePath fileSystemRepresentation], (unsigned int)(mode & PERMISSION_FLAGS)); } } }]; [archive close]; // Set error from enumerating items if we have encountered an error and haven't set it yet NSError *archiveError = archive.error; if (archiveError != nil) { if (verbose) { fprintf(stderr, "\n"); } if (error != NULL && *error == nil) { *error = archiveError; } removeTree(destination); return NO; } progressCallback(5/7.0); // Re-apply file system compression is requested if (header.fileSystemCompression) { if (verbose) { fprintf(stderr, "\nApplying file system compression..."); } NSTask *dittoTask = [[NSTask alloc] init]; dittoTask.executableURL = [NSURL fileURLWithPath:@"/usr/bin/ditto" isDirectory:NO]; dittoTask.arguments = @[@"--hfsCompression", destination, finalDestination]; // If we fail to apply file system compression, we will try falling back to not doing this BOOL failedToApplyFileSystemCompression = NO; NSError *launchError = nil; if (![dittoTask launchAndReturnError:&launchError]) { failedToApplyFileSystemCompression = YES; fprintf(stderr, "\nWarning: failed to launch ditto task for file compression: %s", launchError.localizedDescription.UTF8String); } if (!failedToApplyFileSystemCompression) { [dittoTask waitUntilExit]; if (dittoTask.terminationStatus != 0) { failedToApplyFileSystemCompression = YES; fprintf(stderr, "\nWarning: ditto task for file compression returned exit status %d", dittoTask.terminationStatus); } } if (failedToApplyFileSystemCompression) { // Try to replace bundle normally if (![fileManager replaceItemAtURL:[NSURL fileURLWithPath:finalDestination] withItemAtURL:[NSURL fileURLWithPath:destination isDirectory:YES] backupItemName:nil options:(NSFileManagerItemReplacementOptions)0 resultingItemURL:NULL error:error]) { removeTree(destination); return NO; } } else { // Remove original copy [fileManager removeItemAtURL:[NSURL fileURLWithPath:destination isDirectory:YES] error:NULL]; } } progressCallback(6/7.0); if (verbose) { fprintf(stderr, "\nVerifying destination..."); } unsigned char afterHash[BINARY_DELTA_HASH_LENGTH] = {0}; if (!getRawHashOfTreeWithVersion(afterHash, finalDestination, majorDiffVersion)) { if (verbose) { fprintf(stderr, "\n"); } if (error != NULL) { *error = [NSError errorWithDomain:NSCocoaErrorDomain code:NSFileReadUnknownError userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Unable to calculate hash of tree %@", finalDestination] }]; } removeTree(finalDestination); return NO; } if (memcmp(afterHash, expectedAfterHash, BINARY_DELTA_HASH_LENGTH) != 0) { if (verbose) { fprintf(stderr, "\n"); } if (error != NULL) { *error = [NSError errorWithDomain:NSCocoaErrorDomain code:NSFileReadUnknownError userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Destination doesn't have expected hash (%@ != %@). Giving up.", displayHashFromRawHash(expectedAfterHash), displayHashFromRawHash(afterHash)] }]; } removeTree(finalDestination); return NO; } progressCallback(7/7.0); if (verbose) { fprintf(stderr, "\nDone!\n"); } return YES; } ================================================ FILE: Autoupdate/SUBinaryDeltaCommon.h ================================================ // // SUBinaryDeltaCommon.h // Sparkle // // Created by Mark Rowe on 2009-06-01. // Copyright 2009 Mark Rowe. All rights reserved. // #ifndef SUBINARYDELTACOMMON_H #define SUBINARYDELTACOMMON_H #import #import "SPUDeltaCompressionMode.h" #include #define PERMISSION_FLAGS (S_IRWXU | S_IRWXG | S_IRWXO | S_ISUID | S_ISGID | S_ISVTX) #define VALID_SYMBOLIC_LINK_PERMISSIONS 0755 // Enforcing Sparkle's executable permissions to be valid allows us to perform a preflight test before // downloading delta items #define VALID_SPARKLE_EXECUTABLE_PERMISSIONS 0755 #define APPLE_CODE_SIGN_XATTR_CODE_DIRECTORY_KEY "com.apple.cs.CodeDirectory" #define APPLE_CODE_SIGN_XATTR_CODE_REQUIREMENTS_KEY "com.apple.cs.CodeRequirements" #define APPLE_CODE_SIGN_XATTR_CODE_SIGNATURE_KEY "com.apple.cs.CodeSignature" #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 VERBOSE_CLONED "Cloned" // file is cloned in content from a differently named file // Relative path of custom icon data that may be set on a bundle via a resource fork #define CUSTOM_ICON_PATH @"/Icon\r" // 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) { // Note: support for creating or applying version 1 deltas have been removed SUBinaryDeltaMajorVersion1 = 1, SUBinaryDeltaMajorVersion2 = 2, SUBinaryDeltaMajorVersion3 = 3, SUBinaryDeltaMajorVersion4 = 4, }; extern SUBinaryDeltaMajorVersion SUBinaryDeltaMajorVersionDefault; extern SUBinaryDeltaMajorVersion SUBinaryDeltaMajorVersionLatest; extern SUBinaryDeltaMajorVersion SUBinaryDeltaMajorVersionFirst; extern SUBinaryDeltaMajorVersion SUBinaryDeltaMajorVersionFirstSupported; // Additional compression methods for version 3 or 4 patches that we have for debugging are zlib, bzip2, none #define COMPRESSION_METHOD_ARGUMENT_DESCRIPTION @"The compression method to use for generating delta updates. Supported methods for version 3 delta files are 'lzma' (best compression, slowest), 'lzfse' (good compression, fast), 'lz4' (worse compression, fastest), and 'default'. Note that version 2 delta files only support 'bzip2', and 'default' so other methods will be ignored if version 2 files are being generated. The 'default' compression for version 3 or 4 delta files is currently lzma." //#define COMPRESSION_LEVEL_ARGUMENT_DESCRIPTION @"The compression level to use for generating delta updates. This only applies if the compression method used is bzip2 which accepts values from 1 - 9. A special value of 0 will use the default compression level." // This is the same as CC_SHA1_DIGEST_LENGTH // Major versions >= 4 use a crc32 hash (using a subset of these bytes) while older versions use a sha1 hash #define BINARY_DELTA_HASH_LENGTH 20 SPUDeltaCompressionMode deltaCompressionModeFromDescription(NSString *description, BOOL *requestValid); NSString *deltaCompressionStringFromMode(SPUDeltaCompressionMode mode); extern int compareFiles(const FTSENT **a, const FTSENT **b); BOOL getRawHashOfTreeWithVersion(void *hashBuffer, NSString *path, uint16_t majorVersion); BOOL getRawHashOfTreeAndFileTablesWithVersion(void *hashBuffer, NSString *path, uint16_t majorVersion, NSMutableDictionary *> *hashToFileKeyDictionary, NSMutableDictionary *fileKeyToHashDictionary); NSString *displayHashFromRawHash(const unsigned char *hash); void getRawHashFromDisplayHash(unsigned char *hash, NSString *hexHash); extern NSString *hashOfTreeWithVersion(NSString *path, uint16_t majorVersion); extern NSString *hashOfTree(NSString *path); extern BOOL removeTree(NSString *path); extern BOOL copyTree(NSFileManager *fileManager, 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*); uint16_t latestMinorVersionForMajorVersion(SUBinaryDeltaMajorVersion majorVersion); #endif ================================================ FILE: Autoupdate/SUBinaryDeltaCommon.m ================================================ // // SUBinaryDeltaCommon.m // Sparkle // // Created by Mark Rowe on 2009-06-01. // Copyright 2009 Mark Rowe. All rights reserved. // #include "SUBinaryDeltaCommon.h" #include #include // for crc32() #include #include #include #include #include #include #include #include #include "AppKitPrevention.h" // Note: the framework bundle version must be bumped, and generate_appcast must be updated to compare it, // when we add/change new major versions and defaults. Unit tests need to be updated to use new versions too. SUBinaryDeltaMajorVersion SUBinaryDeltaMajorVersionDefault = SUBinaryDeltaMajorVersion4; SUBinaryDeltaMajorVersion SUBinaryDeltaMajorVersionLatest = SUBinaryDeltaMajorVersion4; SUBinaryDeltaMajorVersion SUBinaryDeltaMajorVersionFirst = SUBinaryDeltaMajorVersion1; SUBinaryDeltaMajorVersion SUBinaryDeltaMajorVersionFirstSupported = SUBinaryDeltaMajorVersion2; SPUDeltaCompressionMode deltaCompressionModeFromDescription(NSString *requestedDescription, BOOL *requestValid) { // Set to NO later if request was not valid if (requestValid != NULL) { *requestValid = YES; } SPUDeltaCompressionMode compression; NSString *description = requestedDescription.lowercaseString; if ([description isEqualToString:@"default"]) { compression = SPUDeltaCompressionModeDefault; } else if ([description isEqualToString:@"none"]) { compression = SPUDeltaCompressionModeNone; } else if ([description isEqualToString:@"bzip2"]) { compression = SPUDeltaCompressionModeBzip2; } else if ([description isEqualToString:@"lzma"]) { compression = SPUDeltaCompressionModeLZMA; } else if ([description isEqualToString:@"lzfse"]) { compression = SPUDeltaCompressionModeLZFSE; } else if ([description isEqualToString:@"lz4"]) { compression = SPUDeltaCompressionModeLZ4; } else if ([description isEqualToString:@"zlib"]) { compression = SPUDeltaCompressionModeZLIB; } else { compression = SPUDeltaCompressionModeDefault; if (requestValid != NULL) { *requestValid = NO; } } return compression; } NSString *deltaCompressionStringFromMode(SPUDeltaCompressionMode mode) { switch (mode) { case SPUDeltaCompressionModeBzip2: return @"bzip2"; case SPUDeltaCompressionModeLZMA: return @"LZMA"; case SPUDeltaCompressionModeNone: return @"no"; case SPUDeltaCompressionModeLZ4: return @"LZ4"; case SPUDeltaCompressionModeLZFSE: return @"LZFSE"; case SPUDeltaCompressionModeZLIB: return @"ZLIB"; default: break; } if (mode == SPUDeltaCompressionModeDefault) { return @"default"; } return @"unknown"; } int compareFiles(const FTSENT **a, const FTSENT **b) { return strcoll_l((*a)->fts_name, (*b)->fts_name, _c_locale); } NSString *pathRelativeToDirectory(NSString *directory, NSString *path) { NSUInteger directoryLength = [directory length]; if ([path hasPrefix:directory]) return [path substringFromIndex:directoryLength]; return path; } NSString *stringWithFileSystemRepresentation(const char *input) { return [[NSFileManager defaultManager] stringWithFileSystemRepresentation:input length:strlen(input)]; } uint16_t latestMinorVersionForMajorVersion(SUBinaryDeltaMajorVersion majorVersion) { switch (majorVersion) { case SUBinaryDeltaMajorVersion1: return 2; case SUBinaryDeltaMajorVersion2: return 5; case SUBinaryDeltaMajorVersion3: return 3; case SUBinaryDeltaMajorVersion4: return 2; } return 0; } NSString *temporaryFilename(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 = (char *)data.mutableBytes; int fd = mkstemp(buffer); if (fd == -1) { perror("mkstemp"); return nil; } if (close(fd) != 0) { perror("close"); return nil; } 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 = (char *)data.mutableBytes; char *templateResult = mkdtemp(buffer); if (templateResult == NULL) { perror("mkdtemp"); return nil; } return stringWithFileSystemRepresentation(templateResult); } static void sha1HashOfBuffer(unsigned char *hash, const char *buffer, ssize_t bufferLength) { assert(bufferLength >= 0 && bufferLength <= UINT32_MAX); CC_SHA1_CTX hashContext; CC_SHA1_Init(&hashContext); CC_SHA1_Update(&hashContext, buffer, (CC_LONG)bufferLength); CC_SHA1_Final(hash, &hashContext); } static BOOL crc32HashOfFileContents(uLong *outChecksum, FTSENT *ent, void *tempBuffer, size_t tempBufferSize) { uLong checksum = *outChecksum; 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 NO; } checksum = crc32(checksum, (const Bytef *)linkDestination, (unsigned int)linkDestinationLength); } else if (ent->fts_info == FTS_F) { ssize_t fileSize = ent->fts_statp->st_size; uint64_t encodedFileSize = (uint64_t)fileSize; checksum = crc32(checksum, (const Bytef *)&encodedFileSize, sizeof(encodedFileSize)); if (fileSize > 0) { FILE *file = fopen(ent->fts_path, "rb"); if (file == NULL) { perror("fopen"); return NO; } size_t bytesLeft = (size_t)fileSize; while (bytesLeft > 0) { size_t bytesToConsume = (bytesLeft >= tempBufferSize) ? tempBufferSize : bytesLeft; if (fread(tempBuffer, bytesToConsume, 1, file) < 1) { perror("fread"); fclose(file); return NO; } checksum = crc32(checksum, (const Bytef *)tempBuffer, (uInt)bytesToConsume); bytesLeft -= bytesToConsume; } fclose(file); } } else { return NO; } *outChecksum = checksum; return YES; } static BOOL sha1HashOfFileContents(unsigned char *hash, FTSENT *ent, void *tempBuffer, size_t tempBufferSize) { 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 NO; } sha1HashOfBuffer(hash, linkDestination, linkDestinationLength); } else if (ent->fts_info == FTS_F) { ssize_t fileSize = ent->fts_statp->st_size; if (fileSize <= 0) { sha1HashOfBuffer(hash, NULL, 0); } else { FILE *file = fopen(ent->fts_path, "rb"); if (file == NULL) { perror("fopen"); return NO; } CC_SHA1_CTX hashContext; CC_SHA1_Init(&hashContext); size_t bytesLeft = (size_t)fileSize; while (bytesLeft > 0) { size_t bytesToConsume = (bytesLeft >= tempBufferSize) ? tempBufferSize : bytesLeft; if (fread(tempBuffer, bytesToConsume, 1, file) < 1) { perror("fread"); fclose(file); return NO; } CC_SHA1_Update(&hashContext, tempBuffer, (CC_LONG)bytesToConsume); bytesLeft -= bytesToConsume; } CC_SHA1_Final(hash, &hashContext); fclose(file); } } else if (ent->fts_info == FTS_D) { memset(hash, 0xdd, CC_SHA1_DIGEST_LENGTH); } else { return NO; } return YES; } BOOL getRawHashOfTreeWithVersion(void *hashBuffer, NSString *path, uint16_t majorVersion) { return getRawHashOfTreeAndFileTablesWithVersion(hashBuffer, path, majorVersion, nil, nil); } BOOL getRawHashOfTreeAndFileTablesWithVersion(void *hashBuffer, NSString *path, uint16_t majorVersion, NSMutableDictionary *> *hashToFileKeyDictionary, NSMutableDictionary *fileKeyToHashDictionary) { char pathBuffer[PATH_MAX] = { 0 }; if (![path getFileSystemRepresentation:pathBuffer maxLength:sizeof(pathBuffer)]) { return NO; } const size_t tempBufferSize = 16384; void *tempBuffer = calloc(1, tempBufferSize); if (tempBuffer == NULL) { perror("calloc"); return NO; } char *const sourcePaths[] = { pathBuffer, 0 }; FTS *fts = fts_open(sourcePaths, FTS_PHYSICAL | FTS_NOCHDIR, compareFiles); if (!fts) { perror("fts_open"); free(tempBuffer); return NO; } CC_SHA1_CTX hashContext; const uLong initialCrc32Value = crc32(0L, Z_NULL, 0); uLong crc32ChecksumValue = initialCrc32Value; if (majorVersion < SUBinaryDeltaMajorVersion4) { CC_SHA1_Init(&hashContext); } // Ensure the path uses filesystem-specific Unicode normalization #1017 NSString *normalizedPath = stringWithFileSystemRepresentation(pathBuffer); 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 *relativePath = pathRelativeToDirectory(normalizedPath, stringWithFileSystemRepresentation(ent->fts_path)); // Ignore icon resource fork data if (relativePath.length == 0 || [relativePath isEqualToString:CUSTOM_ICON_PATH]) { continue; } NSData *fileHashKey; if (majorVersion >= SUBinaryDeltaMajorVersion4) { if (ent->fts_info == FTS_D) { // No need to hash any further values for directories // We hash relative file path and file type later fileHashKey = nil; } else { uLong fileContentsChecksum = initialCrc32Value; if (!crc32HashOfFileContents(&fileContentsChecksum, ent, tempBuffer, tempBufferSize)) { fts_close(fts); free(tempBuffer); return NO; } uint64_t encodedFileContentsChecksum = fileContentsChecksum; crc32ChecksumValue = crc32(crc32ChecksumValue, (const Bytef *)&encodedFileContentsChecksum, sizeof(encodedFileContentsChecksum)); if (ent->fts_info == FTS_F) { fileHashKey = [NSData dataWithBytes:&encodedFileContentsChecksum length:sizeof(encodedFileContentsChecksum)]; } else { fileHashKey = nil; } } } else { unsigned char fileHash[CC_SHA1_DIGEST_LENGTH]; if (!sha1HashOfFileContents(fileHash, ent, tempBuffer, tempBufferSize)) { fts_close(fts); free(tempBuffer); return NO; } CC_SHA1_Update(&hashContext, fileHash, sizeof(fileHash)); if (ent->fts_info == FTS_F) { fileHashKey = [NSData dataWithBytes:fileHash length:sizeof(fileHash)]; } else { fileHashKey = nil; } } // For file hash tables we only track regular files if (fileHashKey != nil) { if (hashToFileKeyDictionary != nil) { if (hashToFileKeyDictionary[fileHashKey] == nil) { hashToFileKeyDictionary[fileHashKey] = [NSMutableArray array]; } [hashToFileKeyDictionary[fileHashKey] addObject:relativePath]; } if (fileKeyToHashDictionary != nil) { fileKeyToHashDictionary[relativePath] = fileHashKey; } } const char *relativePathBytes = [relativePath fileSystemRepresentation]; if (majorVersion >= SUBinaryDeltaMajorVersion4) { crc32ChecksumValue = crc32(crc32ChecksumValue, (const Bytef *)relativePathBytes, (uInt)strlen(relativePathBytes)); } else { CC_SHA1_Update(&hashContext, relativePathBytes, (CC_LONG)strlen(relativePathBytes)); } uint16_t mode = ent->fts_statp->st_mode; uint16_t type = ent->fts_info; uint16_t permissions = mode & PERMISSION_FLAGS; // permission of symlinks are 0777 on some linux file systems and can't be changed, // differing from the 0755 macOS default. // hardcoding a value helps avoid differences between filesystems. uint16_t hashedPermissions = (ent->fts_info == FTS_SL) ? VALID_SYMBOLIC_LINK_PERMISSIONS : permissions; if (majorVersion >= SUBinaryDeltaMajorVersion4) { crc32ChecksumValue = crc32(crc32ChecksumValue, (const Bytef *)&type, sizeof(type)); crc32ChecksumValue = crc32(crc32ChecksumValue, (const Bytef *)&hashedPermissions, sizeof(hashedPermissions)); } else { CC_SHA1_Update(&hashContext, &type, sizeof(type)); CC_SHA1_Update(&hashContext, &hashedPermissions, sizeof(hashedPermissions)); } } free(tempBuffer); fts_close(fts); if (majorVersion >= SUBinaryDeltaMajorVersion4) { uint64_t encodedCrc32ChecksumValue = crc32ChecksumValue; memset(hashBuffer, 0, BINARY_DELTA_HASH_LENGTH); memcpy(hashBuffer, &encodedCrc32ChecksumValue, sizeof(encodedCrc32ChecksumValue)); } else { CC_SHA1_Final((unsigned char *)hashBuffer, &hashContext); } return YES; } void getRawHashFromDisplayHash(unsigned char *hash, NSString *hexHash) { const char *hexString = hexHash.UTF8String; if (hexString == NULL) { return; } for (size_t blockIndex = 0; blockIndex < BINARY_DELTA_HASH_LENGTH; blockIndex++) { const char *currentBlock = hexString + blockIndex * 2; char convertedBlock[3] = {currentBlock[0], currentBlock[1], '\0'}; hash[blockIndex] = (unsigned char)strtol(convertedBlock, NULL, 16); } } NSString *displayHashFromRawHash(const unsigned char *hash) { char hexHash[BINARY_DELTA_HASH_LENGTH * 2 + 1] = {0}; for (size_t i = 0; i < BINARY_DELTA_HASH_LENGTH; i++) { snprintf(hexHash + i * 2, 3, "%02x", hash[i]); } return @(hexHash); } NSString *hashOfTreeWithVersion(NSString *path, uint16_t majorVersion) { unsigned char hash[BINARY_DELTA_HASH_LENGTH] = {0}; if (!getRawHashOfTreeWithVersion(hash, path, majorVersion)) { return nil; } return displayHashFromRawHash(hash); } extern NSString *hashOfTree(NSString *path) { return hashOfTreeWithVersion(path, SUBinaryDeltaMajorVersionLatest); } BOOL removeTree(NSString *path) { 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(NSFileManager *fileManager, NSString *source, NSString *dest) { return [fileManager copyItemAtURL:[NSURL fileURLWithPath:source] toURL:[NSURL fileURLWithPath:dest] error:NULL]; } 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 objectForKey:NSFilePosixPermissions]; if (!permissions) { return NO; } mode_t newMode = ([permissions unsignedShortValue] & ~PERMISSION_FLAGS) | desiredPermissions; int (*changeModeFunc)(const char *, mode_t) = [(NSString *)[attributes objectForKey:NSFileType] isEqualToString:NSFileTypeSymbolicLink] ? lchmod : chmod; if (changeModeFunc([path fileSystemRepresentation], newMode) != 0) { return NO; } return YES; } ================================================ FILE: Autoupdate/SUBinaryDeltaCreate.h ================================================ // // 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" #import "SPUDeltaArchiveProtocol.h" @class NSString; BOOL createBinaryDelta(NSString *source, NSString *destination, NSString *patchFile, SUBinaryDeltaMajorVersion majorVersion, SPUDeltaCompressionMode compression, uint8_t compressionLevel, BOOL verbose, NSError * __autoreleasing *error); #endif ================================================ FILE: Autoupdate/SUBinaryDeltaCreate.m ================================================ // // 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 "SPUDeltaArchiveProtocol.h" #import "SPUSparkleDeltaArchive.h" #import "SPUXarDeltaArchive.h" #import #include #include #include #include #include #include #include #include #include #include "AppKitPrevention.h" extern int bsdiff(int argc, const char **argv); @interface CreateBinaryDeltaOperation : NSOperation @property (nonatomic, copy, readonly) NSString *relativePath; @property (nonatomic, copy, readonly) NSString *clonedRelativePath; @property (nonatomic, readonly) NSString *resultPath; @property (nonatomic, readonly) NSNumber *oldPermissions; @property (nonatomic, readonly) NSNumber *permissions; @property (nonatomic, readonly) NSString *fromPath; @property (nonatomic, readonly) BOOL changingPermissions; - (id)initWithRelativePath:(NSString *)relativePath clonedRelativePath:(NSString *)clonedRelativePath oldTree:(NSString *)oldTree newTree:(NSString *)newTree oldPermissions:(NSNumber *)oldPermissions newPermissions:(NSNumber *)permissions changingPermissions:(BOOL)changingPermissions SPU_OBJC_DIRECT; @end @implementation CreateBinaryDeltaOperation { NSString *_toPath; } @synthesize relativePath = _relativePath; @synthesize clonedRelativePath = _clonedRelativePath; @synthesize resultPath = _resultPath; @synthesize oldPermissions = _oldPermissions; @synthesize permissions = _permissions; @synthesize fromPath = _fromPath; @synthesize changingPermissions = _changingPermissions; - (id)initWithRelativePath:(NSString *)relativePath clonedRelativePath:(NSString *)clonedRelativePath oldTree:(NSString *)oldTree newTree:(NSString *)newTree oldPermissions:(NSNumber *)oldPermissions newPermissions:(NSNumber *)permissions changingPermissions:(BOOL)changingPermissions { if ((self = [super init])) { _relativePath = [relativePath copy]; _clonedRelativePath = [clonedRelativePath copy]; _oldPermissions = oldPermissions; _permissions = permissions; _changingPermissions = changingPermissions; if (clonedRelativePath == nil) { _fromPath = [oldTree stringByAppendingPathComponent:relativePath]; } else { _fromPath = [oldTree stringByAppendingPathComponent:clonedRelativePath]; } _toPath = [newTree stringByAppendingPathComponent:relativePath]; } return self; } - (void)main { NSString *temporaryFile = temporaryFilename(@"BinaryDelta"); const char *argv[] = { "/usr/bin/bsdiff", [_fromPath fileSystemRepresentation], [_toPath fileSystemRepresentation], [temporaryFile fileSystemRepresentation] }; int result = bsdiff(4, argv); if (result == 0) { _resultPath = temporaryFile; } } @end #define INFO_PATH_KEY @"path" #define INFO_TYPE_KEY @"type" #define INFO_PERMISSIONS_KEY @"permissions" #define INFO_SIZE_KEY @"size" static NSDictionary *infoForFile(FTSENT *ent) { 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; NSString *path = @(ent->fts_path); assert(path != nil); return @{ INFO_PATH_KEY: path != nil ? path : @"", INFO_TYPE_KEY: @(ent->fts_info), INFO_PERMISSIONS_KEY: @(permissions), INFO_SIZE_KEY: @(size) }; } static bool aclExists(const FTSENT *ent) { // macOS 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 bool codeSignatureExtendedAttributeExists(const FTSENT *ent) { const int options = XATTR_NOFOLLOW; ssize_t listSize = listxattr(ent->fts_path, NULL, 0, options); if (listSize == -1) { return false; } char *buffer = (char *)malloc((size_t)listSize); assert(buffer != NULL); ssize_t sizeBack = listxattr(ent->fts_path, buffer, (size_t)listSize, options); assert(sizeBack == listSize); size_t startCharacterIndex = 0; for (size_t characterIndex = 0; characterIndex < (size_t)listSize; characterIndex++) { if (buffer[characterIndex] == '\0') { char *attribute = &buffer[startCharacterIndex]; size_t length = characterIndex - startCharacterIndex; if (strncmp(APPLE_CODE_SIGN_XATTR_CODE_DIRECTORY_KEY, attribute, length) == 0 || strncmp(APPLE_CODE_SIGN_XATTR_CODE_REQUIREMENTS_KEY, attribute, length) == 0 || strncmp(APPLE_CODE_SIGN_XATTR_CODE_SIGNATURE_KEY, attribute, length) == 0) { free(buffer); return true; } startCharacterIndex = characterIndex + 1; } } free(buffer); 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 = [(NSNumber *)newInfo[INFO_SIZE_KEY] unsignedLongLongValue]; if (fileSize < MIN_FILE_SIZE_FOR_CREATING_DELTA) { return YES; } if (!originalInfo) { return YES; } unsigned short originalInfoType = [(NSNumber *)originalInfo[INFO_TYPE_KEY] unsignedShortValue]; unsigned short newInfoType = [(NSNumber *)newInfo[INFO_TYPE_KEY] unsignedShortValue]; if (originalInfoType != newInfoType || originalInfoType != FTS_F) { // File types are different or they're not regular files return YES; } NSString *originalPath = originalInfo[INFO_PATH_KEY]; NSString *newPath = newInfo[INFO_PATH_KEY]; // Skip delta if the files are equal in content if ([[NSFileManager defaultManager] contentsEqualAtPath:originalPath andPath:newPath]) { return YES; } return NO; } static BOOL shouldDeleteThenExtract(NSDictionary *originalInfo, NSDictionary *newInfo) { if (!originalInfo) { return NO; } if ([(NSNumber *)originalInfo[INFO_TYPE_KEY] unsignedShortValue] != [(NSNumber *)newInfo[INFO_TYPE_KEY] unsignedShortValue]) { return YES; } return NO; } static BOOL shouldSkipExtracting(NSDictionary *originalInfo, NSDictionary *newInfo) { if (!originalInfo) { return NO; } unsigned short originalInfoType = [(NSNumber *)originalInfo[INFO_TYPE_KEY] unsignedShortValue]; unsigned short newInfoType = [(NSNumber *)newInfo[INFO_TYPE_KEY] unsignedShortValue]; if (originalInfoType != newInfoType) { // File types are different return NO; } NSString *originalPath = originalInfo[INFO_PATH_KEY]; NSString *newPath = newInfo[INFO_PATH_KEY]; // Don't skip extract if files/symlinks entries are not equal in content // (note if the entries are directories, they are equal) if (originalInfoType != FTS_D && ![[NSFileManager defaultManager] contentsEqualAtPath:originalPath andPath:newPath]) { return NO; } return YES; } static BOOL shouldChangePermissions(NSDictionary *originalInfo, NSDictionary *newInfo) { if (!originalInfo) { return NO; } unsigned short originalInfoType = [(NSNumber *)originalInfo[INFO_TYPE_KEY] unsignedShortValue]; unsigned short newInfoType = [(NSNumber *)newInfo[INFO_TYPE_KEY] unsignedShortValue]; if (originalInfoType != newInfoType) { return NO; } unsigned short oldPermissions = [(NSNumber *)originalInfo[INFO_PERMISSIONS_KEY] unsignedShortValue]; unsigned short newPermissions = [(NSNumber *)newInfo[INFO_PERMISSIONS_KEY] unsignedShortValue]; if (oldPermissions == newPermissions) { return NO; } // We don't track new permissions on symbolic links that aren't the 0755 macOS default // Some linux / remotely mounted filesystems may not track permissions on symlinks and use 0777 // We don't want to pick up bad permissions if (newInfoType == FTS_SL && newPermissions != VALID_SYMBOLIC_LINK_PERMISSIONS) { return NO; } return YES; } #define MIN_SIZE_FOR_CLONE 4096 #define MIN_SIZE_FOR_CLONE_DIFF (4096 * 4) static NSString *cloneableRelativePath(NSDictionary *afterFileKeyToHashDictionary, NSDictionary *> *beforeHashToFileKeyDictionary, NSDictionary *frameworkVersionsSubstitutes, NSDictionary *fileSubstitutes, NSDictionary *originalTreeState, NSDictionary *newInfo, NSString *key, NSNumber * __autoreleasing *outNewPermissions, BOOL *clonePermissionsChanged, BOOL *clonedBinaryDiff) { // Avoid clones for small files. Small files can compress very well, sometimes better than tracking clones. if ([(NSNumber *)newInfo[INFO_SIZE_KEY] unsignedLongLongValue] <= MIN_SIZE_FOR_CLONE) { return nil; } if ([(NSNumber *)newInfo[INFO_TYPE_KEY] unsignedShortValue] != FTS_F) { return nil; } { NSData *keyHash = afterFileKeyToHashDictionary[key]; if (keyHash != nil) { // Check for identical clones first for (NSString *oldRelativePath in beforeHashToFileKeyDictionary[keyHash]) { NSDictionary *oldCloneInfo = originalTreeState[oldRelativePath]; if (oldCloneInfo == nil) { continue; } if ([(NSNumber *)oldCloneInfo[INFO_TYPE_KEY] unsignedShortValue] != FTS_F) { continue; } NSString *clonePath = oldCloneInfo[INFO_PATH_KEY]; NSString *newPath = newInfo[INFO_PATH_KEY]; if (![[NSFileManager defaultManager] contentsEqualAtPath:clonePath andPath:newPath]) { continue; } NSNumber *newPermissions = newInfo[INFO_PERMISSIONS_KEY]; if (outNewPermissions != NULL) { *outNewPermissions = newPermissions; } if (clonePermissionsChanged != NULL) { *clonePermissionsChanged = ([(NSNumber *)oldCloneInfo[INFO_PERMISSIONS_KEY] unsignedShortValue] != [newPermissions unsignedShortValue]); } if (clonedBinaryDiff != NULL) { *clonedBinaryDiff = NO; } return oldRelativePath; } } } // For non-identical files where we do a binary diff, make sure file size matches a more strict file size test if ([(NSNumber *)newInfo[INFO_SIZE_KEY] unsignedLongLongValue] <= MIN_SIZE_FOR_CLONE_DIFF) { return nil; } // Look out for any .framework/Versions/{A -> B} changes for (NSString *frameworkVersionPrefix in frameworkVersionsSubstitutes) { if (![key hasPrefix:frameworkVersionPrefix]) { continue; } NSString *cloneFrameworkSubstitutePrefix = frameworkVersionsSubstitutes[frameworkVersionPrefix]; if (cloneFrameworkSubstitutePrefix == nil) { continue; } NSString *cloneRelativeKey = [key stringByReplacingCharactersInRange:NSMakeRange(0, frameworkVersionPrefix.length) withString:cloneFrameworkSubstitutePrefix]; NSDictionary *oldCloneInfo = originalTreeState[cloneRelativeKey]; if (oldCloneInfo == nil) { continue; } // The old file must have the same type as the new one if ([(NSNumber *)oldCloneInfo[INFO_TYPE_KEY] unsignedShortValue] != FTS_F) { continue; } NSNumber *newPermissions = newInfo[INFO_PERMISSIONS_KEY]; if (outNewPermissions != NULL) { *outNewPermissions = newPermissions; } if (clonePermissionsChanged != NULL) { *clonePermissionsChanged = ([(NSNumber *)oldCloneInfo[INFO_PERMISSIONS_KEY] unsignedShortValue] != [newPermissions unsignedShortValue]); } if (clonedBinaryDiff != NULL) { *clonedBinaryDiff = YES; } return cloneRelativeKey; } // Look out for any changes that involve the same named file moving to another directory do { NSString *cloneRelativeKey = fileSubstitutes[key.lastPathComponent]; if (cloneRelativeKey == nil) { break; } NSDictionary *oldCloneInfo = originalTreeState[cloneRelativeKey]; if (oldCloneInfo == nil) { break; } uint64_t cloneSize = [(NSNumber *)oldCloneInfo[INFO_SIZE_KEY] unsignedLongValue]; uint64_t newSize = [(NSNumber *)newInfo[INFO_SIZE_KEY] unsignedLongValue]; uint64_t minSize = MIN(cloneSize, newSize); uint64_t maxSize = MAX(cloneSize, newSize); // Ensure file is at least 60% the same size if (minSize == 0 || maxSize == 0 || (double)minSize / (double)maxSize < 0.60) { break; } NSNumber *newPermissions = newInfo[INFO_PERMISSIONS_KEY]; if (outNewPermissions != NULL) { *outNewPermissions = newPermissions; } if (clonePermissionsChanged != NULL) { *clonePermissionsChanged = ([(NSNumber *)oldCloneInfo[INFO_PERMISSIONS_KEY] unsignedShortValue] != [newPermissions unsignedShortValue]); } if (clonedBinaryDiff != NULL) { *clonedBinaryDiff = YES; } return cloneRelativeKey; } while (NO); return nil; } BOOL createBinaryDelta(NSString *source, NSString *destination, NSString *patchFile, SUBinaryDeltaMajorVersion majorVersion, SPUDeltaCompressionMode compression, uint8_t compressionLevel, BOOL verbose, NSError *__autoreleasing *error) { assert(source); assert(destination); assert(patchFile); assert(majorVersion >= SUBinaryDeltaMajorVersionFirst && majorVersion <= SUBinaryDeltaMajorVersionLatest); uint16_t minorVersion = latestMinorVersionForMajorVersion(majorVersion); NSMutableDictionary *originalTreeState = [NSMutableDictionary dictionary]; char pathBuffer[PATH_MAX] = { 0 }; if (![source getFileSystemRepresentation:pathBuffer maxLength:sizeof(pathBuffer)]) { if (error != NULL) { *error = [NSError errorWithDomain:NSCocoaErrorDomain code:NSFileReadInvalidFileNameError userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Failed to retrieve file system path representation from source %@", source] }]; } return NO; } char *sourcePaths[] = { pathBuffer, 0 }; FTS *fts = fts_open(sourcePaths, FTS_PHYSICAL | FTS_NOCHDIR, compareFiles); if (!fts) { if (error != NULL) { *error = [NSError errorWithDomain:NSPOSIXErrorDomain code:errno userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"fts_open failed on source: %@", @(strerror(errno))] }]; } return NO; } if (verbose) { fprintf(stderr, "Creating version %u.%u patch using %s compression...\n", majorVersion, minorVersion, deltaCompressionStringFromMode(compression).UTF8String); fprintf(stderr, "Processing source, %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; } if ([key isEqualToString:CUSTOM_ICON_PATH]) { if (verbose) { fprintf(stderr, "\n"); } if (error != NULL) { *error = [NSError errorWithDomain:NSCocoaErrorDomain code:NSFileReadUnknownError userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Diffing bundles with a custom icon set via a resource fork is not supported. Detected presence of %@", @(ent->fts_path)] }]; } return NO; } NSDictionary *info = infoForFile(ent); if (!info) { if (verbose) { fprintf(stderr, "\n"); } if (error != NULL) { *error = [NSError errorWithDomain:NSCocoaErrorDomain code:NSFileReadUnknownError userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Failed to retrieve info for file %@", @(ent->fts_path)] }]; } return NO; } originalTreeState[key] = info; // Ensure Sparkle executable permissions are valid if (ent->fts_info == FTS_F && [key.lastPathComponent isEqualToString:@"Sparkle"] && [key.stringByDeletingLastPathComponent.stringByDeletingLastPathComponent.stringByDeletingLastPathComponent.lastPathComponent isEqualToString:@"Sparkle.framework"]) { mode_t permissions = (mode_t)[(NSNumber *)info[INFO_PERMISSIONS_KEY] shortValue]; if (permissions != VALID_SPARKLE_EXECUTABLE_PERMISSIONS) { if (verbose) { fprintf(stderr, "\n"); } if (error != NULL) { *error = [NSError errorWithDomain:NSCocoaErrorDomain code:NSFileReadUnknownError userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Permissions for Sparkle executable must be 0%o (found 0%o) on file %@", (unsigned int)VALID_SPARKLE_EXECUTABLE_PERMISSIONS, permissions, @(ent->fts_path)] }]; } return NO; } } if (aclExists(ent)) { if (verbose) { fprintf(stderr, "\n"); } if (error != NULL) { *error = [NSError errorWithDomain:NSCocoaErrorDomain code:NSFileReadUnknownError userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Diffing ACLs are not supported. Detected ACL in before-tree on file %@", @(ent->fts_path)] }]; } return NO; } if (codeSignatureExtendedAttributeExists(ent)) { if (verbose) { fprintf(stderr, "\n"); } if (error != NULL) { *error = [NSError errorWithDomain:NSCocoaErrorDomain code:NSFileReadUnknownError userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Diffing code signed extended attributes are not supported. Detected extended attribute in before-tree on file %@. For removing code signed extended attributes and improving your bundle's structure, please see https://developer.apple.com/documentation/bundleresources/placing_content_in_a_bundle", @(ent->fts_path)] }]; } return NO; } } fts_close(fts); // This dictionary will help us keep track of clones NSMutableDictionary *> *beforeHashToFileKeyDictionary = (majorVersion >= SUBinaryDeltaMajorVersion3) ? [NSMutableDictionary dictionary] : nil; unsigned char beforeHash[BINARY_DELTA_HASH_LENGTH] = {0}; if (!getRawHashOfTreeAndFileTablesWithVersion(beforeHash, source, majorVersion, beforeHashToFileKeyDictionary, nil)) { if (verbose) { fprintf(stderr, "\n"); } if (error != NULL) { *error = [NSError errorWithDomain:NSCocoaErrorDomain code:NSFileReadUnknownError userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Failed to generate hash for tree %@", source] }]; } return NO; } NSMutableDictionary *newTreeState = [NSMutableDictionary dictionary]; for (NSString *key in originalTreeState) { newTreeState[key] = [NSNull null]; } if (verbose) { fprintf(stderr, "\nProcessing destination, %s...", [destination fileSystemRepresentation]); } pathBuffer[0] = 0; if (![destination getFileSystemRepresentation:pathBuffer maxLength:sizeof(pathBuffer)]) { if (verbose) { fprintf(stderr, "\n"); } if (error != NULL) { *error = [NSError errorWithDomain:NSCocoaErrorDomain code:NSFileReadInvalidFileNameError userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Failed to retrieve file system path representation from destination %@", destination] }]; } return NO; } sourcePaths[0] = pathBuffer; fts = fts_open(sourcePaths, FTS_PHYSICAL | FTS_NOCHDIR, compareFiles); if (!fts) { if (error != NULL) { *error = [NSError errorWithDomain:NSPOSIXErrorDomain code:errno userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"fts_open failed on destination: %@", @(strerror(errno))] }]; } return NO; } bool foundFilesystemCompression = false; uint32_t warningsCount = 0; const uint32_t maxWarningsToPrint = 16; 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; } if ([key isEqualToString:CUSTOM_ICON_PATH]) { if (verbose) { fprintf(stderr, "\n"); } if (error != NULL) { *error = [NSError errorWithDomain:NSCocoaErrorDomain code:NSFileReadUnknownError userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Diffing bundles with a custom icon set via a resource fork is not supported. Detected presence of %@", @(ent->fts_path)] }]; } return NO; } NSDictionary *info = infoForFile(ent); if (!info) { if (verbose) { fprintf(stderr, "\n"); } if (error != NULL) { *error = [NSError errorWithDomain:NSCocoaErrorDomain code:NSFileReadUnknownError userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Failed to retrieve info from file %@", @(ent->fts_path)] }]; } return NO; } // Ensure Sparkle executable permissions are valid if (ent->fts_info == FTS_F && [key.lastPathComponent isEqualToString:@"Sparkle"] && [key.stringByDeletingLastPathComponent.stringByDeletingLastPathComponent.stringByDeletingLastPathComponent.lastPathComponent isEqualToString:@"Sparkle.framework"]) { mode_t permissions = (mode_t)[(NSNumber *)info[INFO_PERMISSIONS_KEY] shortValue]; if (permissions != VALID_SPARKLE_EXECUTABLE_PERMISSIONS) { if (verbose) { fprintf(stderr, "\n"); } if (error != NULL) { *error = [NSError errorWithDomain:NSCocoaErrorDomain code:NSFileReadUnknownError userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Permissions for Sparkle executable must be 0%o (found 0%o) on file %@", (unsigned int)VALID_SPARKLE_EXECUTABLE_PERMISSIONS, permissions, @(ent->fts_path)] }]; } return NO; } } // We should validate ACLs even if we don't store the info in the diff in the case of ACLs // We should also not allow files with code signed extended attributes since Apple doesn't recommend inserting these // inside an application, and since we don't preserve extended attribitutes anyway if (aclExists(ent)) { if (verbose) { fprintf(stderr, "\n"); } if (error != NULL) { *error = [NSError errorWithDomain:NSCocoaErrorDomain code:NSFileReadUnknownError userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Diffing ACLs are not supported. Detected ACL in after-tree on file %@", @(ent->fts_path)] }]; } return NO; } if (codeSignatureExtendedAttributeExists(ent)) { if (verbose) { fprintf(stderr, "\n"); } if (error != NULL) { *error = [NSError errorWithDomain:NSCocoaErrorDomain code:NSFileReadUnknownError userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Diffing code signed extended attributes are not supported. Detected extended attribute in after-tree on file %@. For removing code signed extended attributes and improving your bundle's structure, please see https://developer.apple.com/documentation/bundleresources/placing_content_in_a_bundle", @(ent->fts_path)] }]; } return NO; } if (warningsCount < maxWarningsToPrint) { uint16_t permissions = (ent->fts_statp->st_mode & PERMISSION_FLAGS); if (ent->fts_info == FTS_SL) { if (permissions != VALID_SYMBOLIC_LINK_PERMISSIONS) { fprintf(stderr, "\nWarning: file permissions 0%o of symbolic link '%s' won't be preserved in the delta update (only permissions with mode 0755 are supported for symbolic links).", permissions, ent->fts_path); warningsCount++; } } else if (permissions != 0755 && permissions != 0644) { // This could indicate something is wrong inside of the bundle so it's worth warning the user about fprintf(stderr, "\nWarning: detected irregular file permissions 0%o for '%s'", permissions, ent->fts_path); warningsCount++; } if (warningsCount == maxWarningsToPrint) { fprintf(stderr, "\nWarning: encountered too many warnings.. Ignoring the rest.."); } } // If we find any executable files that are using file system compression, that is sufficient // for recording that the applier should re-apply file system compression. // We check for executable files because they are likely candidates to be compressed. if (!foundFilesystemCompression && (majorVersion >= SUBinaryDeltaMajorVersion3) && ent->fts_info == FTS_F && (ent->fts_statp->st_mode & (S_IXUSR | S_IXGRP | S_IXOTH)) != 0 && (ent->fts_statp->st_flags & UF_COMPRESSED) != 0) { foundFilesystemCompression = true; if (verbose) { fprintf(stderr, " File system compression detected."); } } NSDictionary *oldInfo = originalTreeState[key]; BOOL hasEqualInfo; if (![info isEqual:oldInfo]) { hasEqualInfo = NO; } else { NSString *originalPath = oldInfo[INFO_PATH_KEY]; NSString *newPath = info[INFO_PATH_KEY]; hasEqualInfo = [[NSFileManager defaultManager] contentsEqualAtPath:originalPath andPath:newPath]; } if (hasEqualInfo) { [newTreeState removeObjectForKey:key]; } else { newTreeState[key] = info; if (oldInfo && [(NSNumber *)oldInfo[INFO_TYPE_KEY] unsignedShortValue] == FTS_D && [(NSNumber *)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); // This dictionary will help us keep track of clones NSMutableDictionary *afterFileKeyToHashDictionary = (majorVersion >= SUBinaryDeltaMajorVersion3) ? [NSMutableDictionary dictionary] : nil; unsigned char afterHash[BINARY_DELTA_HASH_LENGTH] = {0}; if (!getRawHashOfTreeAndFileTablesWithVersion(afterHash, destination, majorVersion, nil, afterFileKeyToHashDictionary)) { if (verbose) { fprintf(stderr, "\n"); } if (error != NULL) { *error = [NSError errorWithDomain:NSCocoaErrorDomain code:NSFileReadUnknownError userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Failed to generate hash for tree %@", destination] }]; } return NO; } if (verbose) { fprintf(stderr, "\nGenerating delta..."); } NSString *temporaryFile = temporaryPatchFile(patchFile); if (verbose) { fprintf(stderr, "\nWriting to temporary file %s...", [temporaryFile fileSystemRepresentation]); } id archive; if (majorVersion >= SUBinaryDeltaMajorVersion3) { archive = [[SPUSparkleDeltaArchive alloc] initWithPatchFileForWriting:temporaryFile]; } else { #if SPARKLE_BUILD_LEGACY_DELTA_SUPPORT archive = [[SPUXarDeltaArchive alloc] initWithPatchFileForWriting:temporaryFile]; #else if (verbose) { fprintf(stderr, "\n"); } if (error != NULL) { *error = [NSError errorWithDomain:NSCocoaErrorDomain code:NSFileReadUnknownError userInfo:@{ NSLocalizedDescriptionKey: @"Support for creating legacy delta updates is disabled" }]; } return NO; #endif } // Record creation date of root bundle item NSDate *bundleCreationDate; if (majorVersion >= SUBinaryDeltaMajorVersion4) { NSError *fileAttributesError = nil; NSDictionary *fileAttributes = [[NSFileManager defaultManager] attributesOfItemAtPath:destination error:&fileAttributesError]; if (fileAttributes != nil) { bundleCreationDate = fileAttributes[NSFileCreationDate]; } else { bundleCreationDate = nil; fprintf(stderr, "\nWarning: unable to retrieve file creation date of new bundle: %s", fileAttributesError.localizedDescription.UTF8String); } } else { bundleCreationDate = nil; } SPUDeltaArchiveHeader *header = [[SPUDeltaArchiveHeader alloc] initWithCompression:compression compressionLevel:compressionLevel fileSystemCompression:foundFilesystemCompression majorVersion:majorVersion minorVersion:minorVersion beforeTreeHash:beforeHash afterTreeHash:afterHash bundleCreationDate:bundleCreationDate]; [archive writeHeader:header]; if (archive.error != nil) { if (verbose) { fprintf(stderr, "\n"); } if (error != NULL) { *error = archive.error; } return NO; } 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; }]; // Using a couple of heuristics we track if files have been moved to other locations within the app bundle NSMutableDictionary *frameworkVersionsSubstitutes = [NSMutableDictionary dictionary]; NSMutableDictionary *fileSubstitutes = [NSMutableDictionary dictionary]; if (majorVersion >= SUBinaryDeltaMajorVersion3) { // Heuristic #1: track if an old framework version was removed and a new framework version was added // Keep track of these prefixes in a dictionary // Eg: /Contents/Frameworks/Foo.framework/Versions/B/ (new) -> /Contents/Frameworks/Foo.framework/Versions/A/ (old) NSMutableDictionary *oldFrameworkVersions = [NSMutableDictionary dictionary]; for (NSString *key in keys) { id value = [newTreeState valueForKey:key]; if (![(NSObject *)value isEqual:[NSNull null]]) { continue; } NSDictionary *originalInfo = originalTreeState[key]; if ([(NSNumber *)originalInfo[INFO_TYPE_KEY] unsignedShortValue] != FTS_D) { continue; } NSString *keyWithoutLastPathComponent = key.stringByDeletingLastPathComponent; if (![keyWithoutLastPathComponent.lastPathComponent isEqualToString:@"Versions"]) { continue; } NSString *keyWithoutLastLastPathComponent = keyWithoutLastPathComponent.stringByDeletingLastPathComponent; if (![keyWithoutLastLastPathComponent.pathExtension isEqualToString:@"framework"]) { continue; } oldFrameworkVersions[keyWithoutLastLastPathComponent] = key; } for (NSString *key in keys) { id value = [newTreeState valueForKey:key]; if ([(NSObject *)value isEqual:[NSNull null]] || originalTreeState[key] != nil) { continue; } NSDictionary *newInfo = value; if ([(NSNumber *)newInfo[INFO_TYPE_KEY] unsignedShortValue] != FTS_D) { continue; } NSString *keyWithoutLastPathComponent = key.stringByDeletingLastPathComponent; if (![keyWithoutLastPathComponent.lastPathComponent isEqualToString:@"Versions"]) { continue; } NSString *keyWithoutLastLastPathComponent = keyWithoutLastPathComponent.stringByDeletingLastPathComponent; if (![keyWithoutLastLastPathComponent.pathExtension isEqualToString:@"framework"]) { continue; } NSString *oldFrameworkVersionKey = oldFrameworkVersions[keyWithoutLastLastPathComponent]; if (oldFrameworkVersionKey == nil) { continue; } frameworkVersionsSubstitutes[[key stringByAppendingString:@"/"]] = [oldFrameworkVersionKey stringByAppendingString:@"/"]; } // Heuristic #2: Keep a table of removed filenames, collapsing them by the largest file per unique name // This sees if a file has just been moved to another location for (NSString *key in keys) { id value = [newTreeState valueForKey:key]; if (![(NSObject *)value isEqual:[NSNull null]]) { continue; } NSDictionary *keyInfo = originalTreeState[key]; if ([(NSNumber *)keyInfo[INFO_TYPE_KEY] unsignedShortValue] != FTS_F) { continue; } NSString *lastPathComponent = key.lastPathComponent; NSString *existingKey = fileSubstitutes[lastPathComponent]; if (existingKey == nil) { fileSubstitutes[lastPathComponent] = key; } else { NSDictionary *existingKeyInfo = originalTreeState[existingKey]; if ([(NSNumber *)keyInfo[INFO_SIZE_KEY] unsignedLongValue] > [(NSNumber *)existingKeyInfo[INFO_SIZE_KEY] unsignedLongValue]) { fileSubstitutes[lastPathComponent] = key; } } } } for (NSString *key in keys) { id value = [newTreeState valueForKey:key]; if ([(NSObject *)value isEqual:[NSNull null]]) { [archive addItem:[[SPUDeltaArchiveItem alloc] initWithRelativeFilePath:key commands:SPUDeltaItemCommandDelete mode:0]]; 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 (shouldSkipExtracting(originalInfo, newInfo)) { if (shouldChangePermissions(originalInfo, newInfo)) { [archive addItem:[[SPUDeltaArchiveItem alloc] initWithRelativeFilePath:key commands:SPUDeltaItemCommandModifyPermissions mode:[(NSNumber *)newInfo[INFO_PERMISSIONS_KEY] unsignedShortValue]]]; if (verbose) { fprintf(stderr, "\n👮 %s %s (0%o -> 0%o)", VERBOSE_MODIFIED, [key fileSystemRepresentation], [(NSNumber *)originalInfo[INFO_PERMISSIONS_KEY] unsignedShortValue], [(NSNumber *)newInfo[INFO_PERMISSIONS_KEY] unsignedShortValue]); } } } else { // Check if the new file can be cloned from an old existing one located at a different path NSNumber *newPermissions = nil; BOOL clonePermissionsChanged = NO; BOOL clonedBinaryDiff = NO; NSString *clonedRelativePath = (majorVersion >= SUBinaryDeltaMajorVersion3) ? cloneableRelativePath(afterFileKeyToHashDictionary, beforeHashToFileKeyDictionary, frameworkVersionsSubstitutes, fileSubstitutes, originalTreeState, newInfo, key, &newPermissions, &clonePermissionsChanged, &clonedBinaryDiff) : nil; if (clonedRelativePath != nil) { if (clonedBinaryDiff) { NSDictionary *cloneInfo = originalTreeState[clonedRelativePath]; CreateBinaryDeltaOperation *operation = [[CreateBinaryDeltaOperation alloc] initWithRelativePath:key clonedRelativePath:clonedRelativePath oldTree:source newTree:destination oldPermissions:cloneInfo[INFO_PERMISSIONS_KEY] newPermissions:newPermissions changingPermissions:clonePermissionsChanged]; [deltaQueue addOperation:operation]; [deltaOperations addObject:operation]; } else { SPUDeltaItemCommands commands = SPUDeltaItemCommandClone; if (clonePermissionsChanged) { commands |= SPUDeltaItemCommandModifyPermissions; } SPUDeltaArchiveItem *item = [[SPUDeltaArchiveItem alloc] initWithRelativeFilePath:key commands:commands mode:(clonePermissionsChanged ? newPermissions.unsignedShortValue : 0)]; // Physical path for clones points to the old file item.itemFilePath = [source stringByAppendingPathComponent:clonedRelativePath]; item.sourcePath = item.itemFilePath; item.clonedRelativePath = clonedRelativePath; [archive addItem:item]; if (verbose) { fprintf(stderr, "\n✂️ %s %s -> %s", VERBOSE_CLONED, [clonedRelativePath fileSystemRepresentation], [key fileSystemRepresentation]); } } } else { // Otherwise add a new file NSString *path = [destination stringByAppendingPathComponent:key]; SPUDeltaItemCommands commands = SPUDeltaItemCommandExtract; if (shouldDeleteThenExtract(originalInfo, newInfo)) { commands |= SPUDeltaItemCommandDelete; } SPUDeltaArchiveItem *item = [[SPUDeltaArchiveItem alloc] initWithRelativeFilePath:key commands:commands mode:0]; item.itemFilePath = path; item.sourcePath = path; [archive addItem:item]; 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 = newInfo[INFO_PERMISSIONS_KEY]; CreateBinaryDeltaOperation *operation = [[CreateBinaryDeltaOperation alloc] initWithRelativePath:key clonedRelativePath:nil oldTree:source newTree:destination oldPermissions:originalInfo[INFO_PERMISSIONS_KEY] newPermissions:permissions changingPermissions:shouldChangePermissions(originalInfo, newInfo)]; [deltaQueue addOperation:operation]; [deltaOperations addObject:operation]; } } [deltaQueue waitUntilAllOperationsAreFinished]; BOOL deltaOperationsFailed = NO; for (CreateBinaryDeltaOperation *operation in deltaOperations) { NSString *resultPath = operation.resultPath; if (resultPath == nil) { if (verbose) { fprintf(stderr, "\n"); } if (error != NULL) { *error = [NSError errorWithDomain:NSCocoaErrorDomain code:NSFileWriteUnknownError userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Failed to create patch from source %@ and destination %@", operation.relativePath, resultPath] }]; } deltaOperationsFailed = YES; break; } NSString *clonedRelativePath = [operation clonedRelativePath]; if (verbose) { if (clonedRelativePath == nil) { fprintf(stderr, "\n🔨 %s %s", VERBOSE_DIFFED, [[operation relativePath] fileSystemRepresentation]); } else { fprintf(stderr, "\n🔨 %s %s -> %s", VERBOSE_DIFFED, [clonedRelativePath fileSystemRepresentation], [[operation relativePath] fileSystemRepresentation]); } } NSNumber *mode = operation.permissions; NSString *relativePath = operation.relativePath; SPUDeltaItemCommands commands = SPUDeltaItemCommandBinaryDiff; if (operation.changingPermissions) { commands |= SPUDeltaItemCommandModifyPermissions; } if (clonedRelativePath != nil) { commands |= SPUDeltaItemCommandClone; } SPUDeltaArchiveItem *item = [[SPUDeltaArchiveItem alloc] initWithRelativeFilePath:relativePath commands:commands mode:mode.unsignedShortValue]; item.itemFilePath = resultPath; item.sourcePath = operation.fromPath; item.clonedRelativePath = clonedRelativePath; [archive addItem:item]; if (operation.changingPermissions) { if (verbose) { fprintf(stderr, "\n👮 %s %s (0%o -> 0%o)", VERBOSE_MODIFIED, relativePath.fileSystemRepresentation, operation.oldPermissions.unsignedShortValue, operation.permissions.unsignedShortValue); } } } if (!deltaOperationsFailed) { [archive finishEncodingItems]; } [archive close]; // Clean up operations after the archive has finished encoding for (CreateBinaryDeltaOperation *operation in deltaOperations) { NSString *resultPath = operation.resultPath; if (resultPath != nil) { unlink(resultPath.fileSystemRepresentation); } } if (deltaOperationsFailed) { // We already set an error so let's bail return NO; } NSError *archiveError = archive.error; if (archiveError != nil) { if (verbose) { fprintf(stderr, "\n"); } if (error != NULL) { *error = archiveError; } return NO; } NSFileManager *filemgr; filemgr = [NSFileManager defaultManager]; [filemgr removeItemAtPath: patchFile error: NULL]; if ([filemgr moveItemAtPath: temporaryFile toPath: patchFile error: NULL] != YES) { if (verbose) { fprintf(stderr, "Failed to move temporary file, %s, to %s!\n", [temporaryFile fileSystemRepresentation], [patchFile fileSystemRepresentation]); } return NO; } if (verbose) { fprintf(stderr, "\nDone!\n"); } return YES; } ================================================ FILE: Autoupdate/SUBinaryDeltaUnarchiver.h ================================================ // // SUBinaryDeltaUnarchiver.h // Sparkle // // Created by Mark Rowe on 2009-06-03. // Copyright 2009 Mark Rowe. All rights reserved. // #ifndef SUBINARYDELTAUNARCHIVER_H #define SUBINARYDELTAUNARCHIVER_H #import #import "SUUnarchiverProtocol.h" NS_ASSUME_NONNULL_BEGIN #ifndef BUILDING_SPARKLE_TESTS SPU_OBJC_DIRECT_MEMBERS #endif @interface SUBinaryDeltaUnarchiver : NSObject - (instancetype)initWithArchivePath:(NSString *)archivePath extractionDirectory:(NSString *)extractionDirectory updateHostBundlePath:(NSString *)updateHostBundlePath; + (BOOL)canUnarchivePath:(NSString *)path; @end NS_ASSUME_NONNULL_END #endif ================================================ FILE: Autoupdate/SUBinaryDeltaUnarchiver.m ================================================ // // SUBinaryDeltaUnarchiver.m // Sparkle // // Created by Mark Rowe on 2009-06-03. // Copyright 2009 Mark Rowe. All rights reserved. // #import "SUBinaryDeltaUnarchiver.h" #import "SUUnarchiverNotifier.h" #import "SUBinaryDeltaCommon.h" #import "SUBinaryDeltaApply.h" #import "SULog.h" #import "SUFileManager.h" #include "AppKitPrevention.h" @implementation SUBinaryDeltaUnarchiver { NSString *_archivePath; NSString *_updateHostBundlePath; NSString *_extractionDirectory; } + (BOOL)canUnarchivePath:(NSString *)path { return [[path pathExtension] isEqualToString:@"delta"]; } + (BOOL)mustValidateBeforeExtraction { return YES; } // According to https://developer.apple.com/library/mac/documentation/Carbon/Conceptual/MDImporters/Concepts/Troubleshooting.html // We should make sure mdimporter bundles have an up to date time in the event they were delta updated. // We used to invoke mdimport on the bundle but this is not a very good approach. // There's no need to do that for non-delta updates and for updates that contain no mdimporters. // Moreover, updating the timestamp on the mdimporter bundles is what developers have to do anyway when shipping their new update outside of Sparkle // Note: this is used from unit tests + (void)updateSpotlightImportersAtBundlePath:(NSString *)targetPath #ifndef BUILDING_SPARKLE_TESTS SPU_OBJC_DIRECT #endif { NSURL *targetURL = [NSURL fileURLWithPath:targetPath]; // Only recurse if it's actually a directory. Don't recurse into a // root-level symbolic link. NSDictionary *rootAttributes = [[NSFileManager defaultManager] attributesOfItemAtPath:targetPath error:nil]; NSString *rootType = [rootAttributes objectForKey:NSFileType]; if ([rootType isEqualToString:NSFileTypeDirectory]) { // The NSDirectoryEnumerator will avoid recursing into any contained // symbolic links, so no further type checks are needed. NSDirectoryEnumerator *directoryEnumerator = [[NSFileManager defaultManager] enumeratorAtURL:targetURL includingPropertiesForKeys:nil options:(NSDirectoryEnumerationOptions)0 errorHandler:nil]; NSMutableArray *filesToUpdate = [[NSMutableArray alloc] init]; for (NSURL *file in directoryEnumerator) { if ([file.pathExtension isEqualToString:@"mdimporter"]) { [filesToUpdate addObject:file]; } } SUFileManager *fileManager = [[SUFileManager alloc] init]; for (NSURL *file in filesToUpdate) { NSError *error = nil; if (![fileManager updateModificationAndAccessTimeOfItemAtURL:file error:&error]) { SULog(SULogLevelError, @"Error: During delta unarchiving, failed to touch %@", error); } } } } - (instancetype)initWithArchivePath:(NSString *)archivePath extractionDirectory:(NSString *)extractionDirectory updateHostBundlePath:(NSString *)updateHostBundlePath { self = [super init]; if (self != nil) { _archivePath = [archivePath copy]; _updateHostBundlePath = [updateHostBundlePath copy]; _extractionDirectory = [extractionDirectory copy]; } return self; } - (BOOL)needsVerifyBeforeExtractionKey { return NO; } - (void)unarchiveWithCompletionBlock:(void (^)(NSError * _Nullable))completionBlock progressBlock:(void (^ _Nullable)(double))progressBlock waitForCleanup:(BOOL)__unused waitForCleanup { dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ @autoreleasepool { SUUnarchiverNotifier *notifier = [[SUUnarchiverNotifier alloc] initWithCompletionBlock:completionBlock progressBlock:progressBlock]; [self extractDeltaWithNotifier:notifier]; } }); } - (void)extractDeltaWithNotifier:(SUUnarchiverNotifier *)notifier { NSString *sourcePath = _updateHostBundlePath; NSString *targetPath = [_extractionDirectory stringByAppendingPathComponent:[sourcePath lastPathComponent]]; NSError *applyDiffError = nil; BOOL success = applyBinaryDelta(sourcePath, targetPath, _archivePath, NO, ^(double progress){ [notifier notifyProgress:progress]; }, &applyDiffError); if (success) { [SUBinaryDeltaUnarchiver updateSpotlightImportersAtBundlePath:targetPath]; [notifier notifySuccess]; } else { [notifier notifyFailureWithError:applyDiffError]; } } - (NSString *)description { return [NSString stringWithFormat:@"%@ <%@>", [self class], _archivePath]; } @end ================================================ FILE: Autoupdate/SUCodeSigningVerifier.h ================================================ // // SUCodeSigningVerifier.h // Sparkle // // Created by Andy Matuschak on 7/5/12. // // #ifndef SUCODESIGNINGVERIFIER_H #define SUCODESIGNINGVERIFIER_H #import NS_ASSUME_NONNULL_BEGIN #ifndef BUILDING_SPARKLE_TESTS #define SUCodeSigningVerifierDefinitionAttribute SPU_OBJC_DIRECT_MEMBERS #else #define SUCodeSigningVerifierDefinitionAttribute __attribute__((objc_runtime_name("SUTestCodeSigningVerifier"))) #endif typedef NS_ENUM(NSUInteger, SUValidateConnectionStatus) { SUValidateConnectionStatusSetCodeSigningRequirementSuccess = 0, SUValidateConnectionStatusSetNoRequirementSuccess, SUValidateConnectionStatusAPIFailure, SUValidateConnectionStatusCodeSigningRequirementFailure, SUValidateConectionNoSupportedValidationMethodFailure, }; SUCodeSigningVerifierDefinitionAttribute @interface SUCodeSigningVerifier : NSObject + (BOOL)codeSignatureIsValidAtBundleURL:(NSURL *)newBundleURL andMatchesSignatureAtBundleURL:(NSURL *)oldBundleURL error:(NSError **)error; + (BOOL)codeSignatureIsValidAtBundleURL:(NSURL *)bundleURL checkNestedCode:(BOOL)checkNestedCode error:(NSError **)error; // Same as above except does not check for nested code. This method should be used by the framework. + (BOOL)codeSignatureIsValidAtBundleURL:(NSURL *)bundleURL error:(NSError *__autoreleasing *)error; + (BOOL)codeSignatureIsValidAtDownloadURL:(NSURL *)downloadURL andMatchesDeveloperIDTeamFromOldBundleURL:(NSURL *)oldBundleURL error:(NSError * __autoreleasing *)error; + (BOOL)bundleAtURLIsCodeSigned:(NSURL *)bundleURL; + (NSString * _Nullable)teamIdentifierAtURL:(NSURL *)url; + (NSString * _Nullable)teamIdentifierFromMainExecutable; + (SUValidateConnectionStatus)validateConnection:(NSXPCConnection *)connection error:(NSError * __autoreleasing *)error; @end NS_ASSUME_NONNULL_END #endif ================================================ FILE: Autoupdate/SUCodeSigningVerifier.m ================================================ // // SUCodeSigningVerifier.m // Sparkle // // Created by Andy Matuschak on 7/5/12. // // #include #include #import "SUCodeSigningVerifier.h" #import "SULog.h" #import "SUErrors.h" #include "AppKitPrevention.h" @interface NSXPCConnection (Private) @property (nonatomic, readonly) audit_token_t auditToken; @end @implementation SUCodeSigningVerifier + (BOOL)codeSignatureIsValidAtBundleURL:(NSURL *)newBundleURL andMatchesSignatureAtBundleURL:(NSURL *)oldBundleURL error:(NSError * __autoreleasing *)error { OSStatus result; SecRequirementRef requirement = NULL; SecStaticCodeRef staticCode = NULL; SecStaticCodeRef oldCode = NULL; CFErrorRef cfError = NULL; result = SecStaticCodeCreateWithPath((__bridge CFURLRef)oldBundleURL, kSecCSDefaultFlags, &oldCode); if (result != noErr) { if (error != NULL) { NSString *errorMessage = (result == errSecCSUnsigned) ? [NSString stringWithFormat:@"Bundle is not code signed: %@", oldBundleURL.path] : [NSString stringWithFormat:@"Failed to get static code (%d): %@", result, oldBundleURL.path]; *error = [NSError errorWithDomain:SUSparkleErrorDomain code:SUInsufficientSigningError userInfo:@{ NSLocalizedDescriptionKey: errorMessage }]; } goto finally; } result = SecCodeCopyDesignatedRequirement(oldCode, kSecCSDefaultFlags, &requirement); if (result != noErr) { NSString *message = [NSString stringWithFormat:@"Failed to copy designated requirement. Code Signing OSStatus code: %d", result]; SULog(SULogLevelError, @"%@", message); if (error != NULL) { *error = [NSError errorWithDomain:SUSparkleErrorDomain code:SUInsufficientSigningError userInfo:@{ NSLocalizedDescriptionKey: message }]; } goto finally; } result = SecStaticCodeCreateWithPath((__bridge CFURLRef)newBundleURL, kSecCSDefaultFlags, &staticCode); if (result != noErr) { NSString *message = [NSString stringWithFormat:@"Failed to get static code %d", result]; SULog(SULogLevelError, @"%@", message); if (error != NULL) { *error = [NSError errorWithDomain:SUSparkleErrorDomain code:SUInsufficientSigningError userInfo:@{ NSLocalizedDescriptionKey: message }]; } 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 // Additionally, there are several reasons to stay away from deep verification and to prefer EdDSA signing the download archive instead. // See https://github.com/sparkle-project/Sparkle/pull/523#commitcomment-17549302 and https://github.com/sparkle-project/Sparkle/issues/543 result = SecStaticCodeCheckValidityWithErrors(staticCode, kSecCSCheckAllArchitectures, requirement, &cfError); if (result != errSecSuccess) { NSError *underlyingError; if (cfError != NULL) { NSError *tmpError = CFBridgingRelease(cfError); underlyingError = tmpError; } else { underlyingError = nil; } NSMutableDictionary *userInfo = [NSMutableDictionary dictionary]; if (underlyingError != nil) { userInfo[NSUnderlyingErrorKey] = underlyingError; } if (result == errSecCSUnsigned) { NSString *message = @"The host app is signed, but the new version of the app is not signed using Apple Code Signing. Please ensure that the new app is signed and that archiving did not corrupt the signature."; SULog(SULogLevelError, @"%@", message); if (error != NULL) { userInfo[NSLocalizedDescriptionKey] = message; *error = [NSError errorWithDomain:SUSparkleErrorDomain code:SUInsufficientSigningError userInfo:[userInfo copy]]; } } else if (result == errSecCSReqFailed) { CFStringRef requirementString = nil; NSString *initialMessage; if (SecRequirementCopyString(requirement, kSecCSDefaultFlags, &requirementString) == noErr) { initialMessage = [NSString stringWithFormat:@"Code signature of the new version doesn't match the old version: %@. Please ensure that old and new app is signed using exactly the same certificate.", requirementString]; SULog(SULogLevelError, @"%@", initialMessage); CFRelease(requirementString); } else { initialMessage = @"Code signature of new version doesn't match the old version. Please ensure that old and new app is signed using exactly the same certificate."; } NSDictionary *oldInfo = [self logSigningInfoForCode:oldCode label:@"old info"]; NSDictionary *newInfo = [self logSigningInfoForCode:staticCode label:@"new info"]; if (error != NULL) { userInfo[NSLocalizedDescriptionKey] = [NSString stringWithFormat:@"%@ old info: %@. new info: %@", initialMessage, oldInfo, newInfo]; *error = [NSError errorWithDomain:SUSparkleErrorDomain code:SUInsufficientSigningError userInfo:[userInfo copy]]; } } else { if (error != NULL) { userInfo[NSLocalizedDescriptionKey] = @"Error: Old app bundle code signing signature failed to match new bundle code signature"; *error = [NSError errorWithDomain:SUSparkleErrorDomain code:SUInsufficientSigningError userInfo:[userInfo copy]]; } } } finally: if (oldCode) CFRelease(oldCode); if (staticCode) CFRelease(staticCode); if (requirement) CFRelease(requirement); return (result == noErr); } + (BOOL)codeSignatureIsValidAtBundleURL:(NSURL *)bundleURL error:(NSError *__autoreleasing *)error { return [self codeSignatureIsValidAtBundleURL:bundleURL checkNestedCode:NO error:error]; } + (BOOL)codeSignatureIsValidAtBundleURL:(NSURL *)bundleURL checkNestedCode:(BOOL)checkNestedCode error:(NSError *__autoreleasing *)error { OSStatus result; SecStaticCodeRef staticCode = NULL; CFErrorRef cfError = NULL; // See also code further below where kSecCSCheckNestedCode may be added SecCSFlags flags = kSecCSCheckAllArchitectures; result = SecStaticCodeCreateWithPath((__bridge CFURLRef)bundleURL, kSecCSDefaultFlags, &staticCode); if (result != noErr) { SULog(SULogLevelError, @"Failed to get static code %d", result); if (error != NULL) { *error = [NSError errorWithDomain:SUSparkleErrorDomain code:SUInsufficientSigningError userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Failed to get static code for verifying code signature: %d", result] }]; } goto finally; } // See in -codeSignatureIsValidAtBundleURL:andMatchesSignatureAtBundleURL:error: for why kSecCSCheckNestedCode is not always passed if (checkNestedCode) { flags |= kSecCSCheckNestedCode; } result = SecStaticCodeCheckValidityWithErrors(staticCode, flags, NULL, &cfError); if (result != errSecSuccess) { NSError *underlyingError; if (cfError != NULL) { NSError *tmpError = CFBridgingRelease(cfError); underlyingError = tmpError; } else { underlyingError = nil; } NSMutableDictionary *userInfo = [NSMutableDictionary dictionary]; if (underlyingError != nil) { userInfo[NSUnderlyingErrorKey] = underlyingError; } if (result == errSecCSUnsigned) { NSString *message = [NSString stringWithFormat:@"Error: The app is not signed using Apple Code Signing. %@", bundleURL]; SULog(SULogLevelError, @"%@", message); if (error != NULL) { userInfo[NSLocalizedDescriptionKey] = message; *error = [NSError errorWithDomain:SUSparkleErrorDomain code:SUInsufficientSigningError userInfo:[userInfo copy]]; } } else if (result == errSecCSReqFailed) { if (error != NULL) { NSDictionary *newInfo = [self logSigningInfoForCode:staticCode label:@"new info"]; NSString *message = [NSString stringWithFormat:@"Error: The app failed Apple Code Signing checks: %@ - new info: %@", bundleURL, newInfo]; userInfo[NSLocalizedDescriptionKey] = message; *error = [NSError errorWithDomain:SUSparkleErrorDomain code:SUInsufficientSigningError userInfo:[userInfo copy]]; } } else { if (error != NULL) { NSString *message = [NSString stringWithFormat:@"Error: The app failed Apple Code Signing checks: %@", bundleURL]; userInfo[NSLocalizedDescriptionKey] = message; *error = [NSError errorWithDomain:SUSparkleErrorDomain code:SUInsufficientSigningError userInfo:[userInfo copy]]; } } } finally: if (staticCode) CFRelease(staticCode); return (result == noErr); } static id valueOrNSNull(id value) { return value ? value : [NSNull null]; } + (NSDictionary *)codeSignatureInfoForCode:(SecStaticCodeRef)code SPU_OBJC_DIRECT { CFDictionaryRef signingInfo = nil; const SecCSFlags flags = (SecCSFlags) (kSecCSSigningInformation | kSecCSRequirementInformation | kSecCSDynamicInformation | kSecCSContentInformation); if (SecCodeCopySigningInformation(code, flags, &signingInfo) == noErr) { NSDictionary *signingDict = CFBridgingRelease(signingInfo); NSMutableDictionary *relevantInfo = [NSMutableDictionary dictionary]; for (NSString *key in @[@"format", @"identifier", @"requirements", @"teamid", @"signing-time"]) { [relevantInfo setObject:valueOrNSNull([signingDict objectForKey:key]) forKey:key]; } NSDictionary *infoPlist = [signingDict objectForKey:@"info-plist"]; [relevantInfo setObject:valueOrNSNull([infoPlist objectForKey:@"CFBundleShortVersionString"]) forKey:@"version"]; [relevantInfo setObject:valueOrNSNull([infoPlist objectForKey:(__bridge NSString *)kCFBundleVersionKey]) forKey:@"build"]; return [relevantInfo copy]; } return nil; } + (NSDictionary *)logSigningInfoForCode:(SecStaticCodeRef)code label:(NSString*)label SPU_OBJC_DIRECT { NSDictionary *relevantInfo = [self codeSignatureInfoForCode:code]; SULog(SULogLevelDefault, @"%@: %@", label, relevantInfo); return relevantInfo; } + (BOOL)bundleAtURLIsCodeSigned:(NSURL *)bundleURL { OSStatus result; SecStaticCodeRef staticCode = NULL; result = SecStaticCodeCreateWithPath((__bridge CFURLRef)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); } static NSString * _Nullable SUTeamIdentifierFromCode(SecStaticCodeRef staticCode) { CFDictionaryRef cfSigningInformation = NULL; OSStatus copySigningInfoCode = SecCodeCopySigningInformation(staticCode, kSecCSSigningInformation, &cfSigningInformation); NSDictionary *signingInformation = CFBridgingRelease(cfSigningInformation); if (copySigningInfoCode != noErr) { SULog(SULogLevelError, @"Failed to get signing information for retrieving team identifier: %d", copySigningInfoCode); return nil; } // Note this will return nil for ad-hoc or unsigned binaries return signingInformation[(NSString *)kSecCodeInfoTeamIdentifier]; } + (NSString * _Nullable)teamIdentifierAtURL:(NSURL *)url { SecStaticCodeRef staticCode = NULL; OSStatus staticCodeResult = SecStaticCodeCreateWithPath((__bridge CFURLRef)url, kSecCSDefaultFlags, &staticCode); if (staticCodeResult != errSecSuccess) { SULog(SULogLevelError, @"Failed to get static code for retrieving team identifier: %d", staticCodeResult); return nil; } NSString *teamIdentifier = SUTeamIdentifierFromCode(staticCode); if (staticCode != NULL) { CFRelease(staticCode); } return teamIdentifier; } + (NSString * _Nullable)teamIdentifierFromMainExecutable { SecCodeRef code = NULL; OSStatus result = SecCodeCopySelf(kSecCSDefaultFlags, &code); if (result != errSecSuccess) { SULog(SULogLevelError, @"Failed to get code for retrieving team identifier of main executable: %d", result); return nil; } NSString *teamIdentifier = SUTeamIdentifierFromCode(code); CFRelease(code); return teamIdentifier; } + (BOOL)codeSignatureIsValidAtDownloadURL:(NSURL *)downloadURL andMatchesDeveloperIDTeamFromOldBundleURL:(NSURL *)oldBundleURL error:(NSError * __autoreleasing *)error { NSString *teamIdentifier = nil; NSString *requirementString = nil; SecRequirementRef requirement = NULL; SecStaticCodeRef oldStaticCode = NULL; SecStaticCodeRef downloadStaticCode = NULL; OSStatus result; NSError *resultError = nil; CFErrorRef cfError = NULL; NSString *commonErrorMessage = @"The download archive cannot be validated with Apple Developer ID code signing as fallback (after (Ed)DSA verification has failed)"; result = SecStaticCodeCreateWithPath((__bridge CFURLRef)oldBundleURL, kSecCSDefaultFlags, &oldStaticCode); if (result != errSecSuccess) { NSString *errorMessage = (result == errSecCSUnsigned) ? [NSString stringWithFormat:@"%@. The original app is not code signed: %@", commonErrorMessage, oldBundleURL.path] : [NSString stringWithFormat:@"%@. The static code could not be retrieved from the original app (%d): %@", commonErrorMessage, result, oldBundleURL.path]; resultError = [NSError errorWithDomain:SUSparkleErrorDomain code:SUInsufficientSigningError userInfo:@{ NSLocalizedDescriptionKey: errorMessage }]; goto finally; } teamIdentifier = SUTeamIdentifierFromCode(oldStaticCode); if (teamIdentifier == nil) { resultError = [NSError errorWithDomain:SUSparkleErrorDomain code:SUInsufficientSigningError userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"%@. The team identifier could not be retrieved from the original app: %@", commonErrorMessage, oldBundleURL.path] }]; goto finally; } // Create a designated requirement with developer ID signing with this team ID // Validate it against code signing check of this archive // CertificateIssuedByApple = anchor apple generic // IssuerIsDeveloperID = certificate 1[field.1.2.840.113635.100.6.2.6] // LeafIsDeveloperIDApp = certificate leaf[field.1.2.840.113635.100.6.1.13] // DeveloperIDTeamID = certificate leaf[subject.OU] // https://developer.apple.com/documentation/technotes/tn3127-inside-code-signing-requirements#Xcode-designated-requirement-for-Developer-ID-code requirementString = [NSString stringWithFormat:@"anchor apple generic and certificate 1[field.1.2.840.113635.100.6.2.6] and certificate leaf[field.1.2.840.113635.100.6.1.13] and certificate leaf[subject.OU] = \"%@\"", teamIdentifier]; result = SecRequirementCreateWithString((__bridge CFStringRef)requirementString, kSecCSDefaultFlags, &requirement); if (result != errSecSuccess) { resultError = [NSError errorWithDomain:SUSparkleErrorDomain code:SUInsufficientSigningError userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"%@. The designated requirement string with a Developer ID requirement with team identifier '%@' could not be created with error %d", commonErrorMessage, teamIdentifier, result] }]; goto finally; } result = SecStaticCodeCreateWithPath((__bridge CFURLRef)downloadURL, kSecCSDefaultFlags, &downloadStaticCode); if (result != errSecSuccess) { NSString *message = [NSString stringWithFormat:@"%@. The static code could not be retrieved from the download archive with error %d. The download archive may not be Apple code signed.", commonErrorMessage, result]; SULog(SULogLevelError, @"%@", message); resultError = [NSError errorWithDomain:SUSparkleErrorDomain code:SUInsufficientSigningError userInfo:@{ NSLocalizedDescriptionKey: message }]; goto finally; } result = SecStaticCodeCheckValidityWithErrors(downloadStaticCode, kSecCSDefaultFlags, requirement, &cfError); if (result != errSecSuccess) { NSError *underlyingError; if (cfError != NULL) { NSError *tmpError = CFBridgingRelease(cfError); underlyingError = tmpError; } else { underlyingError = nil; } NSMutableDictionary *userInfo = [NSMutableDictionary dictionary]; if (underlyingError != nil) { userInfo[NSUnderlyingErrorKey] = underlyingError; } if (result == errSecCSUnsigned) { NSString *message = [NSString stringWithFormat:@"%@. The download archive is not Apple code signed.", commonErrorMessage]; SULog(SULogLevelError, @"%@", message); userInfo[NSLocalizedDescriptionKey] = message; resultError = [NSError errorWithDomain:SUSparkleErrorDomain code:SUInsufficientSigningError userInfo:[userInfo copy]]; } else if (result == errSecCSReqFailed) { NSString *initialMessage = [NSString stringWithFormat:@"%@. The Apple code signature of new downloaded archive is either not Developer ID code signed, or doesn't have a Team ID that matches the old app version (%@). Please ensure that the archive and app are signed using the same Developer ID certificate.", commonErrorMessage, teamIdentifier]; NSDictionary *oldInfo = [self logSigningInfoForCode:oldStaticCode label:@"old info"]; NSDictionary *newInfo = [self logSigningInfoForCode:downloadStaticCode label:@"new info"]; userInfo[NSLocalizedDescriptionKey] = [NSString stringWithFormat:@"%@ old info: %@. new info: %@", initialMessage, oldInfo, newInfo]; resultError = [NSError errorWithDomain:SUSparkleErrorDomain code:SUInsufficientSigningError userInfo:[userInfo copy]]; } else { userInfo[NSLocalizedDescriptionKey] = [NSString stringWithFormat:@"%@. The downloaded archive code signing signature failed to validate with an unknown error (%d).", commonErrorMessage, result]; resultError = [NSError errorWithDomain:SUSparkleErrorDomain code:SUInsufficientSigningError userInfo:[userInfo copy]]; } goto finally; } finally: if (oldStaticCode != NULL) { CFRelease(oldStaticCode); } if (requirement != NULL) { CFRelease(requirement); } if (downloadStaticCode != NULL) { CFRelease(downloadStaticCode); } if (resultError != nil && error != NULL) { *error = resultError; } return (resultError == nil); } + (SUValidateConnectionStatus)validateConnection:(NSXPCConnection *)connection error:(NSError * __autoreleasing *)error { // Check if code signing requirement is required NSString *hostTeamIdentifier = [self teamIdentifierFromMainExecutable]; if (hostTeamIdentifier == nil) { return SUValidateConnectionStatusSetNoRequirementSuccess; } // Build the default team ID signing requirement NSString *codeSigningRequirement = [NSString stringWithFormat:@"(anchor apple generic and certificate leaf[subject.OU] = \"%@\")", hostTeamIdentifier]; if (@available(macOS 13.0, *)) { [connection setCodeSigningRequirement:codeSigningRequirement]; return SUValidateConnectionStatusSetCodeSigningRequirementSuccess; } // Fall back to audit token on older OS's if ([connection respondsToSelector:@selector(auditToken)]) { audit_token_t auditToken = [connection auditToken]; NSData *auditTokenData = [NSData dataWithBytes:&auditToken length:sizeof(auditToken)]; NSDictionary *attributes = @{ (NSString *)kSecGuestAttributeAudit: auditTokenData }; SecCodeRef code = NULL; OSStatus result = SecCodeCopyGuestWithAttributes(NULL, (__bridge CFDictionaryRef _Nullable)(attributes), kSecCSDefaultFlags, &code); if (result != errSecSuccess) { CFRelease(code); if (error != NULL) { *error = [NSError errorWithDomain:SUSparkleErrorDomain code:SUInsufficientSigningError userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"The client connection could not be validated because SecCodeCopyGuestWithAttributes() failed with error %d", result] }]; } return SUValidateConnectionStatusAPIFailure; } // Check if the client is code signed with our signing requirement SecRequirementRef requirement = NULL; result = SecRequirementCreateWithString((__bridge CFStringRef)codeSigningRequirement, kSecCSDefaultFlags, &requirement); if (result != errSecSuccess) { CFRelease(code); if (error != NULL) { *error = [NSError errorWithDomain:SUSparkleErrorDomain code:SUInsufficientSigningError userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"The client connection could not be validated because SecRequirementCreateWithString() failed with error %d", result] }]; } return SUValidateConnectionStatusAPIFailure; } CFErrorRef cfError = NULL; // This is not a static code, so we don't pass kSecCSCheckAllArchitectures result = SecCodeCheckValidityWithErrors(code, kSecCSDefaultFlags, requirement, &cfError); CFRelease(requirement); requirement = NULL; if (result != errSecSuccess) { NSError *cfBridgedError = (NSError *)CFBridgingRelease(cfError); if (error != NULL) { *error = [NSError errorWithDomain:SUSparkleErrorDomain code:SUInsufficientSigningError userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"The client connection could not be validated because validating the code signature failed with error %d (%@). Does the app meet the designated requirement: %@", result, cfBridgedError.localizedDescription, codeSigningRequirement] }]; } [self logSigningInfoForCode:code label:@"Client"]; CFRelease(code); return SUValidateConnectionStatusCodeSigningRequirementFailure; } CFRelease(code); return SUValidateConnectionStatusSetCodeSigningRequirementSuccess; } // Not much we can do if auditToken is not supported. This code should not be reached though. return SUValidateConectionNoSupportedValidationMethodFailure; } @end ================================================ FILE: Autoupdate/SUDiskImageUnarchiver.h ================================================ // // SUDiskImageUnarchiver.h // Sparkle // // Created by Andy Matuschak on 6/16/08. // Copyright 2008 Andy Matuschak. All rights reserved. // #ifndef SUDISKIMAGEUNARCHIVER_H #define SUDISKIMAGEUNARCHIVER_H #import #import "SUUnarchiverProtocol.h" NS_ASSUME_NONNULL_BEGIN SPU_OBJC_DIRECT_MEMBERS @interface SUDiskImageUnarchiver : NSObject - (instancetype)initWithArchivePath:(NSString *)archivePath extractionDirectory:(NSString *)extractionDirectory decryptionPassword:(nullable NSString *)decryptionPassword; + (BOOL)canUnarchivePath:(NSString *)path; @end NS_ASSUME_NONNULL_END #endif ================================================ FILE: Autoupdate/SUDiskImageUnarchiver.m ================================================ // // SUDiskImageUnarchiver.m // Sparkle // // Created by Andy Matuschak on 6/16/08. // Copyright 2008 Andy Matuschak. All rights reserved. // #import "SUDiskImageUnarchiver.h" #import "SUUnarchiverNotifier.h" #import "SULog.h" #import "SUErrors.h" #include "AppKitPrevention.h" @interface SUDiskImageUnarchiver () @end @implementation SUDiskImageUnarchiver { NSString *_archivePath; NSString *_decryptionPassword; NSString *_extractionDirectory; SUUnarchiverNotifier *_notifier; double _currentExtractionProgress; double _fileProgressIncrement; } + (BOOL)canUnarchivePath:(NSString *)path { return [[path pathExtension] isEqualToString:@"dmg"]; } + (BOOL)mustValidateBeforeExtraction { return NO; } - (instancetype)initWithArchivePath:(NSString *)archivePath extractionDirectory:(NSString *)extractionDirectory decryptionPassword:(nullable NSString *)decryptionPassword { self = [super init]; if (self != nil) { _archivePath = [archivePath copy]; _decryptionPassword = [decryptionPassword copy]; _extractionDirectory = [extractionDirectory copy]; } return self; } - (BOOL)needsVerifyBeforeExtractionKey { return NO; } - (void)unarchiveWithCompletionBlock:(void (^)(NSError * _Nullable))completionBlock progressBlock:(void (^ _Nullable)(double))progressBlock waitForCleanup:(BOOL)waitForCleanup { dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ SUUnarchiverNotifier *notifier = [[SUUnarchiverNotifier alloc] initWithCompletionBlock:completionBlock progressBlock:progressBlock]; [self extractDMGWithNotifier:notifier waitForCleanup:waitForCleanup]; }); } static NSUInteger fileCountForDirectory(NSFileManager *fileManager, NSString *itemPath) { NSUInteger fileCount = 0; NSDirectoryEnumerator *dirEnum = [fileManager enumeratorAtPath:itemPath]; for (NSString * __unused currentFile in dirEnum) { fileCount++; } return fileCount; } - (BOOL)fileManager:(NSFileManager *)fileManager shouldCopyItemAtURL:(NSURL *)srcURL toURL:(NSURL *)dstURL { _currentExtractionProgress += _fileProgressIncrement; [_notifier notifyProgress:_currentExtractionProgress]; return YES; } // Called on a non-main thread. - (void)extractDMGWithNotifier:(SUUnarchiverNotifier *)notifier waitForCleanup:(BOOL)waitForCleanup SPU_OBJC_DIRECT { @autoreleasepool { BOOL mountedSuccessfully = NO; // get a unique mount point path NSString *mountPoint = nil; NSFileManager *manager; NSError *error = nil; NSArray *contents = nil; do { NSString *uuidString = [[NSUUID UUID] UUIDString]; mountPoint = [@"/Volumes" stringByAppendingPathComponent:uuidString]; } // Note: this check does not follow symbolic links, which is what we want while ([[NSURL fileURLWithPath:mountPoint] checkResourceIsReachableAndReturnError:NULL]); NSMutableData *inputData = [NSMutableData data]; // Prepare stdin data for passwords and license agreements { // If no password is supplied, we will still be asked a password. // In that case we respond with an empty password. NSData *decryptionPasswordData = [_decryptionPassword dataUsingEncoding:NSUTF8StringEncoding]; if (decryptionPasswordData != nil) { [inputData appendData:decryptionPasswordData]; } // From the hdiutil docs: // read a null-terminated passphrase from standard input // // Add the null terminator [inputData appendBytes:"\0" length:1]; // Append prompt data for license agreements [inputData appendBytes:"yes\n" length:4]; } // Finder doesn't verify disk images anymore beyond the code signing signature (if available) // Opt out of the old CRC checksum checks // Also always pass -stdinpass so we gracefully handle password protected disk images even if we aren't expecting them NSArray *arguments = @[@"attach", _archivePath, @"-mountpoint", mountPoint, @"-noverify", @"-nobrowse", @"-noautoopen", @"-stdinpass"]; NSData *output = nil; NSInteger taskResult = -1; NSURL *mountPointURL = [NSURL fileURLWithPath:mountPoint isDirectory:YES]; NSURL *extractionDirectoryURL = [NSURL fileURLWithPath:_extractionDirectory isDirectory:YES]; NSMutableArray *itemsToExtract = [NSMutableArray array]; NSUInteger totalFileExtractionCount = 0; BOOL success = YES; { NSTask *task = [[NSTask alloc] init]; task.launchPath = @"/usr/bin/hdiutil"; task.currentDirectoryPath = @"/"; task.arguments = arguments; NSPipe *inputPipe = [NSPipe pipe]; NSPipe *outputPipe = [NSPipe pipe]; task.standardInput = inputPipe; task.standardOutput = outputPipe; NSFileHandle *fileStdHandle = outputPipe.fileHandleForReading; NSMutableData *currentOutput = [NSMutableData data]; fileStdHandle.readabilityHandler = ^(NSFileHandle *file) { [currentOutput appendData:file.availableData]; }; dispatch_semaphore_t terminationSemaphore = dispatch_semaphore_create(0); task.terminationHandler = ^(NSTask *__unused terminatingTask) { fileStdHandle.readabilityHandler = nil; dispatch_semaphore_signal(terminationSemaphore); }; if (![task launchAndReturnError:&error]) { goto reportError; } if (@available(macOS 10.15, *)) { if (![inputPipe.fileHandleForWriting writeData:inputData error:&error]) { goto reportError; } } #if MAC_OS_X_VERSION_MIN_REQUIRED < MAC_OS_X_VERSION_10_15 else { @try { [inputPipe.fileHandleForWriting writeData:inputData]; } @catch (NSException *) { goto reportError; } } #endif [inputPipe.fileHandleForWriting closeFile]; dispatch_semaphore_wait(terminationSemaphore, DISPATCH_TIME_FOREVER); output = [currentOutput copy]; taskResult = task.terminationStatus; } if (taskResult != 0) { NSString *resultStr = output ? [[NSString alloc] initWithData:output encoding:NSUTF8StringEncoding] : nil; SULog(SULogLevelError, @"hdiutil failed with code: %ld data: <<%@>>", (long)taskResult, resultStr); error = [NSError errorWithDomain:SUSparkleErrorDomain code:SUUnarchivingError userInfo:@{NSLocalizedDescriptionKey:[NSString stringWithFormat:@"Extraction failed due to hdiutil returning %ld status: %@", (long)taskResult, resultStr]}]; goto reportError; } mountedSuccessfully = YES; // Mounting can take some time, so increment progress _currentExtractionProgress = 0.1; [notifier notifyProgress:_currentExtractionProgress]; // Now that we've mounted it, we need to copy out its contents. manager = [[NSFileManager alloc] init]; contents = [manager contentsOfDirectoryAtPath:mountPoint error:&error]; if (contents == nil) { SULog(SULogLevelError, @"Couldn't enumerate contents of archive mounted at %@: %@", mountPoint, error); goto reportError; } // Sparkle can support installing pkg files, app bundles, and other bundle types for plug-ins // We must not filter any of those out for (NSString *item in contents) { NSURL *fromPathURL = [mountPointURL URLByAppendingPathComponent:item]; NSString *lastPathComponent = fromPathURL.lastPathComponent; // Ignore hidden files if ([lastPathComponent hasPrefix:@"."]) { continue; } // Ignore aliases NSNumber *aliasFlag = nil; if ([fromPathURL getResourceValue:&aliasFlag forKey:NSURLIsAliasFileKey error:NULL] && aliasFlag.boolValue) { continue; } // Ignore symbolic links NSNumber *symbolicFlag = nil; if ([fromPathURL getResourceValue:&symbolicFlag forKey:NSURLIsSymbolicLinkKey error:NULL] && symbolicFlag.boolValue) { continue; } // Ensure file is readable NSNumber *isReadableFlag = nil; if ([fromPathURL getResourceValue:&isReadableFlag forKey:NSURLIsReadableKey error:NULL] && !isReadableFlag.boolValue) { continue; } NSNumber *isDirectoryFlag = nil; if (![fromPathURL getResourceValue:&isDirectoryFlag forKey:NSURLIsDirectoryKey error:NULL]) { continue; } BOOL isDirectory = isDirectoryFlag.boolValue; NSString *pathExtension = fromPathURL.pathExtension; if (isDirectory) { // Skip directory types that aren't bundles or regular directories if ([pathExtension isEqualToString:@"rtfd"]) { continue; } } else { // The only non-directory files we care about are (m)pkg files if (![pathExtension isEqualToString:@"pkg"] && ![pathExtension isEqualToString:@"mpkg"]) { continue; } } if (isDirectory) { totalFileExtractionCount += fileCountForDirectory(manager, fromPathURL.path); } else { totalFileExtractionCount++; } [itemsToExtract addObject:item]; } _fileProgressIncrement = (0.99 - _currentExtractionProgress) / (double)totalFileExtractionCount; _notifier = notifier; // Copy all items we want to extract and notify of progress manager.delegate = self; for (NSString *item in itemsToExtract) { NSURL *fromURL = [mountPointURL URLByAppendingPathComponent:item]; NSURL *toURL = [extractionDirectoryURL URLByAppendingPathComponent:item]; if (![manager copyItemAtURL:fromURL toURL:toURL error:&error]) { SULog(SULogLevelError, @"Failed to copy '%@' to '%@' with error: %@", fromURL.path, toURL.path, error); goto reportError; } } [notifier notifyProgress:1.0]; goto finally; reportError: success = NO; finally: if (mountedSuccessfully) { NSTask *task = [[NSTask alloc] init]; task.launchPath = @"/usr/bin/hdiutil"; task.arguments = @[@"detach", mountPoint, @"-force"]; task.standardOutput = [NSPipe pipe]; task.standardError = [NSPipe pipe]; NSError *launchCleanupError = nil; if (![task launchAndReturnError:&launchCleanupError]) { SULog(SULogLevelError, @"Failed to unmount %@", mountPoint); SULog(SULogLevelError, @"Error: %@", launchCleanupError); } else if (waitForCleanup) { [task waitUntilExit]; } } else { SULog(SULogLevelError, @"Can't mount DMG %@", _archivePath); } if (success) { [notifier notifySuccess]; } else { [notifier notifyFailureWithError:error]; } } } - (NSString *)description { return [NSString stringWithFormat:@"%@ <%@>", [self class], _archivePath]; } @end ================================================ FILE: Autoupdate/SUFlatPackageUnarchiver.h ================================================ // // SUFlatPackageUnarchiver.h // Autoupdate // // Created by Mayur Pawashe on 1/30/21. // Copyright © 2021 Sparkle Project. All rights reserved. // #if SPARKLE_BUILD_PACKAGE_SUPPORT #import #import "SUUnarchiverProtocol.h" NS_ASSUME_NONNULL_BEGIN // An unarchiver for flat packages that doesn't really do any unarchiving SPU_OBJC_DIRECT_MEMBERS @interface SUFlatPackageUnarchiver : NSObject - (instancetype)initWithFlatPackagePath:(NSString *)flatPackagePath extractionDirectory:(NSString *)extractionDirectory expectingInstallationType:(NSString *)installationType; + (BOOL)canUnarchivePath:(NSString *)path; @end NS_ASSUME_NONNULL_END #endif ================================================ FILE: Autoupdate/SUFlatPackageUnarchiver.m ================================================ // // SUFlatPackageUnarchiver.m // Autoupdate // // Created by Mayur Pawashe on 1/30/21. // Copyright © 2021 Sparkle Project. All rights reserved. // #if SPARKLE_BUILD_PACKAGE_SUPPORT #import "SUFlatPackageUnarchiver.h" #import "SUUnarchiverNotifier.h" #import "SPUInstallationType.h" #import "SUErrors.h" #include "AppKitPrevention.h" @implementation SUFlatPackageUnarchiver { NSString *_flatPackagePath; NSString *_expectedInstallationType; NSString *_extractionDirectory; } + (BOOL)canUnarchivePath:(NSString *)path { return [path.pathExtension isEqualToString:@"pkg"] || [path.pathExtension isEqualToString:@"mpkg"]; } + (BOOL)mustValidateBeforeExtraction { return YES; } - (instancetype)initWithFlatPackagePath:(NSString *)flatPackagePath extractionDirectory:(NSString *)extractionDirectory expectingInstallationType:(NSString *)installationType { self = [super init]; if (self != nil) { _flatPackagePath = [flatPackagePath copy]; _expectedInstallationType = [installationType copy]; _extractionDirectory = [extractionDirectory copy]; } return self; } - (BOOL)needsVerifyBeforeExtractionKey { return NO; } - (void)unarchiveWithCompletionBlock:(void (^)(NSError * _Nullable))completionBlock progressBlock:(void (^ _Nullable)(double))progressBlock waitForCleanup:(BOOL)__unused waitForCleanup { SUUnarchiverNotifier *notifier = [[SUUnarchiverNotifier alloc] initWithCompletionBlock:completionBlock progressBlock:progressBlock]; // Flat packages must use guided package installs, not interactive BOOL isDirectory = NO; if (![_expectedInstallationType isEqualToString:SPUInstallationTypeGuidedPackage]) { [notifier notifyFailureWithError:[NSError errorWithDomain:SUSparkleErrorDomain code:SUUnarchivingError userInfo:@{ NSLocalizedDescriptionKey:[NSString stringWithFormat:@"Flat package does not have guided installation type but %@ instead", _expectedInstallationType]}]]; } else if (![[NSFileManager defaultManager] fileExistsAtPath:_flatPackagePath isDirectory:&isDirectory] || isDirectory) { [notifier notifyFailureWithError:[NSError errorWithDomain:SUSparkleErrorDomain code:SUUnarchivingError userInfo:@{ NSLocalizedDescriptionKey:[NSString stringWithFormat:@"Flat package does not exist at %@", _flatPackagePath]}]]; } else { // Copying the flat package should be very fast, especially on APFS NSError *copyError = nil; if (![[NSFileManager defaultManager] copyItemAtPath:_flatPackagePath toPath:[_extractionDirectory stringByAppendingPathComponent:_flatPackagePath.lastPathComponent] error:©Error]) { NSMutableDictionary *userInfoDictionary = [NSMutableDictionary dictionaryWithDictionary:@{ NSLocalizedDescriptionKey:[NSString stringWithFormat:@"Flat package (%@) cannot be copied to extraction directory (%@)", _flatPackagePath, _extractionDirectory]}]; if (copyError != nil) { userInfoDictionary[NSUnderlyingErrorKey] = copyError; } [notifier notifyFailureWithError:[NSError errorWithDomain:SUSparkleErrorDomain code:SUUnarchivingError userInfo:userInfoDictionary]]; } else { [notifier notifyProgress:1.0]; [notifier notifySuccess]; } } } - (NSString *)description { return [NSString stringWithFormat:@"%@ <%@>", [self class], _flatPackagePath]; } @end #endif ================================================ FILE: Autoupdate/SUGuidedPackageInstaller.h ================================================ // // 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 macOS'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. */ #if SPARKLE_BUILD_PACKAGE_SUPPORT #import #import "SUInstallerProtocol.h" SPU_OBJC_DIRECT_MEMBERS @interface SUGuidedPackageInstaller : NSObject - (instancetype)initWithPackagePath:(NSString *)packagePath homeDirectory:(NSString *)homeDirectory userName:(NSString *)userName; @end #endif ================================================ FILE: Autoupdate/SUGuidedPackageInstaller.m ================================================ // // SUGuidedPackageInstaller.m // Sparkle // // Created by Graham Miln on 14/05/2010. // Copyright 2010 Dragon Systems Software Limited. All rights reserved. // #if SPARKLE_BUILD_PACKAGE_SUPPORT #import #import "SUGuidedPackageInstaller.h" #import "SUErrors.h" #include "AppKitPrevention.h" @implementation SUGuidedPackageInstaller { NSString *_packagePath; NSString *_homeDirectory; NSString *_userName; } - (instancetype)initWithPackagePath:(NSString *)packagePath homeDirectory:(NSString *)homeDirectory userName:(NSString *)userName { self = [super init]; if (self != nil) { _packagePath = [packagePath copy]; _homeDirectory = [homeDirectory copy]; _userName = [userName copy]; } return self; } - (BOOL)performInitialInstallation:(NSError * __autoreleasing *)__unused error { return YES; } - (BOOL)performFinalInstallationProgressBlock:(nullable void(^)(double))__unused cb error:(NSError * __autoreleasing *)error { // This command *must* be run as root NSString *installerPath = @"/usr/sbin/installer"; NSTask *task = [[NSTask alloc] init]; task.launchPath = installerPath; task.arguments = @[@"-pkg", _packagePath, @"-target", @"/"]; // Set the $HOME and $USER variables so pre/post install scripts reference the correct user environment task.environment = @{@"HOME": _homeDirectory, @"USER": _userName}; task.standardError = nil; task.standardOutput = nil; NSError *launchError = nil; if (![task launchAndReturnError:&launchError]) { if (error != NULL) { NSMutableDictionary *userInfo = [NSMutableDictionary dictionaryWithDictionary:@{ NSLocalizedDescriptionKey: @"Guided package installer failed to launch" }]; if (launchError != nil) { userInfo[NSUnderlyingErrorKey] = launchError; } *error = [NSError errorWithDomain:SUSparkleErrorDomain code:SUInstallationError userInfo:userInfo]; } return NO; } [task waitUntilExit]; if (task.terminationStatus != EXIT_SUCCESS) { if (error != NULL) { *error = [NSError errorWithDomain:SUSparkleErrorDomain code:SUInstallationError userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Guided package installer returned non-zero exit status (%d)", task.terminationStatus] }]; } return NO; } return YES; } - (void)performCleanup { } @end #endif ================================================ FILE: Autoupdate/SUInstaller.h ================================================ // // SUInstaller.h // Sparkle // // Created by Andy Matuschak on 4/10/08. // Copyright 2008 Andy Matuschak. All rights reserved. // #import #import "SUInstallerProtocol.h" NS_ASSUME_NONNULL_BEGIN @class SUHost; SPU_OBJC_DIRECT_MEMBERS @interface SUInstaller : NSObject + (nullable id)installerForHost:(SUHost *)host expectedInstallationType:(NSString *)expectedInstallationType updateDirectory:(NSString *)updateDirectory connectionCodeSigningValidationSkipped:(BOOL)connectionCodeSigningValidationSkipped homeDirectory:(NSString *)homeDirectory userName:(NSString *)userName error:(NSError **)error; + (nullable NSString *)installSourcePathInUpdateFolder:(NSString *)inUpdateFolder forHost:(SUHost *)host #if SPARKLE_BUILD_PACKAGE_SUPPORT isPackage:(BOOL *)isPackagePtr isGuided:(nullable BOOL *)isGuidedPtr #endif ; @end NS_ASSUME_NONNULL_END ================================================ FILE: Autoupdate/SUInstaller.m ================================================ // // SUInstaller.m // Sparkle // // Created by Andy Matuschak on 4/10/08. // Copyright 2008 Andy Matuschak. All rights reserved. // #import "SUInstaller.h" #import "SUPlainInstaller.h" #import "SUGuidedPackageInstaller.h" #import "SUHost.h" #import "SUConstants.h" #import "SULog.h" #import "SUErrors.h" #import "SPUInstallationType.h" #import "SUNormalization.h" #include "AppKitPrevention.h" @implementation SUInstaller + (nullable NSString *)installSourcePathInUpdateFolder:(NSString *)inUpdateFolder forHost:(SUHost *)host #if SPARKLE_BUILD_PACKAGE_SUPPORT isPackage:(BOOL *)isPackagePtr isGuided:(BOOL *)isGuidedPtr #endif { NSParameterAssert(inUpdateFolder); NSParameterAssert(host); // Search subdirectories for the application NSString *currentFile, *newAppDownloadPath = nil, *bundleFileName = [[host bundlePath] lastPathComponent], *alternateBundleFileName = [[host name] stringByAppendingPathExtension:[[host bundlePath] pathExtension]]; #if SPARKLE_BUILD_PACKAGE_SUPPORT NSString *fallbackPackagePath = nil; BOOL isPackage = NO; BOOL isGuided = YES; #endif NSDirectoryEnumerator *dirEnum = [[NSFileManager defaultManager] enumeratorAtPath:inUpdateFolder]; NSString *bundleFileNameNoExtension = [bundleFileName stringByDeletingPathExtension]; while ((currentFile = [dirEnum nextObject])) { NSString *currentPath = [inUpdateFolder stringByAppendingPathComponent:currentFile]; // Ignore all symbolic links and aliases { NSURL *currentPathURL = [NSURL fileURLWithPath:currentPath]; NSNumber *symbolicLinkFlag = nil; [currentPathURL getResourceValue:&symbolicLinkFlag forKey:NSURLIsSymbolicLinkKey error:NULL]; if (symbolicLinkFlag.boolValue) { // NSDirectoryEnumerator won't recurse into symlinked directories continue; } NSNumber *aliasFlag = nil; [currentPathURL getResourceValue:&aliasFlag forKey:NSURLIsAliasFileKey error:NULL]; if (aliasFlag.boolValue) { NSNumber *directoryFlag = nil; [currentPathURL getResourceValue:&directoryFlag forKey:NSURLIsDirectoryKey error:NULL]; // Some DMGs have symlinks into /Applications! That's no good! if (directoryFlag.boolValue) { [dirEnum skipDescendents]; } continue; } } NSString *currentFilename = [currentFile lastPathComponent]; #if SPARKLE_BUILD_PACKAGE_SUPPORT NSString *currentExtension = [currentFile pathExtension]; NSString *currentFilenameNoExtension = [currentFilename stringByDeletingPathExtension]; #endif if ([currentFilename isEqualToString:bundleFileName] || [currentFilename isEqualToString:alternateBundleFileName]) // We found one! { #if SPARKLE_BUILD_PACKAGE_SUPPORT isPackage = NO; #endif newAppDownloadPath = currentPath; break; #if SPARKLE_BUILD_PACKAGE_SUPPORT } else if ([currentExtension isEqualToString:@"pkg"] || [currentExtension isEqualToString:@"mpkg"]) { if ([currentFilenameNoExtension isEqualToString:bundleFileNameNoExtension]) { isPackage = YES; newAppDownloadPath = currentPath; break; } else { // Remember any other non-matching packages we have seen should we need to use one of them as a fallback. fallbackPackagePath = currentPath; } #endif } else { // Try matching on bundle identifiers in case the user has changed the name of the host app NSBundle *incomingBundle = [NSBundle bundleWithPath:currentPath]; NSString *hostBundleIdentifier = host.bundle.bundleIdentifier; if (incomingBundle && [incomingBundle.bundleIdentifier isEqualToString:hostBundleIdentifier]) { #if SPARKLE_BUILD_PACKAGE_SUPPORT isPackage = NO; #endif newAppDownloadPath = currentPath; break; } } } #if SPARKLE_BUILD_PACKAGE_SUPPORT // We don't have a valid path. Try to use the fallback package. if (newAppDownloadPath == nil && fallbackPackagePath != nil) { isPackage = YES; newAppDownloadPath = fallbackPackagePath; } if (isPackage) { // Guided (or now "normal") installs used to be opt-in (i.e, Sparkle would detect foo.sparkle_guided.pkg or foo.sparkle_guided.mpkg), // Interactive installs are no longer supported, so isGuided = NO will later turn into an error // foo.app -> foo.sparkle_interactive.pkg or foo.sparkle_interactive.mpkg if ([[[newAppDownloadPath stringByDeletingPathExtension] pathExtension] isEqualToString:@"sparkle_interactive"]) { isGuided = NO; } } if (isPackagePtr) *isPackagePtr = isPackage; if (isGuidedPtr) *isGuidedPtr = isGuided; #endif if (!newAppDownloadPath) { SULog(SULogLevelError, @"Searched %@ for %@.(app%@)", inUpdateFolder, bundleFileNameNoExtension, #if SPARKLE_BUILD_PACKAGE_SUPPORT @"|pkg" #else @"" #endif ); } return newAppDownloadPath; } + (nullable id)installerForHost:(SUHost *)host expectedInstallationType:(NSString *)expectedInstallationType updateDirectory:(NSString *)updateDirectory connectionCodeSigningValidationSkipped:(BOOL)connectionCodeSigningValidationSkipped homeDirectory:(NSString *)homeDirectory userName:(NSString *)userName error:(NSError * __autoreleasing *)error { #if SPARKLE_BUILD_PACKAGE_SUPPORT BOOL isPackage = NO; BOOL isGuided = NO; #endif NSString *newDownloadPath = [self installSourcePathInUpdateFolder:updateDirectory forHost:host #if SPARKLE_BUILD_PACKAGE_SUPPORT isPackage:&isPackage isGuided:&isGuided #endif ]; if (newDownloadPath == nil) { if (error != NULL) { *error = [NSError errorWithDomain:SUSparkleErrorDomain code:SUMissingUpdateError userInfo:@{ NSLocalizedDescriptionKey: @"Couldn't find an appropriate update in the downloaded package." }]; } return nil; } #if SPARKLE_BUILD_PACKAGE_SUPPORT if (isPackage && connectionCodeSigningValidationSkipped) { // Package updates cannot update arbitrary bundles if code signing validation on the connection was skipped // (This means our helper is not code signed with an Apple issued certificate). // In this case, the main executable must be owned by root and the host must contain our installer tool NSString *installerExecutablePath = NSBundle.mainBundle.executableURL.URLByResolvingSymlinksInPath.path; if (installerExecutablePath == nil) { if (error != NULL) { *error = [NSError errorWithDomain:SUSparkleErrorDomain code:SUMissingUpdateError userInfo:@{ NSLocalizedDescriptionKey: @"The "@SPARKLE_RELAUNCH_TOOL_NAME" executable path failed to be retrieved, which is an internal error that shouldn't happen. Please try to update using a production build of your app." }]; } return nil; } NSString *hostBundlePath = host.bundle.bundleURL.URLByResolvingSymlinksInPath.path; if (hostBundlePath == nil) { if (error != NULL) { *error = [NSError errorWithDomain:SUSparkleErrorDomain code:SUMissingUpdateError userInfo:@{ NSLocalizedDescriptionKey: @"The host bundle path to update to be retrieved, which is an internal error that shouldn't happen. Please try to update using a production build of your app." }]; } return nil; } NSError *attributesError = nil; NSDictionary *fileAttributes = [NSFileManager.defaultManager attributesOfItemAtPath:installerExecutablePath error:&attributesError]; if (fileAttributes == nil) { if (error != NULL) { *error = [NSError errorWithDomain:SUSparkleErrorDomain code:SUMissingUpdateError userInfo:@{ NSLocalizedDescriptionKey: @"File attributes failed to be retrieved from the "@SPARKLE_RELAUNCH_TOOL_NAME" executable, which is an internal error that shouldn't happen. Please try to update using a production build of your app." }]; } return nil; } NSNumber *installerOwnerID = fileAttributes[NSFileOwnerAccountID]; if (installerOwnerID == nil) { if (error != NULL) { *error = [NSError errorWithDomain:SUSparkleErrorDomain code:SUMissingUpdateError userInfo:@{ NSLocalizedDescriptionKey: @"File owner ID failed to be retrieved from the "@SPARKLE_RELAUNCH_TOOL_NAME" executable, which is an internal error that shouldn't happen. Please try to update using a production build of your app." }]; } return nil; } // Check our helper is owned by root if (![installerOwnerID isEqual:@(0)]) { if (error != NULL) { *error = [NSError errorWithDomain:SUSparkleErrorDomain code:SUMissingUpdateError userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Due to security, updating apps with package installers is disabled when '%@' is not signed with the same Team ID as your app, and "@SPARKLE_RELAUNCH_TOOL_NAME" is not owned by root (this specific case) or not residing in '%@'. Please either test an exported & production build of your app, or for development, code sign your app and Sparkle's embedded helpers with the same team: https://sparkle-project.org/documentation/sandboxing#code-signing", installerExecutablePath, hostBundlePath] }]; } return nil; } NSArray *installerExecutablePathComponents = installerExecutablePath.pathComponents; NSArray *hostBundlePathComponents = hostBundlePath.pathComponents; if (installerExecutablePathComponents.count <= hostBundlePathComponents.count) { if (error != NULL) { *error = [NSError errorWithDomain:SUSparkleErrorDomain code:SUMissingUpdateError userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Due to security, updating apps with package installers is disabled when '%@' is not signed with the same Team ID as your app, and "@SPARKLE_RELAUNCH_TOOL_NAME" is not owned by root or not residing in '%@' (this specific case). Please either test an exported & production build of your app, or for development, code sign your app and Sparkle's embedded helpers with the same team: https://sparkle-project.org/documentation/sandboxing#code-signing", installerExecutablePath, hostBundlePath] }]; } return nil; } // Check out helper resides inside the host app to be updated if (![[installerExecutablePathComponents subarrayWithRange:NSMakeRange(0, hostBundlePathComponents.count)] isEqualToArray:hostBundlePathComponents]) { if (error != NULL) { *error = [NSError errorWithDomain:SUSparkleErrorDomain code:SUMissingUpdateError userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Due to security, updating apps with package installers is disabled when '%@' is not signed with the same Team ID as your app, and "@SPARKLE_RELAUNCH_TOOL_NAME" is not owned by root or not residing in '%@' (this specific case). Please either test an exported & production build of your app, or for development, code sign your app and Sparkle's embedded helpers with the same team: https://sparkle-project.org/documentation/sandboxing#code-signing", installerExecutablePath, hostBundlePath] }]; } return nil; } } #else (void)connectionCodeSigningValidationSkipped; #endif // Make sure we find the type of installer that we were expecting to find // We shouldn't implicitly trust the installation type fed into here from the appcast because the installation type helps us determine // ahead of time whether or not this installer tool should be ran as root or not id installer = nil; #if SPARKLE_BUILD_PACKAGE_SUPPORT if (isPackage && isGuided) { if (![expectedInstallationType isEqualToString:SPUInstallationTypeGuidedPackage]) { if (error != NULL) { *error = [NSError errorWithDomain:SUSparkleErrorDomain code:SUInstallationError userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Found guided package installer but '%@=%@' was probably missing in the appcast item enclosure", SUAppcastAttributeInstallationType, SPUInstallationTypeGuidedPackage] }]; } } else { installer = [[SUGuidedPackageInstaller alloc] initWithPackagePath:newDownloadPath homeDirectory:homeDirectory userName:userName]; } } else if (isPackage) { if (error != NULL) { *error = [NSError errorWithDomain:SUSparkleErrorDomain code:SUInstallationError userInfo:@{ NSLocalizedDescriptionKey: @"Found interactive package installer (with 'sparkle_interactive' in the filename) but these are no longer supported. Please remove 'sparkle_interactive' from the filename." }]; } } else #endif { if (![expectedInstallationType isEqualToString:SPUInstallationTypeApplication]) { if (error != NULL) { *error = [NSError errorWithDomain:SUSparkleErrorDomain code:SUInstallationError userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Found regular application update but expected '%@=%@' from the appcast item enclosure instead", SUAppcastAttributeInstallationType, expectedInstallationType] }]; } } else { NSString *normalizedInstallationPath = nil; if (SPARKLE_NORMALIZE_INSTALLED_APPLICATION_NAME) { normalizedInstallationPath = SUNormalizedInstallationPath(host); } // If we have a normalized path, we'll install to "#{CFBundleName}.app", but only if that path doesn't already exist. If we're "Foo 4.2.app," and there's a "Foo.app" in this directory, we don't want to overwrite it! But if there's no "Foo.app," we'll take that name. // Otherwise if there's no normalized path (the more likely case), we'll just use the host bundle's path // Check progress agent app which computes normalized path too according to these rules NSString *installationPath; if (normalizedInstallationPath != nil && ![[NSFileManager defaultManager] fileExistsAtPath:normalizedInstallationPath]) { installationPath = normalizedInstallationPath; } else { installationPath = host.bundlePath; } installer = [[SUPlainInstaller alloc] initWithHost:host bundlePath:newDownloadPath installationPath:installationPath]; } } return installer; } @end ================================================ FILE: Autoupdate/SUInstallerProtocol.h ================================================ // // SUInstallerProtocol.h // Sparkle // // Created by Mayur Pawashe on 3/12/16. // Copyright © 2016 Sparkle Project. All rights reserved. // #import NS_ASSUME_NONNULL_BEGIN @protocol SUInstallerProtocol // Any installation work can be done prior to user application being terminated and relaunched // No UI should occur during this stage (i.e, do not show package installer apps, etc..) // Should be able to be called from non-main thread - (BOOL)performInitialInstallation:(NSError **)error; // Any installation work after the user application has has been terminated. This is where the final installation work can be done. // After this stage is done, the user application may be relaunched. // Should be able to be called from non-main thread - (BOOL)performFinalInstallationProgressBlock:(nullable void(^)(double))cb error:(NSError **)error; // Any clean up work can be done here // This is work that may be performed after the user application may have been updated / relaunched, // or after an error occurred in the previous stages. // Should be able to be called from any thread - (void)performCleanup; @end NS_ASSUME_NONNULL_END ================================================ FILE: Autoupdate/SUPipedUnarchiver.h ================================================ // // SUPipedUnarchiver.h // Sparkle // // Created by Andy Matuschak on 6/16/08. // Copyright 2008 Andy Matuschak. All rights reserved. // #ifndef SUPIPEDUNARCHIVER_H #define SUPIPEDUNARCHIVER_H #import #import "SUUnarchiverProtocol.h" NS_ASSUME_NONNULL_BEGIN SPU_OBJC_DIRECT_MEMBERS @interface SUPipedUnarchiver : NSObject - (instancetype)initWithArchivePath:(NSString *)archivePath extractionDirectory:(NSString *)extractionDirectory; + (BOOL)canUnarchivePath:(NSString *)path; @end NS_ASSUME_NONNULL_END #endif ================================================ FILE: Autoupdate/SUPipedUnarchiver.m ================================================ // // SUPipedUnarchiver.m // Sparkle // // Created by Andy Matuschak on 6/16/08. // Copyright 2008 Andy Matuschak. All rights reserved. // #import "SUPipedUnarchiver.h" #import "SUUnarchiverNotifier.h" #import "SULog.h" #import "SUErrors.h" #include "AppKitPrevention.h" @implementation SUPipedUnarchiver { NSString *_archivePath; NSString *_extractionDirectory; } // pipingData == NO is only supported for zip archives static NSArray * _Nullable argumentsConformingToTypeOfPath(NSString *path, BOOL pipingData, NSString * __autoreleasing *outCommand) { NSArray *extractTGZ = @[@"/usr/bin/tar", @"-zxC"]; NSArray *extractTBZ = @[@"/usr/bin/tar", @"-jxC"]; NSArray *extractTXZ = extractTGZ; // Note: keep this list in sync with generate_appcast's unarchiveUpdates() NSMutableDictionary *> *extractCommandDictionary = [@{ @".zip" : (pipingData ? @[@"/usr/bin/ditto", @"-x", @"-k", @"-"] : @[@"/usr/bin/ditto", @"-x", @"-k", path]), @".tar" : @[@"/usr/bin/tar", @"-xC"], @".tar.gz" : extractTGZ, @".tgz" : extractTGZ, @".tar.bz2" : extractTBZ, @".tbz" : extractTBZ, @".tar.xz" : extractTXZ, @".txz" : extractTXZ, @".tar.lzma" : extractTXZ, } mutableCopy]; // At least the latest versions of 10.15 understand how to extract aar files // Versions before 10.15 do not understand extracting newly created aar files // Note encrypted aea files are supported in macOS 12 onwards, if we ever want to support those one day if (@available(macOS 10.15.7, *)) { NSString *appleArchiveCommand; if (@available(macOS 11, *)) { appleArchiveCommand = @"/usr/bin/aa"; } else { // In 10.15 the utility was named yaa, which was later renamed to aar appleArchiveCommand = @"/usr/bin/yaa"; } NSArray *extractAppleArchive = @[appleArchiveCommand, @"extract", @"-d"]; [extractCommandDictionary addEntriesFromDictionary:@{ @".aar" : extractAppleArchive, @".yaa" : extractAppleArchive, }]; } NSString *lastPathComponent = [path lastPathComponent]; for (NSString *currentType in extractCommandDictionary) { if ([lastPathComponent hasSuffix:currentType]) { NSArray *commandAndArguments = [extractCommandDictionary objectForKey:currentType]; if (outCommand != NULL) { *outCommand = commandAndArguments.firstObject; } return [commandAndArguments subarrayWithRange:NSMakeRange(1, commandAndArguments.count - 1)]; } } return nil; } + (BOOL)canUnarchivePath:(NSString *)path { return argumentsConformingToTypeOfPath(path, YES, NULL) != nil; } + (BOOL)mustValidateBeforeExtraction { return NO; } - (instancetype)initWithArchivePath:(NSString *)archivePath extractionDirectory:(NSString *)extractionDirectory { self = [super init]; if (self != nil) { _archivePath = [archivePath copy]; _extractionDirectory = [extractionDirectory copy]; } return self; } - (BOOL)needsVerifyBeforeExtractionKey { return ([_archivePath hasSuffix:@".aar"] || [_archivePath hasSuffix:@".yaa"]); } - (void)unarchiveWithCompletionBlock:(void (^)(NSError * _Nullable))completionBlock progressBlock:(void (^ _Nullable)(double))progressBlock waitForCleanup:(BOOL)__unused waitForCleanup { NSString *command = nil; NSArray *arguments = argumentsConformingToTypeOfPath(_archivePath, YES, &command); assert(arguments != nil); assert(command != nil); dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ @autoreleasepool { SUUnarchiverNotifier *notifier = [[SUUnarchiverNotifier alloc] initWithCompletionBlock:completionBlock progressBlock:progressBlock]; NSError *extractError = nil; if ([self extractArchivePipingData:YES command:command arguments:arguments notifier:notifier error:&extractError]) { [notifier notifySuccess]; } else { // If we fail due to an IO PIPE write failure for zip files, // we may re-attempt extracting the archive without piping archive data // (and without fine grained progress reporting). // This is to workaround a bug in ditto which causes extraction to fail when piping data for // some types of constructed zip files. // Note this bug is fixed on macOS 15+, so this workaround is not needed there. // https://github.com/sparkle-project/Sparkle/issues/2544 BOOL useNonPipingWorkaround; if (@available(macOS 15, *)) { useNonPipingWorkaround = NO; } else { NSError *underlyingError = extractError.userInfo[NSUnderlyingErrorKey]; useNonPipingWorkaround = [self->_archivePath.pathExtension isEqualToString:@"zip"] && ([extractError.domain isEqualToString:SUSparkleErrorDomain] && extractError.code == SUUnarchivingError && [underlyingError.domain isEqualToString:NSPOSIXErrorDomain] && underlyingError.code == EPIPE); } if (!useNonPipingWorkaround) { [notifier notifyFailureWithError:extractError]; } else { // Re-create the extraction directory, then try extracting without piping NSFileManager *fileManager = [NSFileManager defaultManager]; NSURL *extractionDirectoryURL = [NSURL fileURLWithPath:self->_extractionDirectory isDirectory:YES]; NSError *removeError = nil; if (![fileManager removeItemAtURL:extractionDirectoryURL error:&removeError]) { SULog(SULogLevelError, @"Failed to remove extraction directory path for non-piping workaround with error: %@", removeError); [notifier notifyFailureWithError:extractError]; } else { NSError *createError = nil; if (![fileManager createDirectoryAtPath:self->_extractionDirectory withIntermediateDirectories:NO attributes:nil error:&createError]) { SULog(SULogLevelError, @"Failed to create new extraction directory path for non-piping workaround with error: %@", createError); [notifier notifyFailureWithError:extractError]; } else { // The ditto command will be the same so no need to fetch it again NSArray *nonPipingArguments = argumentsConformingToTypeOfPath(self->_archivePath, NO, NULL); assert(nonPipingArguments != nil); NSError *nonPipingExtractError = nil; if ([self extractArchivePipingData:NO command:command arguments:nonPipingArguments notifier:notifier error:&nonPipingExtractError]) { [notifier notifySuccess]; } else { [notifier notifyFailureWithError:nonPipingExtractError]; } } } } } } }); } // This method abstracts the types that use a command line tool piping data from stdin. - (BOOL)extractArchivePipingData:(BOOL)pipingData command:(NSString *)command arguments:(NSArray*)args notifier:(SUUnarchiverNotifier *)notifier error:(NSError * __autoreleasing *)outError SPU_OBJC_DIRECT { // *** GETS CALLED ON NON-MAIN THREAD!!! NSString *destination = _extractionDirectory; NSFileManager *fileManager = [NSFileManager defaultManager]; if (pipingData) { SULog(SULogLevelDefault, @"Extracting using '%@' '%@' < '%@' '%@'", command, [args componentsJoinedByString:@"' '"], _archivePath, destination); } else { SULog(SULogLevelDefault, @"Extracting using '%@' '%@' '%@'", command, [args componentsJoinedByString:@"' '"], destination); } // Get expected file size for piping the archive NSUInteger expectedLength; if (pipingData) { NSDictionary *attributes = [fileManager attributesOfItemAtPath:_archivePath error:nil]; expectedLength = [(NSNumber *)[attributes objectForKey:NSFileSize] unsignedIntegerValue]; if (expectedLength == 0) { NSError *error = [NSError errorWithDomain:SUSparkleErrorDomain code:SUUnarchivingError userInfo:@{ NSLocalizedDescriptionKey:[NSString stringWithFormat:@"Extraction failed, archive '%@' is empty", _archivePath]}]; if (outError != NULL) { *outError = error; } return NO; } } else { expectedLength = 0; } NSTask *task = [[NSTask alloc] init]; NSPipe *pipe; if (pipingData) { pipe = [NSPipe pipe]; [task setStandardInput:pipe]; } else { pipe = nil; } [task setStandardError:[NSFileHandle fileHandleWithStandardError]]; [task setStandardOutput:[NSFileHandle fileHandleWithStandardOutput]]; [task setLaunchPath:command]; [task setArguments:[args arrayByAddingObject:destination]]; NSError *launchError = nil; if (![task launchAndReturnError:&launchError]) { if (outError != NULL) { *outError = launchError; } return NO; } NSError *underlyingOutError = nil; NSUInteger bytesWritten = 0; if (pipingData) { NSFileHandle *archiveInput = [NSFileHandle fileHandleForReadingAtPath:_archivePath]; NSFileHandle *archiveOutput = [pipe fileHandleForWriting]; #if MAC_OS_X_VERSION_MIN_REQUIRED < MAC_OS_X_VERSION_10_15 BOOL hasIOErrorMethods; if (@available(macOS 10.15, *)) { hasIOErrorMethods = YES; } else { hasIOErrorMethods = NO; } #endif do { NSData *data; #if MAC_OS_X_VERSION_MIN_REQUIRED < MAC_OS_X_VERSION_10_15 if (!hasIOErrorMethods) { @try { data = [archiveInput readDataOfLength:256*1024]; } @catch (NSException *exception) { SULog(SULogLevelError, @"Failed to read data from archive with exception reason %@", exception.reason); data = nil; } } else #endif { NSError *readError = nil; #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wpartial-availability" data = [archiveInput readDataUpToLength:256*1024 error:&readError]; #pragma clang diagnostic pop if (data == nil) { SULog(SULogLevelError, @"Failed to read data from archive with error %@", readError); underlyingOutError = readError; } } NSUInteger len = [data length]; if (len == 0) { break; } NSError *writeError = nil; #if MAC_OS_X_VERSION_MIN_REQUIRED < MAC_OS_X_VERSION_10_15 if (!hasIOErrorMethods) { @try { [archiveOutput writeData:data]; } @catch (NSException *exception) { SULog(SULogLevelError, @"Failed to write data to pipe with exception reason %@", exception.reason); if ([exception.name isEqualToString:NSFileHandleOperationException]) { NSError *underlyingFileHandleError = exception.userInfo[@"NSFileHandleOperationExceptionUnderlyingError"]; NSError *underlyingPOSIXError = underlyingFileHandleError.userInfo[NSUnderlyingErrorKey]; underlyingOutError = underlyingPOSIXError; } break; } } else #endif { #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wpartial-availability" if (![archiveOutput writeData:data error:&writeError]) { #pragma clang diagnostic pop SULog(SULogLevelError, @"Failed to write data to pipe with error %@", writeError); NSError *underlyingPOSIXError = writeError.userInfo[NSUnderlyingErrorKey]; underlyingOutError = underlyingPOSIXError; break; } } bytesWritten += len; [notifier notifyProgress:(double)bytesWritten / (double)expectedLength]; } while(bytesWritten < expectedLength); if (@available(macOS 10.15, *)) { NSError *archiveOutputCloseError = nil; if (![archiveOutput closeAndReturnError:&archiveOutputCloseError]) { SULog(SULogLevelError, @"Failed to close pipe with error %@", archiveOutputCloseError); } } #if MAC_OS_X_VERSION_MIN_REQUIRED < MAC_OS_X_VERSION_10_15 else { [archiveOutput closeFile]; } #endif if (@available(macOS 10.15, *)) { NSError *archiveInputCloseError = nil; if (![archiveInput closeAndReturnError:&archiveInputCloseError]) { SULog(SULogLevelError, @"Failed to close archive input with error %@", archiveInputCloseError); } } #if MAC_OS_X_VERSION_MIN_REQUIRED < MAC_OS_X_VERSION_10_15 else { [archiveInput closeFile]; } #endif } [task waitUntilExit]; if ([task terminationStatus] != 0) { NSMutableDictionary *userInfo = [@{ NSLocalizedDescriptionKey:[NSString stringWithFormat:@"Extraction failed, command '%@' returned %d", command, [task terminationStatus]]} mutableCopy]; if (underlyingOutError != nil) { userInfo[NSUnderlyingErrorKey] = underlyingOutError; } NSError *error = [NSError errorWithDomain:SUSparkleErrorDomain code:SUUnarchivingError userInfo:[userInfo copy]]; if (outError != NULL) { *outError = error; } return NO; } if (!pipingData) { [notifier notifyProgress:1.0]; } else if (bytesWritten != expectedLength) { // Don't set underlying error in this case // This may fail due to a write PIPE error but we don't currently support extracting archives that have // extraneous data leftover because these may be corrupt. // We don't want to later workaround extraction by not piping the data. NSError *error = [NSError errorWithDomain:SUSparkleErrorDomain code:SUUnarchivingError userInfo:@{ NSLocalizedDescriptionKey:[NSString stringWithFormat:@"Extraction failed, command '%@' got only %ld of %ld bytes", command, (long)bytesWritten, (long)expectedLength]}]; if (outError != NULL) { *outError = error; } return NO; } return YES; } - (NSString *)description { return [NSString stringWithFormat:@"%@ <%@>", [self class], _archivePath]; } @end ================================================ FILE: Autoupdate/SUPlainInstaller.h ================================================ // // SUPlainInstaller.h // Sparkle // // Created by Andy Matuschak on 4/10/08. // Copyright 2008 Andy Matuschak. All rights reserved. // #import #import "SUInstallerProtocol.h" @class SUHost; @protocol SUVersionComparison; SPU_OBJC_DIRECT_MEMBERS @interface SUPlainInstaller : NSObject /** @param host The current (old) bundle host @param bundlePath The path to the new bundle that will be installed. @param installationPath The path the new bundlePath will be installed to. */ - (instancetype)initWithHost:(SUHost *)host bundlePath:(NSString *)bundlePath installationPath:(NSString *)installationPath; @end ================================================ FILE: Autoupdate/SUPlainInstaller.m ================================================ // // SUPlainInstaller.m // Sparkle // // Created by Andy Matuschak on 4/10/08. // Copyright 2008 Andy Matuschak. All rights reserved. // #import "SUPlainInstaller.h" #import "SUFileManager.h" #import "SUConstants.h" #import "SUHost.h" #import "SULog.h" #import "SUErrors.h" #import "SUVersionComparisonProtocol.h" #import "SUStandardVersionComparator.h" #import "SUCodeSigningVerifier.h" #include "AppKitPrevention.h" @implementation SUPlainInstaller { SUHost *_host; NSString *_bundlePath; NSString *_installationPath; NSURL *_temporaryOldDirectory; // We get an obj-c warning if we use 'newTemporaryDirectory' name about new + ownership stuff, so use 'temporaryNewDirectory' instead NSURL *_temporaryNewDirectory; BOOL _newAndOldBundlesOnSameVolume; BOOL _canPerformSafeAtomicSwap; } - (instancetype)initWithHost:(SUHost *)host bundlePath:(NSString *)bundlePath installationPath:(NSString *)installationPath { self = [super init]; if (self != nil) { _host = host; _bundlePath = [bundlePath copy]; _installationPath = [installationPath copy]; } return self; } - (void)_performInitialInstallationWithFileManager:(SUFileManager *)fileManager oldBundleURL:(NSURL *)oldBundleURL newBundleURL:(NSURL *)newBundleURL performGatekeeperScan:(BOOL)performGatekeeperScan progressBlock:(nullable void(^)(double))progress SPU_OBJC_DIRECT { // Release our new app from quarantine NSError *quarantineError = nil; if (![fileManager releaseItemFromQuarantineAtRootURL:newBundleURL error:&quarantineError]) { // Not big enough of a deal to fail the entire installation SULog(SULogLevelError, @"Failed to release quarantine at %@ with error %@", newBundleURL.path, quarantineError); } if (progress) { progress(5/11.0); } // Try to preserve Finder Tags NSArray *resourceTags = nil; BOOL retrievedResourceTags = [oldBundleURL getResourceValue:&resourceTags forKey:NSURLTagNamesKey error:NULL]; if (retrievedResourceTags && resourceTags.count > 0) { [newBundleURL setResourceValue:resourceTags forKey:NSURLTagNamesKey error:NULL]; } if (progress) { progress(6/11.0); } // Update owner and group (if possible) NSError *changeOwnerAndGroupError = nil; if (![fileManager changeOwnerAndGroupOfItemAtRootURL:newBundleURL toMatchURL:oldBundleURL error:&changeOwnerAndGroupError]) { // Not a fatal error SULog(SULogLevelError, @"Failed to change owner and group of new app at %@ to match old app at %@", newBundleURL.path, oldBundleURL.path); SULog(SULogLevelError, @"Error: %@", changeOwnerAndGroupError); } if (progress) { progress(7/11.0); } // Register the new bundle with LaunchServices and the system NSError *touchError = nil; if (![fileManager updateModificationAndAccessTimeOfItemAtURL:newBundleURL error:&touchError]) { // Not a fatal error, but a pretty unfortunate one SULog(SULogLevelError, @"Failed to update modification and access time of new app at %@", newBundleURL.path); SULog(SULogLevelError, @"Error: %@", touchError); } if (progress) { progress(8/11.0); } if (performGatekeeperScan) { // Perform a Gatekeeper scan to pre-warm the app launch // This avoids users seeing a "Verifying..." dialog when the installed update is launched // Note the tool we use to perform the Gatekeeper scan (gktool) is technically available on macOS 14.0, // however there are some potential bugs/issues with performing a Gatekeeper scan on versions before 14.4: // https://github.com/sparkle-project/Sparkle/issues/2491 if (@available(macOS 14.4, *)) { // Only perform Gatekeeper scan if we're updating an app bundle NSString *newBundlePath = newBundleURL.path; if ([newBundlePath.pathExtension caseInsensitiveCompare:@"app"] == NSOrderedSame) { NSURL *gktoolURL = [NSURL fileURLWithPath:@"/usr/bin/gktool" isDirectory:NO]; if ([gktoolURL checkResourceIsReachableAndReturnError:NULL]) { NSTask *gatekeeperScanTask = [[NSTask alloc] init]; gatekeeperScanTask.executableURL = gktoolURL; gatekeeperScanTask.arguments = @[@"scan", newBundlePath]; NSError *taskError; if (![gatekeeperScanTask launchAndReturnError:&taskError]) { // Not a fatal error SULog(SULogLevelError, @"Failed to perform GateKeeper scan on '%@' with error %@", newBundlePath, taskError); } else { [gatekeeperScanTask waitUntilExit]; if (gatekeeperScanTask.terminationStatus != 0) { SULog(SULogLevelError, @"gktool failed and returned exit status %d", gatekeeperScanTask.terminationStatus); } } } } } } } - (BOOL)startInstallationToURL:(NSURL *)installationURL fromUpdateAtURL:(NSURL *)newURL withHost:(SUHost *)host progressBlock:(nullable void(^)(double))progress error:(NSError * __autoreleasing *)error SPU_OBJC_DIRECT { if (installationURL == nil || newURL == nil) { // this really shouldn't happen but just in case SULog(SULogLevelError, @"Failed to perform installation because either installation URL (%@) or new URL (%@) is nil", installationURL, newURL); if (error != NULL) { *error = [NSError errorWithDomain:SUSparkleErrorDomain code:SUInstallationError userInfo:@{ NSLocalizedDescriptionKey: @"Failed to perform installation because the paths to install at and from are not valid" }]; } return NO; } if (progress) { progress(1/11.0); } SUFileManager *fileManager = [[SUFileManager alloc] init]; // Update the access time of our entire application before moving it into a temporary directory // The system periodically cleans up files by looking at the mod & access times, so we have to make sure they're up to date // They could be potentially be preserved when archiving an application, but also an update could just be sitting on the system for a long time // before being installed if (!_newAndOldBundlesOnSameVolume) { NSError *accessTimeError = nil; if (![fileManager updateAccessTimeOfItemAtRootURL:newURL error:&accessTimeError]) { if (error != NULL) { NSMutableDictionary *userInfo = [NSMutableDictionary dictionaryWithDictionary:@{ NSLocalizedDescriptionKey: @"Failed to recursively update new application's modification time before moving into temporary directory" }]; if (accessTimeError != nil) { userInfo[NSUnderlyingErrorKey] = accessTimeError; } *error = [NSError errorWithDomain:SUSparkleErrorDomain code:SUInstallationError userInfo:userInfo]; } return NO; } } NSURL *oldURL = [NSURL fileURLWithPath:host.bundlePath]; if (oldURL == nil) { // this really shouldn't happen but just in case SULog(SULogLevelError, @"Failed to construct URL from bundle path: %@", host.bundlePath); if (error != NULL) { *error = [NSError errorWithDomain:SUSparkleErrorDomain code:SUInstallationError userInfo:@{ NSLocalizedDescriptionKey: @"Failed to perform installation because a path could not be constructed for the old installation" }]; } return NO; } if (progress) { progress(2/11.0); } NSURL *tempNewDirectoryURL; if (!_newAndOldBundlesOnSameVolume) { // Create a temporary directory for our new app that resides on our destination's volume // We use oldURL here instead of installationURL because in the case of normalization, installationURL may not exist // And we don't want to use either of the URL's parent directories because the parent directory could be on a different volume tempNewDirectoryURL = [fileManager makeTemporaryDirectoryAppropriateForDirectoryURL:oldURL error:error]; if (tempNewDirectoryURL == nil) { return NO; } _temporaryNewDirectory = tempNewDirectoryURL; } else { tempNewDirectoryURL = nil; } if (progress) { progress(3/11.0); } // Move the new app to our temporary directory if needed NSURL *newFinalURL; if (!_newAndOldBundlesOnSameVolume) { NSString *newURLLastPathComponent = newURL.lastPathComponent; newFinalURL = [tempNewDirectoryURL URLByAppendingPathComponent:newURLLastPathComponent]; NSError *newTempMoveError = nil; if (![fileManager moveItemAtURL:newURL toURL:newFinalURL error:&newTempMoveError]) { if (error != NULL) { NSMutableDictionary *userInfo = [NSMutableDictionary dictionaryWithDictionary:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Failed to move the new app from %@ to its temp directory at %@", newURL.path, newFinalURL.path] }]; if (newTempMoveError != nil) { userInfo[NSUnderlyingErrorKey] = newTempMoveError; } *error = [NSError errorWithDomain:SUSparkleErrorDomain code:SUInstallationError userInfo:userInfo]; } return NO; } } else { newFinalURL = newURL; } if (progress) { progress(4/11.0); } if (!_newAndOldBundlesOnSameVolume) { // If we're updating a bundle on another volume, the install process can be pretty slow. // In this case let's get out of the way and skip the Gatekeeper scan [self _performInitialInstallationWithFileManager:fileManager oldBundleURL:oldURL newBundleURL:newFinalURL performGatekeeperScan:NO progressBlock:progress]; } if (progress) { progress(9/11.0); } // First try swapping the application atomically NSError *swapError = nil; BOOL swappedApp; // If we can not safely perform an atomic swap, or if the app is normalized and the installation path differs, go through the old swap path if (!_canPerformSafeAtomicSwap || (SPARKLE_NORMALIZE_INSTALLED_APPLICATION_NAME && ![oldURL.path isEqual:installationURL.path])) { swappedApp = NO; } else { // We will be cleaning up the temporary directory later in -performCleanup: // We don't want to clean it up now because it can take some time swappedApp = [fileManager swapItemAtURL:installationURL withItemAtURL:newFinalURL error:&swapError]; } if (!swappedApp) { // Otherwise swap out the old and new applications using the legacy path if (swapError != nil) { SULog(SULogLevelDefault, @"Invoking fallback from failing to replace original item with error: %@", swapError); } // Create a temporary directory for our old app that resides on its volume NSURL *tempOldDirectoryURL = [fileManager makeTemporaryDirectoryAppropriateForDirectoryURL:oldURL error:error]; if (tempOldDirectoryURL == nil) { return NO; } _temporaryOldDirectory = tempOldDirectoryURL; if (progress) { progress(10/11.0); } NSString *oldURLFilename = oldURL.lastPathComponent; if (oldURLFilename == nil) { // this really shouldn't happen.. SULog(SULogLevelError, @"Failed to retrieve last path component from old URL: %@", oldURL.path); if (error != NULL) { *error = [NSError errorWithDomain:SUSparkleErrorDomain code:SUInstallationError userInfo:@{ NSLocalizedDescriptionKey: @"Failed to perform installation because the last path component of the old installation URL could not be constructed." }]; } return NO; } // Move the old app to the temporary directory NSURL *oldTempURL = [tempOldDirectoryURL URLByAppendingPathComponent:oldURLFilename]; NSError *oldMoveError = nil; if (![fileManager moveItemAtURL:oldURL toURL:oldTempURL error:&oldMoveError]) { if (error != NULL) { NSMutableDictionary *userInfo = [NSMutableDictionary dictionaryWithDictionary:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Failed to move the old app at %@ to a temporary location at %@", oldURL.path, oldTempURL.path] }]; if (oldMoveError != nil) { userInfo[NSUnderlyingErrorKey] = oldMoveError; } *error = [NSError errorWithDomain:SUSparkleErrorDomain code:SUInstallationError userInfo:userInfo]; } return NO; } if (progress) { progress(10.5/11.0); } // Move the new app to its final destination NSError *installMoveError = nil; if (![fileManager moveItemAtURL:newFinalURL toURL:installationURL error:&installMoveError]) { if (error != NULL) { NSMutableDictionary *userInfo = [NSMutableDictionary dictionaryWithDictionary:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Failed to move new app at %@ to final destination %@", newFinalURL.path, installationURL.path] }]; if (installMoveError != nil) { userInfo[NSUnderlyingErrorKey] = installMoveError; } *error = [NSError errorWithDomain:SUSparkleErrorDomain code:SUInstallationError userInfo:userInfo]; } // Attempt to restore our old app back the way it was on failure [fileManager moveItemAtURL:oldTempURL toURL:oldURL error:NULL]; return NO; } } if (progress) { progress(11/11.0); } return YES; } - (BOOL)performInitialInstallation:(NSError * __autoreleasing *)error { // Prevent malicious downgrades // Note that we may not be able to do this for package installations, hence this code being done here NSString *hostVersion = [_host version]; NSBundle *bundle = [NSBundle bundleWithPath:_bundlePath]; SUHost *updateHost = [[SUHost alloc] initWithBundle:bundle]; NSString *updateVersion = [updateHost objectForInfoDictionaryKey:(__bridge NSString *)kCFBundleVersionKey ofClass:NSString.class]; id comparator = [[SUStandardVersionComparator alloc] init]; if (!updateVersion || [comparator compareVersion:hostVersion toVersion:updateVersion] == NSOrderedDescending) { if (error != NULL) { NSString *errorMessage = [NSString stringWithFormat:@"For security reasons, updates that downgrade version of the application are not allowed. Refusing to downgrade app from version %@ to %@. Aborting update.", hostVersion, updateVersion]; *error = [NSError errorWithDomain:SUSparkleErrorDomain code:SUDowngradeError userInfo:@{ NSLocalizedDescriptionKey: errorMessage }]; } return NO; } SUFileManager *fileManager = [[SUFileManager alloc] init]; BOOL updateHasCustomUpdateSecurityPolicy = NO; if (@available(macOS 13.0, *)) { // If the new update is notarized / developer ID code signed and Autoupdate is not signed with the same Team ID as the new update, // then we may run into Privacy & Security prompt issues from the OS // which think we are modifying the update (but we're not) // To avoid these, we skip the gatekeeper scan and skip performing an atomic swap during install // If the update has a custom update security policy, the same team ID policy may not apply, // so in that case we will also skip performing an atomic swap updateHasCustomUpdateSecurityPolicy = updateHost.hasUpdateSecurityPolicy; if (updateHasCustomUpdateSecurityPolicy) { // We don't handle working around a custom update security policy _canPerformSafeAtomicSwap = NO; } else { NSString *installerTeamIdentifier = [SUCodeSigningVerifier teamIdentifierFromMainExecutable]; NSString *bundleTeamIdentifier = [SUCodeSigningVerifier teamIdentifierAtURL:bundle.bundleURL]; // If bundleTeamIdentifier is nil, then the update isn't code signed so atomic swap is okay _canPerformSafeAtomicSwap = (bundleTeamIdentifier == nil || (installerTeamIdentifier != nil && [installerTeamIdentifier isEqualToString:bundleTeamIdentifier])); } } else { _canPerformSafeAtomicSwap = YES; } if (!_canPerformSafeAtomicSwap) { if (updateHasCustomUpdateSecurityPolicy) { SULog(SULogLevelDefault, @"Skipping atomic rename/swap and gatekeeper scan because new update %@ has a custom NSUpdateSecurityPolicy", bundle.bundleURL.lastPathComponent); } else { SULog(SULogLevelDefault, @"Skipping atomic rename/swap and gatekeeper scan because Autoupdate is not signed with same identity as the new update %@", bundle.bundleURL.lastPathComponent); } } _newAndOldBundlesOnSameVolume = [fileManager itemAtURL:bundle.bundleURL isOnSameVolumeItemAsURL:_host.bundle.bundleURL]; // We can do a lot of the installation work ahead of time if the new app update does not need to be copied to another volume if (_newAndOldBundlesOnSameVolume) { [self _performInitialInstallationWithFileManager:fileManager oldBundleURL:_host.bundle.bundleURL newBundleURL:bundle.bundleURL performGatekeeperScan:_canPerformSafeAtomicSwap progressBlock:NULL]; } return YES; } - (BOOL)performFinalInstallationProgressBlock:(nullable void(^)(double))cb error:(NSError *__autoreleasing*)error { // Note: we must do most installation work in the third stage due to relying on our application sitting in temporary directories. // It must not be possible for our update to sit in temporary directories for a very long time. return [self startInstallationToURL:[NSURL fileURLWithPath:_installationPath] fromUpdateAtURL:[NSURL fileURLWithPath:_bundlePath] withHost:_host progressBlock:cb error:error]; } - (void)performCleanup { SUFileManager *fileManager = [[SUFileManager alloc] init]; if (_temporaryNewDirectory != nil) { [fileManager removeItemAtURL:_temporaryNewDirectory error:NULL]; } if (_temporaryOldDirectory != nil) { [fileManager removeItemAtURL:_temporaryOldDirectory error:NULL]; } } @end ================================================ FILE: Autoupdate/SUSignatureVerifier.h ================================================ // // SUSignatureVerifier.h // Sparkle // // Created by Andy Matuschak on 3/16/06. // Copyright 2006 Andy Matuschak. All rights reserved. // // Includes code by Zach Waldowski on 10/18/13. // Copyright 2014 Big Nerd Ranch. Licensed under MIT. // // Includes code from Plop by Mark Hamlin. // Copyright 2011 Mark Hamlin. Licensed under BSD. // #ifndef SUDSAVERIFIER_H #define SUDSAVERIFIER_H #import @class SUSignatures; @class SUPublicKeys; @class SPUVerifierInformation; NS_ASSUME_NONNULL_BEGIN #ifndef BUILDING_SPARKLE_TESTS #define SUSignatureVerifierDefinitionAttribute SPU_OBJC_DIRECT_MEMBERS #else #define SUSignatureVerifierDefinitionAttribute __attribute__((objc_runtime_name("SUTestSignatureVerifier"))) #endif SUSignatureVerifierDefinitionAttribute @interface SUSignatureVerifier : NSObject // Helper for verifying downloaded updates + (BOOL)validatePath:(NSString *)path withSignatures:(SUSignatures *)signatures withPublicKeys:(SUPublicKeys *)pkeys verifierInformation:(SPUVerifierInformation * _Nullable)verifierInformation error:(NSError * __autoreleasing *)error; - (instancetype)init NS_UNAVAILABLE; - (instancetype)initWithPublicKeys:(SUPublicKeys *)pkeys; // Used for verifying downloaded updates - (BOOL)verifyFileAtPath:(NSString *)path signatures:(SUSignatures *)signatures verifierInformation:(SPUVerifierInformation * _Nullable)verifierInformation error:(NSError * __autoreleasing *)error; // Used for verifying non-update data (like release notes) - (BOOL)verifyData:(NSData *)data signatures:(SUSignatures *)signatures fileKind:(NSString *)fileKind verifierInformation:(SPUVerifierInformation * _Nullable)verifierInformation error:(NSError * __autoreleasing *)error; @end NS_ASSUME_NONNULL_END #endif ================================================ FILE: Autoupdate/SUSignatureVerifier.m ================================================ // // SUDSAVerifier.m // Sparkle // // Created by Andy Matuschak on 3/16/06. // Copyright 2006 Andy Matuschak. All rights reserved. // // Includes code by Zach Waldowski on 10/18/13. // Copyright 2014 Big Nerd Ranch. Licensed under MIT. // // Includes code from Plop by Mark Hamlin. // Copyright 2011 Mark Hamlin. Licensed under BSD. // #import "SUSignatureVerifier.h" #import "SULog.h" #import "SUSignatures.h" #import "SUErrors.h" #import "SPUVerifierInformation.h" #import "SUConstants.h" #if SPARKLE_BUILD_LEGACY_DSA_SUPPORT #include #endif #import "ed25519.h" #include "AppKitPrevention.h" @implementation SUSignatureVerifier { SUPublicKeys *_pubKeys; } + (BOOL)validatePath:(NSString *)path withSignatures:(SUSignatures *)signatures withPublicKeys:(SUPublicKeys *)pkeys verifierInformation:(SPUVerifierInformation * _Nullable)verifierInformation error:(NSError * __autoreleasing *)error { SUSignatureVerifier *verifier = [(SUSignatureVerifier *)[self alloc] initWithPublicKeys:pkeys]; return [verifier verifyFileAtPath:path signatures:signatures verifierInformation:verifierInformation error:error]; } - (instancetype)initWithPublicKeys:(SUPublicKeys *)pubkeys { self = [super init]; if (self != nil) { _pubKeys = pubkeys; } return self; } #if SPARKLE_BUILD_LEGACY_DSA_SUPPORT - (SecKeyRef)dsaSecKeyRef SPU_OBJC_DIRECT { NSData *data = [_pubKeys.dsaPubKey dataUsingEncoding:NSASCIIStringEncoding]; if (!self || !data.length) { SULog(SULogLevelError, @"Could not read public DSA key"); return nil; } SecExternalFormat format = kSecFormatOpenSSL; SecExternalItemType itemType = kSecItemTypePublicKey; CFArrayRef items = NULL; OSStatus status = SecItemImport((__bridge CFDataRef)data, NULL, &format, &itemType, (SecItemImportExportFlags)0, NULL, NULL, &items); if (status != errSecSuccess || !items) { if (items) { CFRelease(items); } SULog(SULogLevelError, @"Public DSA key could not be imported: %d", status); return nil; } SecKeyRef dsaPubKeySecKey = nil; if (format == kSecFormatOpenSSL && itemType == kSecItemTypePublicKey && CFArrayGetCount(items) == 1) { // Seems silly, but we can't quiet the warning about dropping CFTypeRef's const qualifier through // any manner of casting I've tried, including interim explicit cast to void*. The -Wcast-qual // warning is on by default with -Weverything and apparently became more noisy as of Xcode 7. #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wcast-qual" dsaPubKeySecKey = (SecKeyRef)CFRetain(CFArrayGetValueAtIndex(items, 0)); #pragma clang diagnostic pop } CFRelease(items); return dsaPubKeySecKey; } #endif - (BOOL)verifyFileAtPath:(NSString *)path signatures:(SUSignatures *)signatures verifierInformation:(SPUVerifierInformation * _Nullable)verifierInformation error:(NSError * __autoreleasing *)error { // Data is used only in the case where ed25519 signature is present NSData *data; if (signatures.ed25519SignatureStatus != SUSigningInputStatusPresent) { data = nil; } else { NSError *dataError = nil; data = [NSData dataWithContentsOfFile:path options:NSDataReadingMappedIfSafe error:&dataError]; if (data == nil || data.length == 0) { SULog(SULogLevelError, @"Failed to load file %@: %@", path, dataError); if (error != NULL) { NSMutableDictionary *userInfo = [NSMutableDictionary dictionary]; userInfo[NSLocalizedDescriptionKey] = [NSString stringWithFormat:@"Failed to load file: %@", path]; if (dataError != nil) { userInfo[NSUnderlyingErrorKey] = dataError; } *error = [NSError errorWithDomain:SUSparkleErrorDomain code:SUValidationError userInfo:[userInfo copy]]; } return NO; } } return [self verifyData:data signatures:signatures fileKind:@"update" fromPath:path verifierInformation:verifierInformation error:error]; } - (BOOL)verifyData:(NSData *)data signatures:(SUSignatures *)signatures fileKind:(NSString *)fileKind verifierInformation:(SPUVerifierInformation * _Nullable)verifierInformation error:(NSError * __autoreleasing *)error { return [self verifyData:data signatures:signatures fileKind:fileKind fromPath:nil verifierInformation:verifierInformation error:error]; } // Note the path must be provided for verifying update archives - (BOOL)verifyData:(NSData * _Nullable)data signatures:(SUSignatures *)signatures fileKind:(NSString *)fileKind fromPath:(NSString * _Nullable)path verifierInformation:(SPUVerifierInformation * _Nullable)verifierInformation error:(NSError * __autoreleasing *)error SPU_OBJC_DIRECT { if (!signatures) { if (error != NULL) { *error = [NSError errorWithDomain:SUSparkleErrorDomain code:SUValidationError userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"No signatures given to verify data for %@", fileKind] }]; } return NO; } switch (_pubKeys.ed25519PubKeyStatus) { case SUSigningInputStatusAbsent: if (signatures.ed25519SignatureStatus != SUSigningInputStatusAbsent) { SULog(SULogLevelDefault, @"The %@ has an EdDSA signature, but it won't be used, because the old app doesn't have an EdDSA public key", fileKind); } break; case SUSigningInputStatusInvalid: if (signatures.ed25519SignatureStatus != SUSigningInputStatusAbsent) { NSString *message = [NSString stringWithFormat:@"The %@ has an EdDSA signature, but the app has an invalid EdDSA public key, so the %@ will automatically be rejected.", fileKind, fileKind]; SULog(SULogLevelError, @"%@", message); if (error != NULL) { *error = [NSError errorWithDomain:SUSparkleErrorDomain code:SUValidationError userInfo:@{ NSLocalizedDescriptionKey: message }]; } return NO; } SULog(SULogLevelDefault, @"The app has an invalid EdDSA public key, but there is no EdDSA signature in the %@. Falling back to DSA.", fileKind); break; case SUSigningInputStatusPresent: switch (signatures.ed25519SignatureStatus) { case SUSigningInputStatusAbsent: { NSString *message = [NSString stringWithFormat:@"The app has an EdDSA public key, but there is no EdDSA signature in the %@, so the %@ will be rejected.", fileKind, fileKind]; SULog(SULogLevelError, @"%@", message); if (error != NULL) { *error = [NSError errorWithDomain:SUSparkleErrorDomain code:SUValidationError userInfo:@{ NSLocalizedDescriptionKey: message }]; } return NO; } case SUSigningInputStatusInvalid: { NSString *message = [NSString stringWithFormat:@"The %@ has an EdDSA signature, but it's invalid, so the %@ will automatically be rejected.", fileKind, fileKind]; SULog(SULogLevelError, @"%@", message); if (error != NULL) { *error = [NSError errorWithDomain:SUSparkleErrorDomain code:SUValidationError userInfo:@{ NSLocalizedDescriptionKey: message }]; } return NO; } case SUSigningInputStatusPresent: { assert(data != nil); if (ed25519_verify(signatures.ed25519Signature, (const unsigned char *)data.bytes, data.length, _pubKeys.ed25519PubKey)) { SULog(SULogLevelDefault, @"OK: EdDSA signature is correct for %@", fileKind); #if SPARKLE_BUILD_LEGACY_DSA_SUPPORT // No need to check DSA when EdDSA verification succeeded, unless a DSA signature is provided and it's // erroneously invalid if (signatures.dsaSignatureStatus != SUSigningInputStatusInvalid) #endif { return YES; } } else { NSMutableString *message = [NSMutableString stringWithFormat:@"EdDSA signature does not match. Data of the %@ being checked is different than data that has been signed, or the public key and the private key are not from the same set.", fileKind]; // Elaborate on the error message if we have more information about the download archive // If there is verifierInformation it must be for a downloaded update (rather than feed or release notes) if (verifierInformation != nil) { BOOL reportedDiscrepancy = NO; NSString *downloadedFileDescription = (path != nil) ? [NSString stringWithFormat:@"%@ (%@)", fileKind, path.lastPathComponent] : fileKind; if (verifierInformation.expectedContentLength > 0 && verifierInformation.actualContentLength > 0) { if (verifierInformation.expectedContentLength != verifierInformation.actualContentLength) { [message appendFormat:@" The downloaded %@ is likely different than the signed file because the expected content length from the appcast item (%llu bytes) differs from the downloaded file length (%llu bytes).", downloadedFileDescription, verifierInformation.expectedContentLength, verifierInformation.actualContentLength]; reportedDiscrepancy = YES; } } NSString *actualVersion = verifierInformation.actualVersion; if (actualVersion != nil && ![verifierInformation.expectedVersion isEqualToString:actualVersion]) { [message appendFormat:@" The downloaded %@ also has a CFBundleVersion (%@) which differs from the %@ in the appcast item (%@).", downloadedFileDescription, actualVersion, SUAppcastAttributeVersion, verifierInformation.expectedVersion]; reportedDiscrepancy = YES; } if (!reportedDiscrepancy && verifierInformation.expectedContentLength > 0) { [message appendFormat:@" The downloaded %@ is likely not signed correctly because the file has the expected content length (%llu bytes)%@ which matches the appcast item.", downloadedFileDescription, verifierInformation.actualContentLength, (actualVersion == nil ? @"" : [NSString stringWithFormat:@" and CFBundleVersion (%@)", actualVersion])]; } } SULog(SULogLevelError, @"%@", [message copy]); #if SPARKLE_BUILD_LEGACY_DSA_SUPPORT // Legacy DSA verification is only applicable if archive file path is provided if (signatures.dsaSignatureStatus != SUSigningInputStatusAbsent && path != nil) { SULog(SULogLevelDefault, @"DSA signature won't be checked, because EdDSA verification has already failed"); } #endif if (error != NULL) { *error = [NSError errorWithDomain:SUSparkleErrorDomain code:SUValidationError userInfo:@{ NSLocalizedDescriptionKey: message }]; } return NO; } } } break; } #if SPARKLE_BUILD_LEGACY_DSA_SUPPORT // Legacy DSA verification is only for downloaded updates switch (_pubKeys.dsaPubKeyStatus) { case SUSigningInputStatusAbsent: if (signatures.dsaSignatureStatus != SUSigningInputStatusAbsent && path != nil) { SULog(SULogLevelDefault, @"The update has a DSA signature, but it can't be used, because the old app doesn't have a DSA public key"); } break; case SUSigningInputStatusInvalid: if (signatures.dsaSignatureStatus != SUSigningInputStatusAbsent && path != nil) { // We will have already logged an error for this failure when the public key was read in, so just do an informational log here. NSString *message = @"The update has a DSA signature, but the app has an invalid DSA public key, so the update will automatically be rejected."; SULog(SULogLevelError, @"%@", message); if (error != NULL) { *error = [NSError errorWithDomain:SUSparkleErrorDomain code:SUValidationError userInfo:@{ NSLocalizedDescriptionKey: message }]; } return NO; } SULog(SULogLevelDefault, @"The app has an invalid DSA public key, but there is no DSA signature in the update."); break; case SUSigningInputStatusPresent: switch (signatures.dsaSignatureStatus) { case SUSigningInputStatusAbsent: SULog(SULogLevelError, @"There is no DSA signature in the update"); break; case SUSigningInputStatusInvalid: { NSString *message = @"The update has a DSA signature, but it's invalid, so the update will automatically be rejected."; SULog(SULogLevelError, @"%@", message); if (error != NULL) { *error = [NSError errorWithDomain:SUSparkleErrorDomain code:SUValidationError userInfo:@{ NSLocalizedDescriptionKey: message }]; } return NO; } case SUSigningInputStatusPresent: { NSInputStream *dataInputStream = (path != nil) ? [NSInputStream inputStreamWithFileAtPath:(NSString * _Nonnull)path] : nil; return [self verifyDSASignatureOfStream:dataInputStream dsaSignature:signatures.dsaSignature error:error]; } } } #else switch (_pubKeys.dsaPubKeyStatus) { case SUSigningInputStatusAbsent: break; case SUSigningInputStatusInvalid: // We don't keep track of DSA signatures, so we will ignore this mistake and treat it as if it were absent break; case SUSigningInputStatusPresent: if (error != NULL) { *error = [NSError errorWithDomain:SUSparkleErrorDomain code:SUValidationError userInfo:@{ NSLocalizedDescriptionKey: @"The old app has a DSA public key but DSA support is disabled, and the old app does not have an EdDSA public key." }]; } return NO; } #endif if (error != NULL) { // Use generic failure *error = [NSError errorWithDomain:SUSparkleErrorDomain code:SUValidationError userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"EdDSA and DSA verification for the %@ has failed", fileKind] }]; } return NO; } #if SPARKLE_BUILD_LEGACY_DSA_SUPPORT - (BOOL)verifyDSASignatureOfStream:(NSInputStream *)stream dsaSignature:(NSData *)dsaSignature error:(NSError * __autoreleasing *)outError SPU_OBJC_DIRECT { if (!stream || !dsaSignature) { SULog(SULogLevelError, @"Invalid arguments to verifyStream"); if (outError != NULL) { *outError = [NSError errorWithDomain:SUSparkleErrorDomain code:SUValidationError userInfo:@{ NSLocalizedDescriptionKey: @"Invalid arguments to verifyStream" }]; } return NO; } SecKeyRef dsaPubKeySecKey = [self dsaSecKeyRef]; if (!dsaPubKeySecKey) { if (outError != NULL) { *outError = [NSError errorWithDomain:SUSparkleErrorDomain code:SUValidationError userInfo:@{ NSLocalizedDescriptionKey: @"Failed to create DSA Sec Key Ref" }]; } return NO; } // Sparkle's DSA support is deprecated #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdeprecated-declarations" __block SecGroupTransformRef group = SecTransformCreateGroupTransform(); __block SecTransformRef dataReadTransform = NULL; __block SecTransformRef dataDigestTransform = NULL; __block SecTransformRef dataVerifyTransform = NULL; __block CFErrorRef error = NULL; BOOL (^cleanup)(void) = ^{ if (group) CFRelease(group); if (dataReadTransform) CFRelease(dataReadTransform); if (dataDigestTransform) CFRelease(dataDigestTransform); if (dataVerifyTransform) CFRelease(dataVerifyTransform); if (error) CFRelease(error); if (dsaPubKeySecKey) CFRelease(dsaPubKeySecKey); return NO; }; dataReadTransform = SecTransformCreateReadTransformWithReadStream((__bridge CFReadStreamRef)stream); if (!dataReadTransform) { if (outError != NULL) { *outError = [NSError errorWithDomain:SUSparkleErrorDomain code:SUValidationError userInfo:@{ NSLocalizedDescriptionKey: @"File containing update archive could not be read (failed to create SecTransform for input stream)" }]; } return cleanup(); } dataDigestTransform = SecDigestTransformCreate(kSecDigestSHA1, CC_SHA1_DIGEST_LENGTH, NULL); if (!dataDigestTransform) { if (outError != NULL) { *outError = [NSError errorWithDomain:SUSparkleErrorDomain code:SUValidationError userInfo:@{ NSLocalizedDescriptionKey: @"File containing update archive could not be read (failed to create SecDigest for input stream)" }]; } return cleanup(); } dataVerifyTransform = SecVerifyTransformCreate(dsaPubKeySecKey, (__bridge CFDataRef)dsaSignature, &error); if (!dataVerifyTransform) { SULog(SULogLevelError, @"Could not understand format of the signature: %@; Signature data: %@", error, dsaSignature); if (outError != NULL) { NSError *underlyingError = (__bridge NSError *)error; NSMutableDictionary *userInfo = [NSMutableDictionary dictionary]; userInfo[NSLocalizedDescriptionKey] = [NSString stringWithFormat:@"Could not understand format of the signature data %@", dsaSignature]; if (underlyingError != NULL) { userInfo[NSUnderlyingErrorKey] = underlyingError; } *outError = [NSError errorWithDomain:SUSparkleErrorDomain code:SUValidationError userInfo:[userInfo copy]]; } return cleanup(); } #pragma clang diagnostic pop #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdeprecated-declarations" SecTransformConnectTransforms(dataReadTransform, kSecTransformOutputAttributeName, dataDigestTransform, kSecTransformInputAttributeName, group, &error); #pragma clang diagnostic pop if (error) { if (outError != NULL) { *outError = [NSError errorWithDomain:SUSparkleErrorDomain code:SUValidationError userInfo:@{NSLocalizedDescriptionKey : @"Failed to connect data read transform", NSUnderlyingErrorKey: (__bridge NSError *)error}]; } return cleanup(); } #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdeprecated-declarations" SecTransformConnectTransforms(dataDigestTransform, kSecTransformOutputAttributeName, dataVerifyTransform, kSecTransformInputAttributeName, group, &error); #pragma clang diagnostic pop if (error) { if (outError != NULL) { *outError = [NSError errorWithDomain:SUSparkleErrorDomain code:SUValidationError userInfo:@{NSLocalizedDescriptionKey : @"Failed to connect data digest transform", NSUnderlyingErrorKey: (__bridge NSError *)error}]; } return cleanup(); } #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdeprecated-declarations" NSNumber *result = CFBridgingRelease(SecTransformExecute(group, &error)); #pragma clang diagnostic pop if (error) { SULog(SULogLevelError, @"DSA signature verification failed: %@", error); if (outError != NULL) { *outError = [NSError errorWithDomain:SUSparkleErrorDomain code:SUValidationError userInfo:@{ NSLocalizedDescriptionKey: @"DSA signature verification failed", NSUnderlyingErrorKey: (__bridge NSError *)error}]; } return cleanup(); } if (!result.boolValue) { if (outError != NULL) { *outError = [NSError errorWithDomain:SUSparkleErrorDomain code:SUValidationError userInfo:@{ NSLocalizedDescriptionKey: @"DSA signature does not match. Data of the update file being checked is different than data that has been signed, or the public key and the private key are not from the same set"}]; } } cleanup(); return result.boolValue; } #endif @end ================================================ FILE: Autoupdate/SUStatusInfoProtocol.h ================================================ // // SUStatusInfoProtocol.h // Sparkle // // Created by Mayur Pawashe on 7/10/16. // Copyright © 2016 Sparkle Project. All rights reserved. // #import NS_ASSUME_NONNULL_BEGIN @protocol SUStatusInfoProtocol - (void)probeStatusInfoWithReply:(void (^)(NSData * _Nullable installationInfoData))reply; - (void)probeStatusConnectivityWithReply:(void (^)(void))reply; @end NS_ASSUME_NONNULL_END ================================================ FILE: Autoupdate/SUUnarchiver.h ================================================ // // SUUnarchiver.h // Sparkle // // Created by Andy Matuschak on 3/16/06. // Copyright 2006 Andy Matuschak. All rights reserved. // #import NS_ASSUME_NONNULL_BEGIN @protocol SUUnarchiverProtocol; SPU_OBJC_DIRECT_MEMBERS @interface SUUnarchiver : NSObject + (nullable id )unarchiverForPath:(NSString *)path extractionDirectory:(NSString *)extractionDirectory updatingHostBundlePath:(nullable NSString *)hostPath decryptionPassword:(nullable NSString *)decryptionPassword expectingInstallationType:(NSString *)installationType; @end NS_ASSUME_NONNULL_END ================================================ FILE: Autoupdate/SUUnarchiver.m ================================================ // // SUUnarchiver.m // Sparkle // // Created by Andy Matuschak on 3/16/06. // Copyright 2006 Andy Matuschak. All rights reserved. // #import "SUUnarchiver.h" #import "SUUnarchiverProtocol.h" #import "SUPipedUnarchiver.h" #import "SUDiskImageUnarchiver.h" #import "SUBinaryDeltaUnarchiver.h" #import "SUFlatPackageUnarchiver.h" #include "AppKitPrevention.h" @implementation SUUnarchiver + (nullable id )unarchiverForPath:(NSString *)path extractionDirectory:(NSString *)extractionDirectory updatingHostBundlePath:(nullable NSString *)hostPath decryptionPassword:(nullable NSString *)decryptionPassword expectingInstallationType:(NSString *)installationType { if ([SUPipedUnarchiver canUnarchivePath:path]) { return [[SUPipedUnarchiver alloc] initWithArchivePath:path extractionDirectory:extractionDirectory]; } else if ([SUDiskImageUnarchiver canUnarchivePath:path]) { return [[SUDiskImageUnarchiver alloc] initWithArchivePath:path extractionDirectory:extractionDirectory decryptionPassword:decryptionPassword]; } else if ([SUBinaryDeltaUnarchiver canUnarchivePath:path]) { assert(hostPath != nil); NSString *nonNullHostPath = hostPath; return [[SUBinaryDeltaUnarchiver alloc] initWithArchivePath:path extractionDirectory:extractionDirectory updateHostBundlePath:nonNullHostPath]; } #if SPARKLE_BUILD_PACKAGE_SUPPORT else if ([SUFlatPackageUnarchiver canUnarchivePath:path]) { // Flat packages are only supported for guided packaage installs return [[SUFlatPackageUnarchiver alloc] initWithFlatPackagePath:path extractionDirectory:extractionDirectory expectingInstallationType:installationType]; } #endif return nil; } @end ================================================ FILE: Autoupdate/SUUnarchiverNotifier.h ================================================ // // SUUnarchiverNotifier.h // Sparkle // // Created by Mayur Pawashe on 12/21/16. // Copyright © 2016 Sparkle Project. All rights reserved. // #import NS_ASSUME_NONNULL_BEGIN SPU_OBJC_DIRECT_MEMBERS @interface SUUnarchiverNotifier : NSObject - (instancetype)initWithCompletionBlock:(void (^)(NSError * _Nullable))completionBlock progressBlock:(void (^ _Nullable)(double))progressBlock; - (void)notifySuccess; - (void)notifyFailureWithError:(NSError * _Nullable)reason; - (void)notifyProgress:(double)progress; @end NS_ASSUME_NONNULL_END ================================================ FILE: Autoupdate/SUUnarchiverNotifier.m ================================================ // // SUUnarchiverNotifier.m // Sparkle // // Created by Mayur Pawashe on 12/21/16. // Copyright © 2016 Sparkle Project. All rights reserved. // #import "SUUnarchiverNotifier.h" #import "SULocalizations.h" #import "SUErrors.h" #include "AppKitPrevention.h" @implementation SUUnarchiverNotifier { void (^_completionBlock)(NSError * _Nullable); void (^ _Nullable _progressBlock)(double); } - (instancetype)initWithCompletionBlock:(void (^)(NSError * _Nullable))completionBlock progressBlock:(void (^ _Nullable)(double))progressBlock { self = [super init]; if (self != nil) { _completionBlock = [completionBlock copy]; _progressBlock = [progressBlock copy]; } else { assert(false); } return self; } - (void)notifySuccess { dispatch_async(dispatch_get_main_queue(), ^{ self->_completionBlock(nil); }); } - (void)notifyFailureWithError:(NSError * _Nullable)reason { NSMutableDictionary *userInfo = [NSMutableDictionary dictionaryWithObject:@"An error occurred while extracting the archive. Please try again later." forKey:NSLocalizedDescriptionKey]; if (reason) { [userInfo setObject:(NSError * _Nonnull)reason forKey:NSUnderlyingErrorKey]; } NSError *error = [NSError errorWithDomain:SUSparkleErrorDomain code:SUUnarchivingError userInfo:userInfo]; dispatch_async(dispatch_get_main_queue(), ^{ self->_completionBlock(error); }); } - (void)notifyProgress:(double)progress { if (_progressBlock != nil) { dispatch_async(dispatch_get_main_queue(), ^{ self->_progressBlock(progress); }); } } @end ================================================ FILE: Autoupdate/SUUnarchiverProtocol.h ================================================ // // SUUnarchiverProtocol.h // Sparkle // // Created by Mayur Pawashe on 3/26/16. // Copyright © 2016 Sparkle Project. All rights reserved. // #import NS_ASSUME_NONNULL_BEGIN @protocol SUUnarchiverProtocol + (BOOL)mustValidateBeforeExtraction; - (void)unarchiveWithCompletionBlock:(void (^)(NSError * _Nullable))completionBlock progressBlock:(void (^ _Nullable)(double))progressBlock waitForCleanup:(BOOL)waitForCleanup; @property (nonatomic, readonly) BOOL needsVerifyBeforeExtractionKey; - (NSString *)description; @end NS_ASSUME_NONNULL_END ================================================ FILE: Autoupdate/StatusInfo.h ================================================ // // StatusInfo.h // Sparkle // // Created by Mayur Pawashe on 7/10/16. // Copyright © 2016 Sparkle Project. All rights reserved. // #import #import "SUStatusInfoProtocol.h" NS_ASSUME_NONNULL_BEGIN SPU_OBJC_DIRECT_MEMBERS @interface StatusInfo : NSObject - (instancetype)initWithHostBundleIdentifier:(NSString *)bundleIdentifier; @property (nonatomic, nullable) NSData *installationInfoData; - (void)startListener; - (void)invalidate; @end NS_ASSUME_NONNULL_END ================================================ FILE: Autoupdate/StatusInfo.m ================================================ // // StatusInfo.m // Sparkle // // Created by Mayur Pawashe on 7/10/16. // Copyright © 2016 Sparkle Project. All rights reserved. // #import "StatusInfo.h" #import "SPUMessageTypes.h" #include "AppKitPrevention.h" #define REPLY_STATUS_INFO_TIMEOUT 2 @interface StatusInfo () @end @implementation StatusInfo { NSXPCListener *_xpcListener; NSMutableDictionary *_pendingReplies; NSUInteger _pendingReplyCounter; } @synthesize installationInfoData = _installationInfoData; - (instancetype)initWithHostBundleIdentifier:(NSString *)bundleIdentifier { self = [super init]; if (self != nil) { _xpcListener = [[NSXPCListener alloc] initWithMachServiceName:SPUStatusInfoServiceNameForBundleIdentifier(bundleIdentifier)]; _xpcListener.delegate = self; _pendingReplies = [NSMutableDictionary dictionary]; } return self; } - (void)startListener { [_xpcListener resume]; } - (void)invalidate { [_xpcListener invalidate]; _xpcListener = nil; } - (BOOL)listener:(NSXPCListener *)__unused listener shouldAcceptNewConnection:(NSXPCConnection *)newConnection { newConnection.exportedInterface = [NSXPCInterface interfaceWithProtocol:@protocol(SUStatusInfoProtocol)]; newConnection.exportedObject = self; [newConnection resume]; return YES; } - (void)setInstallationInfoData:(NSData *)installationInfoData { _installationInfoData = installationInfoData; // Respond to all of our pending replies for (NSNumber *replyKey in _pendingReplies) { void (^replyBlock)(NSData * _Nullable) = self->_pendingReplies[replyKey]; replyBlock(_installationInfoData); } [_pendingReplies removeAllObjects]; } - (void)probeStatusInfoWithReply:(void (^)(NSData * _Nullable))reply { dispatch_async(dispatch_get_main_queue(), ^{ if (self->_installationInfoData != nil) { reply(self->_installationInfoData); } else { // If we don't have the installation info data currently, we may receive it in a very short window afterwards // In this case wait a bit for the reply. If we receive the data it will be in -setInstallationInfoData: NSUInteger currentReplyCounter = self->_pendingReplyCounter; self->_pendingReplyCounter++; NSNumber *currentReplyCounterKey = @(currentReplyCounter); self->_pendingReplies[currentReplyCounterKey] = [reply copy]; dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(REPLY_STATUS_INFO_TIMEOUT * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ void (^replyBlock)(NSData * _Nullable) = self->_pendingReplies[currentReplyCounterKey]; if (replyBlock != nil) { replyBlock(self->_installationInfoData); [self->_pendingReplies removeObjectForKey:currentReplyCounterKey]; } }); } }); } - (void)probeStatusConnectivityWithReply:(void (^)(void))reply { reply(); } @end ================================================ FILE: Autoupdate/main.m ================================================ #import #import "AppInstaller.h" #include "AppKitPrevention.h" int main(int __unused argc, const char __unused *argv[]) { @autoreleasepool { NSArray *args = [[NSProcessInfo processInfo] arguments]; if (args.count != 4) { return EXIT_FAILURE; } NSString *hostBundleIdentifier = args[1]; NSString *homeDirectory = args[2]; NSString *userName = args[3]; AppInstaller *appInstaller = [[AppInstaller alloc] initWithHostBundleIdentifier:hostBundleIdentifier homeDirectory:homeDirectory userName:userName]; [appInstaller start]; // Ignore SIGTERM because we are going to catch it ourselves signal(SIGTERM, SIG_IGN); // Ignore SIGPIPE because we won't want read or write failures due to broken pipe to unexpectably // terminate the process (e.g, when extracting archives or performing package installs). signal(SIGPIPE, SIG_IGN); dispatch_source_t sigtermSource = dispatch_source_create(DISPATCH_SOURCE_TYPE_SIGNAL, SIGTERM, 0, dispatch_get_main_queue()); dispatch_source_set_event_handler(sigtermSource, ^{ // Don't clear the update directory because the installer may be in middle of installing an update // We still need to set an event handler for receiving SIGTERM though, otherwise our job may not terminate // (This is also recommended from the developer documentation). // Simply exit with SIGTERM if we receive this signal exit(SIGTERM); }); dispatch_resume(sigtermSource); [[NSRunLoop currentRunLoop] run]; } return EXIT_SUCCESS; } ================================================ FILE: BinaryDelta/Bridging-Header.h ================================================ // // Use this file to import your target's public headers that you would like to expose to Swift. // #import "SUBinaryDeltaApply.h" #import "SUBinaryDeltaCreate.h" #import "SPUDeltaArchive.h" #import "SPUDeltaArchiveProtocol.h" ================================================ FILE: BinaryDelta/main.swift ================================================ // // main.swift // BinaryDelta // // Created by Mayur Pawashe on 1/3/22. // Copyright © 2022 Sparkle Project. All rights reserved. // import Foundation import ArgumentParser // Create a patch from an old and new bundle struct Create: ParsableCommand { @Option(name: .long, help: ArgumentHelp("The major version of the patch to generate. Defaults to the latest stable version. Older versions will need to be specified for updating from applications using older versions of Sparkle.", valueName: "version")) var version: Int = Int(SUBinaryDeltaMajorVersionDefault.rawValue) @Flag(name: .customLong("verbose"), help: ArgumentHelp("Enable logging of the changes being archived into the generated patch.")) var verbose: Bool = false @Option(name: .long, help: ArgumentHelp(COMPRESSION_METHOD_ARGUMENT_DESCRIPTION, valueName: "compression")) var compression: String = "default" @Option(name: .long, help: .hidden) var compressionLevel: UInt8 = 0 @Argument(help: ArgumentHelp("Path to original bundle to create a patch from.")) var beforeTree: String @Argument(help: ArgumentHelp("Path to new bundle to create a patch from.")) var afterTree: String @Argument(help: ArgumentHelp("Path to new patch file to create.")) var patchFile: String func validate() throws { var validCompression: ObjCBool = false let compressionMode = deltaCompressionModeFromDescription(compression, &validCompression) guard validCompression.boolValue else { fputs("Error: unrecognized compression \(compression)\n", stderr) throw ExitCode(1) } switch compressionMode { case SPUDeltaCompressionModeDefault: fallthrough case .none: guard compressionLevel == 0 else { fputs("Error: compression level must be 0 for compression \(compression)\n", stderr) throw ExitCode(1) } break case .bzip2: guard compressionLevel >= 0 && compressionLevel <= 9 else { fputs("Error: compression level \(compressionLevel) is not valid.\n", stderr) throw ExitCode(1) } break case .LZMA: fallthrough case .LZFSE: fallthrough case .LZ4: fallthrough case .ZLIB: guard version >= 3 else { fputs("Error: version \(version) patch files do not support compression \(compression)\n", stderr) throw ExitCode(1) } guard compressionLevel == 0 else { fputs("Error: compression level provided must be 0 for compression \(compression)\n", stderr) throw ExitCode(1) } break @unknown default: fputs("Error: unrecognized compression \(compression)\n", stderr) throw ExitCode(1) } guard version >= SUBinaryDeltaMajorVersionFirst.rawValue else { fputs("Error: version provided \(version) is not valid.\n", stderr) throw ExitCode(1) } guard version >= SUBinaryDeltaMajorVersionFirstSupported.rawValue else { fputs("Error: creating version \(version) patches is no longer supported.\n", stderr) throw ExitCode(1) } guard version <= SUBinaryDeltaMajorVersionLatest.rawValue else { fputs("Error: this program is too old to create a version \(version) patch, or the version number provided is invalid.\n", stderr) throw ExitCode(1) } let fileManager = FileManager.default var isDirectory: ObjCBool = false if !fileManager.fileExists(atPath: beforeTree, isDirectory: &isDirectory) || !isDirectory.boolValue { fputs("Error: before-tree must be a directory\n", stderr) throw ExitCode(1) } if !fileManager.fileExists(atPath: afterTree, isDirectory: &isDirectory) || !isDirectory.boolValue { fputs("Error: after-tree must be a directory\n", stderr) throw ExitCode(1) } } func run() throws { let compressionMode = deltaCompressionModeFromDescription(compression, nil) guard let majorDeltaVersion = SUBinaryDeltaMajorVersion(rawValue: UInt16(version)) else { // We shouldn't reach here fputs("Error: failed to retrieve major version from provided version: \(version)\n", stderr) throw ExitCode(1) } var createDiffError: NSError? = nil if !createBinaryDelta(beforeTree, afterTree, patchFile, majorDeltaVersion, compressionMode, compressionLevel, verbose, &createDiffError) { if let error = createDiffError { fputs("\(error.localizedDescription)\n", stderr) } else { fputs("Error: Failed to create patch due to unknown reason.\n", stderr) } throw ExitCode(1) } } } // Apply a patch from an old bundle to generate a new bundle struct Apply: ParsableCommand { static let configuration = CommandConfiguration(abstract: "Apply a patch against an original bundle to generate a new bundle.") @Flag(name: .customLong("verbose"), help: ArgumentHelp("Enable logging of changes being applied from the patch.")) var verbose: Bool = false @Argument(help: ArgumentHelp("Path to original bundle to patch.")) var beforeTree: String @Argument(help: ArgumentHelp("Path to new bundle to create.")) var afterTree: String @Argument(help: ArgumentHelp("Path to patch file to apply.")) var patchFile: String func validate() throws { let fileManager = FileManager.default var isDirectory: ObjCBool = false if !fileManager.fileExists(atPath: beforeTree, isDirectory: &isDirectory) || !isDirectory.boolValue { fputs("Error: before-tree must be a directory\n", stderr) throw ExitCode(1) } if !fileManager.fileExists(atPath: patchFile, isDirectory: &isDirectory) || isDirectory.boolValue { fputs("Error: patch-file must be a file\n", stderr) throw ExitCode(1) } } func run() throws { var applyDiffError: NSError? if (!applyBinaryDelta(beforeTree, afterTree, patchFile, verbose, { _ in }, &applyDiffError)) { if let error = applyDiffError { fputs("\(error.localizedDescription)\n", stderr) } else { fputs("Error: patch failed to apply for unknown reason\n", stderr) } throw ExitCode(1) } } } // Output information for BinaryDelta or from a patch file struct Info: ParsableCommand { @Argument(help: ArgumentHelp("Path to patch file to extract information from.")) var patchFile : String? func run() throws { if let patchFile = patchFile { // Print version of patch file var header: SPUDeltaArchiveHeader? = nil let archive = SPUDeltaArchiveReadPatchAndHeader(patchFile, &header) if let error = archive.error { fputs("Error: Unable to open patch \(patchFile): \(error.localizedDescription)\n", stderr) throw ExitCode(1) } if let header = header { if header.majorVersion < SUBinaryDeltaMajorVersionFirst.rawValue { fputs("Error: major version \(header.majorVersion) is invalid.\n", stderr) throw ExitCode(1) } fputs("Patch version \(header.majorVersion).\(header.minorVersion)", stdout) // Print out compression info only if it's available // We can't get compression info from version 2 files if header.compression != SPUDeltaCompressionModeDefault, let compressionDescription = deltaCompressionStringFromMode(header.compression) { fputs(" using \(compressionDescription) compression", stdout) // Compression level isn't available or applicable for all formats if header.compressionLevel != 0 { fputs(" with level \(header.compressionLevel)", stdout) } } fputs("\n", stdout) } else { fputs("Error: Failed to retrieve header due to unknown reason.\n", stderr) throw ExitCode(1) } } else { // Print version of program fputs("BinaryDelta version \(SUBinaryDeltaMajorVersionLatest.rawValue).\(latestMinorVersionForMajorVersion(SUBinaryDeltaMajorVersionLatest))\n", stdout) } } } struct BinaryDelta: ParsableCommand { static let configuration = CommandConfiguration(commandName: "BinaryDelta", abstract: "Create and apply small and efficient delta patches between an old and new version of a bundle.", subcommands: [Create.self, Apply.self, Info.self]) } DispatchQueue.global().async(execute: { BinaryDelta.main() CFRunLoopStop(CFRunLoopGetMain()) }) CFRunLoopRun() ================================================ FILE: CHANGELOG ================================================ # 2.9.0 * Add basic markdown support for release notes (requires macOS 12+) including [customizing its presentation](https://sparkle-project.org/documentation/publishing#adapting-markdown-or-plain-text-release-notes) (#2810, #2817) (Zorg) * Add support for [signing and verifying appcast feeds](https://sparkle-project.org/documentation#signing-feeds-optional) (#2822, #2828) (Zorg) * Add [sparkle:hardwareRequirements](https://sparkle-project.org/documentation/publishing#minimum-system-version-requirements) for enforcing an Apple silicon (arm64) requirement (#2797) (Zorg) * Add [sparkle:minimumUpdateVersion](https://sparkle-project.org/documentation/publishing/#upgrading-to-newer-features) for specifying a minimum version an app needs to be on before upgrading (#2811) (Zorg) * Add API annotations for Swift concurrency (#2827) (Zorg) * Validate Obj-C class when reading objects from user defaults / Info plists (#2782) (Zorg) * Download temporary files in-memory using NSURLSessionDataTask (#2825) (Zorg) * Allow [impatient update check interval](https://sparkle-project.org/documentation/customization/) to be configured for updates that are downloaded automatically (#2799) (Zorg) * Probe agent & status service as soon as we launch it to reduce timeout issues (#2852) (Zorg) * Add `allowsAutomaticUpdates` property to determine if automatic downloading/installing of updates option should be enabled (#2809) (Zorg) * Add Vietnamese translation (#2816, #2839) (TranPhuong319) * Add missing nn language to Installer progress Info.plist (#2818) (Zorg) * Improve German localization (#2847) (Marco Hillger) * Find non-canonical Sparkle.framework locations in generate_appcast when creating delta updates to determine compatibility (#2833) (Zorg) * Make Debug builds of Sparkle use same time interval settings as Release (#2805) (Zorg) * Remove sparkle-cli from the binary distribution (#2826) (Zorg) * Make generate_appcast deltas order stable and thread-safe (#2848) (Nathan Manceaux-Panot) * Fix Xcode 26.4 beta compiler warnings (#2850) (Zorg) # 2.8.1 * Enforce RunAtLoad to reduce potential timeout issue when launching updater task (#2795) (Zorg) * Add missing executable bit permission warnings on connection failure (#2792) (Zorg) * Add missing localizations to zh-CN & zh-TW (#2789, #2791) (Francis Feng) * Add documentation note for delegates being weakly referenced (#2802) (Zorg) * Include app name in startUpdater: failure in SPUStandardUpdaterController (#2780) (Zorg) # 2.8.0 * UI modernization and macOS Tahoe support * Modernize update alert and release notes UI (#2737) (Zorg, Noah Nuebling, Cykelero, Daniel Jalkut, Peter Nowell) * Update retrieving app icon to work better in Tahoe (#2742) (Zorg) * Improve retrieval of main app icon for authorization dialog (#2743) (Zorg) * Delta updates * Improve bsdiff performance by preventing excessive iterations when processing similar data blocks (#2693) (Will Fairclough) * Fix an issue while searching a cloneable file for delta updates (#2748, #2753) (Vincent Bénony, Zorg) * Add support for relative URLs for delta updates (#2741) (jj) * Localization * Set STRINGS_FILE_OUTPUT_ENCODING build setting to "binary" (#2712) (Nicolas Kick) * Move all localizations to main Sparkle.strings (#2760) (Zorg) * Synchronize updater settings with user defaults to fix out-of-sync UI state (#2728) (Zorg) * Document and better enforce main thread only requirement for using Sparkle methods (#2746, #2754, #2768)) (Sebastien Marchand, Zorg) * Make -[SPUUserDriver showUpdateInFocus] optional (#2717) (Zorg) * Add private module map for framework (#2722) (Zorg) * Workaround a corner case in which the bundle path of a running application contains Contents/MacOS/Executable (#2726, #2747) (Jeremy Huddleston Sequoia, Zorg) * Disable false dependency scan analysis warnings when building Sparkle from source (#2762) (Daniel Jalkut) * Refactor the logic for avoiding re-sending the system profile more frequently than once a week (#2720) (Daniel Jalkut) * Remove deprecated interactive package installer type (#2767) (Zorg) # 2.7.3 * Double quote team identifiers in requirement strings to fix crash if Team ID starts with number (#2766) (Zorg) # 2.7.2 * Harden policy on what operations clients are allowed to take (#2763) (Zorg) # 2.7.1 * Fix typo in NN localisation (#2694) (Sjur N Moshagen) * Fix compiler warnings for Xcode 16.3 (#2709) (Zorg) * Fix Sparkle not building when SPARKLE_COPY_LOCALIZATIONS=0 (#2707) (Zorg) * Fix release notes constraints when compiled with macOS 26 SDK (#2730) (Zorg) * Fix reserved identifier warnings for Xcode 26 (#2729) (Zorg) # 2.7.0 * Unarchiver / validation improvements (Zorg) * Remove old checksum verification checks for dmg archives to improve extraction speed (#2568) (Zorg) * Skip extracting auxiliary files and improve extraction progress for disk images (#2569) (Zorg) * Improve robustness around extracting dmg's with passwords (#2627, #2571) (Zorg) * Randomize the download archive name the installer extracts/executes (#2584) (Zorg) * Retry extracting zip file without piping if extraction fails to workaround bug prior to macOS 15 (#2616) (Zorg) * Add opt-in SUVerifyUpdateBeforeExtraction option to force verification of updates before extraction (#2667) (Zorg) * Add support for extracting Apple Archives (.aar files; requires SUVerifyUpdateBeforeExtraction, macOS 10.15+) (#2586, #2588, #2590) (Zorg) * Don't allow removal of signing keys more strictly (#2647) (Zorg) * Remove SPARKLE_BUILD_DMG_SUPPORT option (#2690) (Zorg) * Add new BinaryDelta format (version 4) (Zorg) * Preserve bundle creation date when creating and applying delta updates (#2583) (Zorg) * Use faster crc32 hashes for binary delta version 4 (#2638) (Zorg) * Make binary delta version 4 the default (#2668) (Zorg) * Language / layout improvements * Fix recovery error suggestion not shown when app is translocated or on read-only mount (#2689) (Zorg) * Fix typo in Dutch localisation (#2642) (Eitot) * Add baseline alignment to status text in SUStatus dialog (#2587) (Eitot) * Make horizontal hugging priority required for status text field (#2614) (Zorg) * Adjust the layout of anonymous system profile info to align better with the rest of the panel's UI (#2564) (Daniel Jalkut) * Fix typo in Dutch localisation (#2642) (Eitot) * Internationalize system profile display keys (#2577) (Zorg) * Update hebrew locale and add right-to-left characters (#2573, #2576, #2578, #2579) (Shlomo) * Update localisations for Dutch and German (#2582) (Eitot) * Add unlocalized strings in Japanese (#2589) (1024jp) * Fix typo in LICENSE (#2648) (fujisoft) * Deprecate custom version comparators (#2639) (Zorg) * Skip preflight update check in sparkle-cli if user is root (#2645) (Zorg) * Avoid assert/crash when app is moved before update alert shows (#2658) (Zorg) * Use default NSURLRequest timeoutInterval for the downloader (currently 60s) (#2673) (Zorg) * Fix process substitution failing to work for providing the private key as file argument (#2615) (Zorg) * Improve unable decode private key error messages in generate_appcast (#2675) (Zorg) * Clarify that default channel must be in allowed channels set in API documentation (#2676) (Zorg) * Call update permission prompt delegate method only when needed (#2622) (Zorg) * Resolve duplicate class definitions from BinaryDelta, Sparkle Test App, and unit tests (#2570, #2629) (Zorg) # 2.6.4 * Fix app modification prompt from appearing when downloaded update overrides NSUpdateSecurityPolicy (#2593) # 2.6.3 * Guard update timer update check against sessionInProgress to fix rare crash when checking for updates (#2561) * Remove extra writeData: call when unarchiving disk images (#2562) * Ignore crashes due to SIGPIPE in generate_appcast when failing to extract zip files (#2563) # 2.6.2 * Create and use temp extraction directory in generate_appcast again (#2555) (Zorg) # 2.6.1 * Extract archives in a separate directory from the input archive (#2550) (Zorg) * Fix the release notes WebKit view not updating background when transitioning from light to dark mode (#2542) (Zorg) * Add NN (Norwegian Nynorsk) locale (#2532) (Sjur N Moshagen, Zorg) * Create tar.xz files with built-in tar and remove bzip2 fallback for creating a release distribution (#2535) (Zorg) * Add fallback in case SULocalizedStringFromTableInBundle() fails (#2533) (Zorg) * Remove assert on download response being available fixing rare crash (#2547) (Zorg) * Clarify when authoriation prompt may show in SPUUserDriver documentation (#2531, #2534) (Zorg) * Fix typos in codebase (#2537) (Viktor Szépe) # 2.6.0 * Perform Gatekeeper scan to pre-warm app launch (#2505) (Zorg) * Disable sandboxing for the Downloader XPC service by default to fix downloader prompt warnings (#2511) (Zorg) * Store private seed as the secret for newly generated keys (#2472) (Zorg) * Improve signing error message to developers if they serve the wrong update file (#2471) (Zorg) * Prevent app modification warnings from external updaters (like sparkli-cli) by improving installation (#2516) (Zorg) * Update Korean localization (#2504) (CheolHyun Mun) * Use $PROJECT_DIR instead of $SRCROOT (#2489) (Zorg) * Set Package.swift minimum deployment to macOS 10.13 (#2481) (Eitot) * Fix false positive analyzer warning about resumableUpdate type (#2454) (Zorg) # 2.5.2 * Don't clean up update directory when Autoupdate receives SIGTERM (#2479) (Zorg) * Update Japanese localization (#2475) (1024jp) * Improve Turkish translations (#2464) (Emir SARI) * Update Spanish translation for 'You are currently running version %@.' and 'Version History' (#2463) (Billy Gray) # 2.5.1 * Default to English for XML nodes when no xml:lang is present (#2440) (Zorg) * Filter for archive files in generate_appcast more intelligently (#2448) (Zorg) * Use correct entitlements and dsym files when using custom bundle id and XPC names in ConfigCommon (#2446) (floorish) # 2.5.0 * Add ability to adapt release notes based on the currently installed version (#2373) (Nathan Manceaux-Panot) * Allow developers to use custom URL schemes in the release notes view (#2393) (Zorg) * Adopt cooperative app activation APIs in macOS 14 Sonoma (#2409) (Zorg) * Improve permission prompt layout (#2420) (Zorg) * Remove hyphenation in "You're up to date" message (#2425) (Zorg, Dom Neill) * Pre-warm installs before relaunch and resolve sporadic failures in CI (#2421) (Zorg) * Fix make release not building distribution successfully (#2430) (Zorg) * Fix Updater app not starting when running Sparkle as root (e.g. from CLI with sudo or a daemon) on macOS 14 Sonoma (#2432) (Zorg) * Fix KVO usage for updaterController.updater.* (#2404) (Zorg) * Replace CFUUID* with NSUUID (#2395) (Eitot) * Report an error when detecting duplicate updates in generate_appcast (#2407) (Zorg) * Improve error for rejecting xattr based code signing for delta updates (#2408) (Zorg) * Fail gracefully when auxiliary tool cannot be located (#2436) (Zorg) # 2.4.2 * Ignore release notes download when we shouldn't show release notes (#2381) (Zorg) * Fix NSKeyedUnarchiver warning for not specifying keyed NSString class (#2381) (Zorg) * Harden verification of Sparkle update download (#2392) (Zorg) # 2.4.1 * Remove auxiliary apps and relocate symbols in SPM package to resolve missing AvailableLibraries warnings (#2356) (Zorg) * Add -Wno-declaration-after-statement to silence warnings that only apply to pre-C99 (#2345) (Kent Sutherland) * Fix compile error when setting SPARKLE_EMBED_DOWNLOADER_XPC_SERVICE=0 (#2346) (Zorg) * Improve how downloaded update is passed to the installer (#2359) (Zorg) # 2.4.0 * Reduce code size * Enable deployment postprocessing in Release to properly strip debug symbols and strip all non-global symbols (#2286, #2305) (Zorg) * Remove duplicate Sparkle localization strings in Updater app (#2288) (Zorg) * Optimize codebase for generated code size (less properties, direct methods, etc) (#2305) (Zorg) * Add additional settings to `ConfigCommon.xcconfig` for disabling features * Ask permission for automatically downloading and installing new updates (#2285) (Zorg) * Add support for plain text release notes view that does not use web view (#2315) (Zorg) * Update `SUVersionDisplay` to better customize and display how versions are shown (#2321) (Zorg) * Add deprecations/warnings for incorrect background update checking usage (#2295) (Zorg) * Deprecate `-[SPUUpdater setFeedURL:]` API and add `-[SPUUpdater clearFeedURLFromUserDefaults]` for migrating away from `-[SPUUpdater setFeedURL:]` (#2295) * Trigger a new update check in `-[SPUUpdater resetUpdateCycle]` if the updater's feed or allowed channels have changed (#2324) (Zorg) * Exit with an error if generate_appcast cannot sign an update that must be signed (#2322) (Zorg) * Remove a redundant Apple code signing check when verifying new updates (#2341) (Zorg) * Turn off auto-linking for XPC targets to alleviate circular dependency issues (#2332) (Daniel Jalkut) # 2.3.2 * Fix potential crash during download if appcast item includes invalid enclosure URL (#2317) (Zorg) * Add delegate method to hide showing version history option (#2303) (Zorg) * Finnish localization grammar fixes (#2311) (Lauri-Matti Parppei) # 2.3.1 * Fix update permission alert title text overlapping with question text in some languages (#2284) (Zorg) * Log the URL that failed to download correctly (#2296) (Zorg) * Update Czech translation (#2275) (Sam) * Add zh_HK (Chinese, Hong Kong) localization (#2273) (Bing Zheung) # 2.3.0 * Bump minimum deployment target to macOS 10.13 (#2196) (Zorg) * Remove and preserve necessary updates in generate_appcast (#2218) (Zorg) * Move old update files no longer needed to old_updates/ in archive directory in generate_appcast (#2228) (Zorg) * Expose maximum-versions option (per branch) in generate_appcast to preserve in the feed (#2259) (Zorg) * Add -p option to sign_update to only print the signed signature (#2268) (Zorg) * Hide automatic install check box when allowsAutomaticUpdates is disabled (#2202) (Jie) * Allow developer to always force allowing automatic updates (#2266) (Zorg) * Fix update window going haywire during resize when release notes are hidden by disabling resizing (#2200) (Zorg) * Allow user to re-try installing/relaunching application when quit is delayed/cancelled (#2234) (Zorg) * Add delta update attributes for validating an app hasn't been stripped from removed localizations or architectures (#2219) (Zorg) * Fix delta patches not applying edge case if files from source that need to be diffed are not writeable (#2211) (Zorg) * Add delegate callback when user makes choice to install, dismiss, or skip an update (#2250) (Zorg) * Reject serving updates with DSA only and no EdDSA (#2167) (Zorg) * Find potential matching running apps that are translocated in the updater agent (#2233) (Zorg) * Expose -[SPUStandardUserDriver activeUpdateAlert] as private API (#2255) (Zorg) * Add zh_CN l10n for "Version History" (#2247) (kakaiikaka) * Update zh_TW localization (#2271) (Chiahong) * Remove ed25519 git submodule in favor of including the dependency directly (#2244) (Zorg) * Clean up left-over code (#2243) (Eitot) * Update Package.resolved (#2245) (Zorg) * Remove model translation table for system profiling (#2188) (Zorg) * Improve documentation usage for SPUUpdater properties (#2256) (Zorg) This release improves generate_appcast by automatically removing updates that are no longer needed in the generated appcast, and moving old update files to old_updates/ in the archives directory. New delta update attributes are also added to let Sparkle know when to skip downloading delta updates if the application has been stripped. macOS 10.13 or later is now required (due to Xcode 14 dropping support for deploying to older OS versions). If you're not generating appcasts automatically, remember to add `10.13` element to ``s in your appcast. # 2.2.2 * Critical update alerts may not show up as promptly as they should when they are being automatically installed (#2230) (Zorg) * Remove module imports from framework headers (#2217) (Zorg) * Update Portuguese localization (#2224) (Kent Sutherland) * Update zh_TW localization (#2246) (Chiahong) # 2.2.1 * Make scroll bar dark in dark theme for Release Notes by default (#2187) (Pavel Moiseenko) * Fix memory leaks when using generate_appcast (#2193) (Zorg) * Update Italian localization (#2192) (VinBoiSoft) # 2.2.0 * Update Focus Improvements (#924) * Present new scheduled updates in utmost focus only at opportune times (Zorg) * For regular apps opportune times are: app launch, app re-activation, and system being idle (without a power assertion being held to prevent display sleep). * For background (dockless) apps opportune time is just app launch. Otherwise, the update is now shown behind other applications and windows, instead of previously stealing focus from other active apps. * Add APIs and documentation for adding gentle update reminders to compliment Sparkle's standard user interface (Zorg) (#2122) * Allow status window to be minimizable for regular app installs (Zorg) (#2100) * Center status window and inherit key focus from the previously shown update alert window (Zorg) * Activate app when checking for updates if the app is not currently active (e.g, from a menu bar extra menu item) (Zorg) * Fix issue where bringing status window to front made other active windows exit in macOS Ventura's Stage Manager (#2153) (Zorg) * Fix showing update in focus not bringing the "checking for updates" window in focus (Zorg) (#2150) * Rename XPC Service filenames for Sandboxing to show more friendly human-readable name in authorization dialog (Zorg) (#2096) * Add support for running the framework and sparkle-cli as root (Zorg) (#2119) * Fix issue where update cycle may not complete in unusual configuration if automatic checks are disabled + automatic downloading is enabled + install requires user interaction (Zorg) (#2133) * Synchronize usage of XPC connections to main queue to fix potential race conditions (Zorg) (#2178) * Update last update check time when choosing to install an update & relaunch (Zorg) (#2136) * Improve error reporting in the framework and sparkle-cli when installation fails with no write permission (Zorg) (#2157) * Use displayVersionString instead of versionString for OS version mismatch error message (samschott) (#2138) * Make displayVersionString non-null and update fallback documentation (Zorg) (#2139) * Ignore custom icons set via resource forks when applying delta updates (Zorg) (#2114) * Fall back to regular update if delta update fails to download (Zorg) (#2118) * Skip downloading delta updates when application has been moved to a file system (like FAT) that doesn't support regular permission modes (Zorg) (#2148) * Bump initial installer message timeouts and declare daemon/agents processes as Interactive (Zorg) (#2162) * Add and improve translation strings and update pt-BR (BR Lingo) (#2094) * Update localisations (Eitot) (#2113) * Update Greek localizations (seitsme) (#2184, #2185) * Update Japanese localization (1024jp) (#2182) * Replace deprecated code with newer APIs (Eitot) (#2112) * Remove obsolete fallbacks for older OS versions (Eitot) (#2110) * Remove SPUURLRequest (Zorg) (#2124) * Silence ivar deprecation warnings (Zorg) (#2099) * Fix Xcode 14 project warnings (Zorg) (#2147, #2179) * Deprecate -s flag and add --ed-key-file option to generate_appcast (Zorg) (#2170) * Update text for external licenses (Zorg) (#2164) This update renames the bundled XPC Services, brings improvements to notifying users of new updates without disrupting their focus, and adds [gentle update reminder APIs](https://sparkle-project.org/documentation/gentle-reminders/) to further customize how Sparkle's standard user interface delivers new update alerts. The `-s` flag for passing a raw private EdDSA key to sign_update and generate_appcast is now deprecated. If you were using this option previously, please see the help pages of these tools for more information. # 2.1.0 * New Binary Delta format (version 3) * Features a custom and more efficient container format, migrating away from the deprecated xar format (Zorg) (#2051) * Adds delta compression options for lzma, bzip2, zlib, lzfse, lz4, and no compression (Zorg) (#2051) * Changes default delta format compression from bzip2 (in version 2) to lzma (in version 3) resulting in smaller deltas (Zorg) (#2051) * Preserve file system (HFS+/apfs) level compression when applying delta updates (Zorg) (#2084) * Tracks renames and binary diffs for files that have moved around to new locations using intelligent heuristics (Zorg) (#2051, #2053) * Added more unit tests, UI tests, and generate_appcast/BinaryDelta tools support for the new format (Zorg) (#2052, #2054) * Major upgrade improvements (#2070) * Fix skipping a major version to not skip subsequent major versions (Zorg) (#2079) * Add sparkle:belowVersion element for informational updates (Zorg) (#2080) * Add option to allow developers to ignore/reset user skipped upgrades (Zorg) (#2081) * Fix progress bar and button alignment for checking updates (Zorg) (#2066) * This deprecates `-[SPUUserDriver showInstallingUpdate]` and `-[SPUUserDriver showSendingTerminationSignal]` in favor for `-[SPUUserDriver showInstallingUpdateWithApplicationTerminated:]` * Fix unsteady progress when installing updates (Zorg) (#2072) * Check http statusCode in didFinishDownloadingToURL (Eric Shapiro, Zorg) (#2049, #2073) * Use strcoll_l() for locale-independent comparisons for delta updates (Dan Rigdon-Bel) (#2087) * Fix version compare not treating '2.1.0' and '2.1' as being equal (Zorg) (#2065) * Add verify and account options for signing updates (Zorg) (#2074) * Add delta patch and Apple code signing verification in generate_appcast (Zorg) (#2076, #2077) * Use more modern NSSecureCoding APIs when available (Zorg) (#2058) * Use more modern FileManager APIs for copying files (Zorg) (#2059) * Fix make release failing when customizing XPC_SERVICE_BUNDLE_ID_PREFIX (Zorg) (#2060) * Preserve the Entitlements directory in podspec (digitalMoksha) (#2062) * Add hidden option to generate_appcast to set max CDATA threshold (Zorg) (#2075) This update introduces a new major format for [delta updates](https://sparkle-project.org/documentation/delta-updates/), which migrates away from deprecated APIs (xar) and creates smaller patches. If you don't use `generate_appcast`, please check the [compatibility notes for creating delta updates](https://sparkle-project.org/documentation/delta-updates/#compatibility). # 2.0.0 * Support for Sandboxed Applications (Zorg) * Support for writing custom user interfaces (Zorg) * Support for updating external Sparkle-based bundles (Zorg) * Added command line utility to update Sparkle-based bundles (Zorg) * Modern architecture * Moves extraction, validation, and installation into a submitted launchd agent/daemon with XPC communication (Zorg) * Features faster installs with shorter update/relaunch times (#1802) (Zorg) * Provides more robust installs when user authorization is needed (Zorg) * Adoption of improved atomic-safe updates leveraging APFS (#1801) (Zorg) * API Changes * Introduced new SPUStandardUpdaterController, SPUUpdater, SPUUserDriver classes/protocols (Zorg) * Decoupled AppKit and UI logic in the framework from core functionality (Zorg) * Ensure (most) API compatibility with Sparkle 1; you can likely test Sparkle 2 in an existing app with little to no changes (Zorg) * Deprecated SUUpdater, albeit it is still functional for testing and transitional purposes (Zorg) * `-bestValidUpdateInAppcast:forUpdater:` delegate method behavior has been refined and discouraged for some cases. Please review its updated header documentation in `SPUUpdaterDelegate.h` if you use this method. (#1838, #1862, #1879, #1880) (Zorg) * Delegation methods may have been removed or added to the newer updater API. Please review `SPUUpdaterDelegate` if using `SPUUpdater`. (Zorg) * Updater Changes * Automatic silent and manual update alert prompts are now merged together (Zorg) * Updates will attempt to install even if the user quits the application without relaunching the application update explicitly (Zorg) * Updates can be downloaded in the background automatically but later prompt the user to install them, particularly if Sparkle doesn't have sufficient permission to install them without the user's permission (Zorg) * Authorization now occurs before launching the installer and before terminating the application, which can be canceled by the user cleanly (Zorg) * Sparkle uses the icon of the bundle to update for its authorization dialog. A 32x32 image representation of the icon is needed. (Zorg) * Sudden termination for silent automatic updates isn't disabled anymore (Zorg) * Policy Changes * Non-bare package based updates that are zipped or archived must add sparkle:installationType="package" to the appcast item enclosure (this doesn't apply to bare packages which aren't archived) (Zorg) * We now recommend using sparkle:version and sparkle:shortVersionString top level elements instead of enclosure attributes (#1878) (Zorg) * The link element in an appcast item is now used for directing users to the product's website if they don't meet minimum system requirements (#1877) (Zorg) * Expose why a new update is unavailable and direct user to prior release notes or website info (#1877, #1886) (Zorg) * Add element allowing Sparkle to show a better stylized and full changelog to the user (#2001) (aONe) * Add delegate API allowing applications to show full in-app or offline version history to the user (#1989) (Billy Gray) * Major/Paid Upgrades Enhancements * Latest minor updates are preferred over major updates (specified by sparkle:minimumAutoupdateVersion) (#1850) (Zorg) * Major updates can be skipped with a user confirmation (#1853) (Zorg) * Informational only and critical updates can be specified selectively by app version (#1862) * Add support for posting updates only on specific channels (eg for supporting beta updates) (#1879) (Zorg) * System profiler privacy and transparency (#1690) (Martin Pilkington) * Support getting app icon from asset catalog (#1694) (Charles Srstka) * Don't bring up authorization just because group ID doesn't match (#1830) (Zorg) * Raise minimum system version to macOS 10.11 (Zorg) * Special thanks to developers using early builds of this release in production and contributors for keeping this running (Kornel, Jonas Zaugg, Gwynne Raskind, Jordan Rose, Tony Arnold, Bryan Jones, Christian Tietze, Jakob Egger, and many more) Please visit [Sparkle's website](http://sparkle-project.org) for more information on documentation and migration. If you are migrating from earlier beta versions of Sparkle 2 and use sandboxing, please re-familiarize yourself with the [Sandboxing guide](https://sparkle-project.org/documentation/sandboxing/). Some of the XPC Services are now optional and integration with XPC Services and code signing have been simplified. The SPUUserDriver protocol for custom user interfaces has been greatly simplified too. # 1.27.3 * Create and use temp extraction directory in generate_appcast again (#2556) (Zorg) # 1.27.2 * Extract archives in a separate directory from the input archive (#2552) (Zorg) * Fix incorrect xz log warning in make release (#2044) (trss) # 1.27.1 * Use px instead of pt when specifying font size to fix default font size in WebView from #1962 (regressed in 1.25.0 with WKWebView adoption) (Daniel Jalkut) * Fix to prevent Sparkle manipulating the host app's high level WebView defaults from #1961 (affecting only macOS versions before 10.11 in 1.x branch) (Daniel Jalkut) * Add safer handling of applying binary delta files from #1990 (Zorg) * Don't use current date unless necessary when scheduling next update from #1991 (Zorg) * Whitelist about:srcdoc as a safe web URL from #1938, #2007 (Louis Pontoise, Zorg) * Backport not hiding update window on deactivation from #2008 (Zorg) # 1.27.0 * Deprecate not using EdDSA and skip DSA verification when possible in #1892 (Zorg) * Pass http headers and user agent when downloading release notes from #1873 (Zorg) * Fix project warnings from #1893 (Zorg) * Update sample appcast from #1895 (Zorg) * Fix appcast pubDate tag generation in different locales from #1898 (Sungbin Jo) * Create valid xar archives for generating binary delta files from #1906 (Zorg) * Fix a few issues with German localization from #1931 (J-rg) * Fix issues with Russian localizations from #1947 (Konstantin Zamyakin) * Fix issues with Czech localizations from #1943 (Vojtěch Koňařík) * Add an automated workflow that builds and publishes official Sparkle releases (Tony Arnold, Zorg) # 1.26.0 * Flat package support from #1745 (Zorg) * Correct generate_appcast -s command line argument usage help (Lance Lovette) * Fix -f command line argument handling (Lance Lovette) * Fix progress for guided pkg install (Zorg) * Fix XQuartz update failing because NSLog caused issues (Zorg) * Update localization (Vojtěch Koňařík) * Various fixes to SPM logic (Jonas Zaugg) # 1.25.0 * Raise minimum system version to 10.9 (Kornel) * Allow an appcast to prevent the new version from being installed automatically (#1688) (pierswalter) * UI fixes * Show check for updates button only when automatic updates are enabled (Lorena Rangel) * Don't initialize the webview if we don't show release notes (Tobias Fonfara) * Keep skip button available if there is a minimum autoupdate version (Zorg) * Fix automatic updates checkbox state when not showing release notes (Zorg) * Fix edge cases with hiding alert buttons and using "Install Later" (Zorg) * Re-add setting WebUIDelegate fix for legacy web view (Zorg) * Adopt WKWebView for 1.x (Zorg) * Add auto layout constraints to SUAutomaticUpdateAlert window (Zorg) * Made buttons wide enough to display the full text. Made window wide enough to display the widened buttons (el, fi, ru, uk) (Piers Uso Walter) * Remove max size from update alert. (George Nachman) * Integrate generate_keys export/import options changes from #1730 (Zorg) * Add `—release-notes-url-prefix` support to localized release notes (Adam Tharani) * Translations * Enable base internalization for alert xibs (Zorg) * Capitalizes french translation of "Vous utilisez actuellement..." ("You are currently running...") (Micah Moore) * Fix and cleanup Hebrew, Catalan, Finnish localizations (Zorg) * Remove non-english localizations from TestApplication (Zorg) * Fix catch-22 Swift Package Manager binary target (Jonas Zaugg) * Add explicit dependencies to fix Xcode linking errors (Kornel) * Add phased rollouts feature to automatic update driver too (Zorg) * Remove hiding skip button and retitling later button (Zorg) * Deprecate installUpdatesIfAvailable (Zorg) * Apply custom headers to app download (Geraint White) # 1.24.0 * Prioritize UserDefaults when fetching value for EnableAutomaticChecks (Nicolas Bosi) * Improve best appcast item selection handling (Ian Bytchek) * Create lockfile for improved compatibility with 3rd party updaters (CoreCode) * Enable SPM support via binary target (Jonas Zaugg) * `generate_appcast` improvements: * follow symbolic links (Denis Dzyubenko) * update the release notes element’s URL when required (Brad Andalman) * Added output filename option to generate_appcast (Brad Andalman) * Added —release-notes-url-prefix to generate_appcast (Brad Andalman) * Command line option to provide a download url prefix is now parsed and set on each archive item (Dominik H) * Added help command line option (Dominik H) * Locale updates: * Update zh_CN localization (柳东原 · Dongyuan Liu) * Update Sparkle.strings (Vojtěch Koňařík) * Update Croatian (#1603) (milotype) * Update SUUpdateAlert.xib (DanielFirlej) * fix: read SUAutomaticallyUpdate from Info.plist (Trevor DeVore) * Fix casting Boolean to BOOL on Apple Silicon (Kevin Wojniak) * Use build matrix to test on multiple Xcode versions (Rajiv Shah) * Set UpdateAlert and AutomaticUpdateAlert window's fullscreen collectionBehavior to NSWindowCollectionBehaviorFullScreenAuxiliary to allow them to present on top of the Main App's window if it's fullscreen. (Micah Moore) * Fixed error about "about:blank" release notes (Louis Pontoise) * Support App Store URL scheme in release note webview (Bi11) * Use the SHA-256 hash of the archive as the cache path (Nate Weaver) * Don't return an optional from the FileHandle method (Nate Weaver) * Added URL+Hashing (Nate Weaver) # 1.23.0 * Support generating appcast with localizations (#1499) (Alik Vovkotrub) * Support versions with git commit SHA (#1504) (Alec Larson) * Hide "Skip..." and "Remind..." buttons when they're not relevant (#1480) (Kenneth Johnsen) * Preserve Finder tags while updating apps (#1512) (CoreCode) * Read-only update alert dialog formatting improvements (#1515) (Quinn Taylor) * Check if `SUBundleName` is set before normalizing (Jake Fenton) * `NSInteger` cast warning on Xcode 11 (Marga Keuvelaar) * Correct appcast file extension (Tom Vos) * Update Sparkle.strings (Emir Sarı) * Fix spelling (#1508) (Frank Chiarulli Jr) # 1.22.0 * Enabled "Hardened Runtime" build option for Apple's notarization requirement * Add delegate methods to suppress update alerts (George Nachman) * Improved error when running from translocated location (Michael Buckley) * Add phased rollout feature (#1381) (Fabian Jäger) * Ignore non-standard permissions in delta updates instead of failing the build (Kornel Lesiński) * Notify user when installed version is newer than the latest in the appcast (CoreCode) * Reset timers after computer sleep (CoreCode) * Block-based alternatives to `NSInvocation`-based delegate methods (Fabian Jäger) * Add delegate `userDidSkipThisVersion` (BobZombie, Leo Natan, bono yusuke) * Pass item to updaterShouldShowUpdateAlertForScheduledUpdate delegate method (George Nachman) * Support providing private key as argument in `generate_appcast` (Yakuhzi, marchyang) * Separate the ed25519 sources into a new static library (Tony Arnold) * Disambiguate signing error messages (Nate Weaver) * Use `XMLNode.Options.nodePrettyPrint` in `generate_appcast` instead of trying to add whitespace manually (fumoboy007) * Annotate SUHost for nullability (Michael Buckley) * Use SUAVAILABLE macro (Christiaan Hofman) * Fix warnings when using modules (nivekkagicom) * Correction of Czech localization inconsistency (#1403) (vojtakonarik) * BR locale fix (BR Lingo) * Update Japanese localization (fujisoft) * French Sparkle.strings (Jean-Baptiste) # 1.21.3 * Losslessly reduced the size of PNG (Barijaona Ramaholimihaso) * Catch exceptions from subcommands (Julian Mayer) * `generate_appcast` can sign any bundles instead of just apps (Nate Weaver) * Check that effectiveAppearance is being observed before calling removeObserver (Pierluigi) # 1.21.2 * Allow EdDSA for delta updates, too (Kornel) * Warning fixes (Brian Bergstrand) * Improvements to release notes view context menu and dark mode (Bi11) # 1.21.0 * Added support EdDSA (ed25519) signatures (Kornel) * DSA signatures are considered outdated, and Apple's `Security.framework` only supports weaker SHA-1-based DSA. * Both old DSA and new EdDSA are still supported (and one app can use both), but new applications should use EdDSA only, and we recommend migrating away from DSA signatures. * `generate_keys` is now a Swift tool that stores EdDSA private keys in the Keychain * Existing apps can continue using their old DSA keys, but we've dropped support for generation of old DSA keys * `sign_update` is now a Swift tool that signs using EdDSA from private keys in the Keychain * The old DSA-based signing script has been moved to `bin/old_dsa_scripts` * The old DSA-based signing script has been fixed to work on pre-10.13 systems (Thomas Tempelmann) * `generate_appcast` has been updated to support EdDSA signatures * It can sign both DSA (if `dsa_priv.pem` file is specified) and EdDSA (from Keychain) * The tool now uses Caches directory and doesn't generate unnecessary delta files * Fixed verification of delta updates on filesystems that change permissions of symlinks * Fixed `NSURLSession` leak (Michael Ehrmann) # 1.20.0 * `generate_appcast` option to read private key directly from the keychain (Tamás Lustyik) * Add delegate callbacks for finished download and extraction related events (Csaba Horony) * Don't check for updates if Do Not Disturb is on (Kornel) * Expose `CodesigningVerifier`, add codesign info API (sunuslee) * Threading fixes: * Fix potential hang with `dispatch_sync` to main thread (Brian Bergstrand) * Fix closeCheckingWindow called from background thread (Alexey Martemyanov) * Improve 'read-only' error message (#1192) (Adrian Thomas) * New Spanish localisation (Ken Arroyo Ohori) * Updated Finnish language resources (Jason Pollack) * Hungarian localization (Csaba Horony) * Log more information about authentication requests (Kornel) * Explicitly specify types to silence "Messaging unqualified id" warning that's new in Xcode 10. Removed __has_feature(objc_generics) check and use generisc to help silence the warnings. (Kent Sutherland) * Fix binary delta creation on network drives (sagecook) * Fix compilation issues on Xcode 10 with new build system (Leo Natan) # 1.19.0 * Refactoring of downloader code to avoid deprecated methods (Deadpikle) * Changes to which methods run on the main thread. Note: some delegate methods may be called on non-main thread now. (Kornel) * Update Japanese localization (1024jp) * Update Sparkle.strings (Stefan Paychère) * Fix Sparkle clients polling too frequently (Jonathan Bullard) * Handle SecTransformExecute errors (Kornel) * Silence Touch Bar availability warnings on Xcode 9 by using API_AVAILABLE. Disable gnu-zero-variadic-macro-arguments to prevent warnings from use of API_AVAILABLE. (Kent Sutherland) * 10.11 SDK compatibility (David Fuhrmann) # 1.18.1 * Add optional updaterDidRelaunchApplication: method on SUUpdaterDelegate (#1115) (App Tyrant) * Implemented sparkle:os attribute as documented (Memphiz) * Additional termination detection in case kpoll fails. (fujisoft) * Included bin files in CocoaPods installation (Keith Smiley) * Updated Dutch localization (Eitot) * Updated German localization (Eitot) * Updated Japanese translation (1024jp) * Updated Portuguese translation (Victor) * Updated to Xcode 9/Swift 4 # 1.18.0 * Name of the host app is used in authorization prompt (the `SPARKLE_RELAUNCH_TOOL_NAME` setting is now obsolete) * More detailed progress bar for package installers (Kornel Lesiński) * Disabled the keyboard shortcut for the install button for scheduled updates to avoid accidental installs. (George Nachman) * generate_appcast tool adds release notes if there's an .html file with the same base name as the archive (Brett Walker) * Added `sparkle:shortVersionString` to the enclosure, #1032 (Brett Walker) * Fixed Japanese localization (1024jp) * Fixed escaping of system profile URLs * Added more logging in various failure cases (Kornel Lesiński) * Better error message for quarantined apps that can't be updated # 1.17.0 * Added Touch Bar support (Bi11) * Upgraded SULog to use logging APIs that Apple provides built-in (Zorg) * Skip buttons are disabled if the update is marked as critical (Kornel Lesiński) * Background updates ask OS for lower-priority networking (Kornel Lesiński) * Refactorings to sync with upcoming 2.0 * Added kqueue based termination listener (Zorg) * Added AppKit prevention guards to modules that shouldn't import it (Zorg) * Added Obj-C generics where applicable (Zorg) * Made SUBundleIcon & SUApplicationInfo take SUHost, not NSBundle (Zorg) * Improved -[SUHost objectForInfoDictionaryKey:] (Zorg) * Detect and fail if any two-way dependencies exist in the project (Zorg) * generate_appcast: * fixed handling of multiple directories in an archive * percent encode the filename used in the delta url (Brett Walker) * Update Sparkle.strings (BR Lingo) * Improved handling of non-ASCII names in delta archives (Kornel Lesiński) * Don't touch Info.plist unless git version changes (Václav Slavík) # 1.16.0 * Guided package installs are now the default for updating packages (Zorg) - `pkg` installers won't show any UI. If you require the old behavior of showing a full installer window, rename the `*.pkg` file to `*.sparkle_interactive.pkg` * Previous version of the app is now deleted instead of staying in the trash (Zorg) * Added `generate_appcast` helper tool (Kornel Lesiński) * Made manual check resume pending automatic updates instead of starting a new update (Kornel Lesiński) * Started using `length` value from RSS if HTTP doesn't give one (Zorg) * Hidden automatic updates checkbox for information only updates (Bi11) * Added progressbar for DMG and binary delta extraction (Kornel Lesiński) * Fixed showing of download status if we attempt a 2nd download (Zorg) * Refactorings to sync with upcoming 2.0 * Decoupled and simplified installation code using protocols (Zorg) * Added nullability annotations (Zorg) * Allowed delegate methods that return an object to return nil (Zorg) * Decreased responsibility of SUHost and moved code into other components (Zorg) * Removed Sparkle.pch and many file #includes (Zorg) # 1.15.0 * A new icon! Thanks to 1024jp * Show alert when an update is sent over insecure HTTP with no DSA key (Zorg) - If you can't use HTTPS, you must at least sign updates with a DSA key. * Improved binary delta implementation (Zorg) * Added improved -validateMenuItem: as a method in SUUpdater.h for public use (Zorg) * Removed reachability preflight check (Zorg) * Clear update caches directory before downloading new update (Zorg) * Check the bundle's parent directory for writability too (Zorg) * Don't follow symbolic links for file operations (Zorg) * Don't bring up an authorized dialog during cleanup (Zorg) * Made Sparkle look for the highest compatible version regardless of timestamps (Zorg) * Fixed compatibility with 10.7 * Fixed crash on 10.7 - subscript operator not available (kleuter) * Fixed warnings caused by -Wpartial-availability (Zorg) * Fixed german l10n. (Sebastian Volland) * Error code for download errors (Kornel Lesiński) * Update last update check date when the update driver finishes (Zorg) * Scale app icon up if needed in Software Update window (Nicholas Riley) * Don't register for termination notifications more than once (Zorg) * Don't terminate the app if we're already terminating (Zorg) * Removed SUEnableAutomaticChecksKeyOld and SUCheckAtStartup constants (Eitot) * Updated Sparkle framework headers to use modules if modules are available (B. Kevin Hardman) * Fixed warnings, fixed uses of SULocalizedString (Jerry Krinock) * Improved signing verifier to take any host and s/application/bundle/ (Zorg) * Improved Spotlight updates after delta extraction (Zorg) # 1.14.0 (Mar 11, 2016) * Disable javascript by default and make it opt-in (Zorg) * URL-encoding of appcast URLs is preserved (Kornel Lesiński) * Delegate is asked for fallback updates if delta update fails (Kornel Lesiński) * Fixed crash on 10.7 - subscript operator not available (kleuter) * Fixed check of feed URL before delegate had a chance to set it (Kornel Lesiński) * Re-added support for password-protected dmg images (Andrew K. Boyd) * Added warning about ATS blocking (Kornel Lesiński) * Translation fixes for pt-BR. (vitu) * Add some Japanese lozalized strings (1024jp) * Made test app available in all languages #695 (LIU Dongyuan / 柳东原) * Czech localizations update (Frantisek Erben) * Removed a test resource from the framework bundle (Karl Moskowski) * Test if the updated app is the frontmost one (Zorg) * UI Tests for the Test Application (Zorg) # 1.13.1 (Jan 31, 2016) Important security fixes: * Prevent inclusion of local files via file:// XML entities * Disable redirects to non-HTTP URLs in release notes webview # 1.13.0 (Dec 18, 2015) * Changed framework's bundle ID from `org.andymatuschak.Sparkle` to `org.sparkle-project.Sparkle`. # 1.12.0 (Dec 13, 2015) * Rewritten file operations for updating an app (Zorg) - Ensuring atomic move operations, robust error handling. - Faster. - Using modern APIs where possible (no FSPathMakeRef, FSGetCatalogInfo, FSFindFolder, etc.) - Strong documentation, easier to read code. * Automatic updates won't be installed if the system is about to shut off (Zorg) * Deprecated serving over HTTP without DSA (Zorg) - Note that Apple has deprecated insecure HTTP in macOS 10.11 * Improved Autoupdate application (Zorg) * Do all the installation work after the runloop is set up * TerminationListener only does termination listening now * Handle cases where host path is not installation path and host path is not desired executable path * Don't show Autoupdate dock icon if we shouldn't show UI * Update modification & access time for new update * Added installUpdatesIfAvailable (Ian Langworth) * Removed extensions from shell scripts (Jake Petroules) * Rewritten test app so it works again, and from a local web server (Zorg) * Replaced use of Python with built-in web server (Kevin Wojniak) * Set LD_RUNPATH_SEARCH_PATHS in Podspec (Jake Petroules) * Don't install automatic updates if the system might shut off (Zorg) * Don't show Autoupdate dock icon if we shouldn't show UI (Zorg) * Updated layout constraints when removing release notes (Zorg) * Improved BinaryDelta error handling & logging (Zorg) * Refactored quarantine removal (Zorg) * Fixed German localization (1024jp) * Updated zh_CN translation (LIU Dongyuan / 柳东原) * Updated Mac models list until July 2015 (Gabriel Ulici) * Updated Polish translation (Kornel Lesiński) * Updated Xcode project languages for which we have translations (Jake Petroules) * Updated XIB files (Kornel Lesiński) * Use NSByteCountFormatter if available (Jake Petroules) * Declared protocols on SUUpdateAlert for the 10.11 SDK (Daniel Jalkut) * Silenced warning about casting away const-ness and -Wassign-enum (Daniel Jalkut) * Added script to generate a report comparing the Sparkle.strings files (Kevin Wojniak) * Check for empty strings (as well as nil) in SUHost's -name method (Karl Moskowski) * Don't follow symlinks for checking file existence (Zorg) * Unit tests in Swift (Zorg, Jake Petroules) * Fixed framework imports (Felix Schulze) * Fixed issues with copying files from different mounted drives (Zorg) * Disallowed automatic updates when user can't write to the bundle (Zorg) * Set the task working directories instead of changing the process working directory (Kevin Wojniak) # 1.11.1 (Nov 9, 2015) * Don't install automatic updates when system is about to shut down # 1.11.0 (Aug 24, 2015) * Big improvements to code signing and DSA verification - Sparkle now checks not only whether an update is correctly signed, but also whether the updated version will be able to verify future updates. Updates now must either use DSA keys correctly, or not try to use them at all. Same goes for Apple Code Signing. - Rely on code signing and the DSA key in the new app instead of appcast. If the new app has a public DSA key, then the appcast item must have a DSA signature for the app, even if the app is code signed. (Zorg) * More verbose error message when DSA keys don't match (Kornel Lesiński) * Added delegate methods for pre-download and immediately post-failed-download (Isaac Greenspan) * Fix Lucida Grande is always used for release notes (LIU Dongyuan / 柳东原) * Only remove quarantine with setResourceValue: when it's present. Fixes "Unable to quarantine: 93" messages from showing up in the console. (Zorg) * Fixed const and nullability warnings (Jake Petroules, Kornel Lesiński) * Replaced deprecated NSRunAlertPanel/alertWithMessageText (Kevin Wojniak) * Imported the Foundation umbrella header in all the public headers (C.W. Betts) * pt-BR localization update (Victor Figueiredo) * Reject unsupported code-signing xattrs in binary delta (Zorg) * Fixed crash while applying delta update (antonc27) * Added logging of appcast/download URL on error (Kornel Lesiński) * More robust reading of Autoupdate.app path from Sparkle bundle # 1.10.0 (Apr 26, 2015) * 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 (Jan 26, 2015) * 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 (Jul 26, 2014) * New SUDSAVerifier based on up-to-date macOS APIs (Zachary Waldowski) * Detailed error log for failed signature checks (Kornel Lesiński) * Converted Sparkle to ARC (C.W. Betts) * Converted ivars to properties. (Jake Petroules) * Cocoapod support (Xhacker Liu) * Quarantine removal on macOS 10.10 (C.W. Betts) * Updated Japanese localization (1024jp) * Added Greek localization # 1.7.1 (Jul 2, 2014) * Removed option to install unverified updates (Kornel Lesiński) * Added detailed log when code signing verification fails (Sam Deane) * Restored original Sparkle icon. (Jake Petroules) * Switched SUUpdateAlert.xib to AutoLayout (Kornel Lesiński) * Replace references to andymatuschak.org with sparkle-project.org. (Jake Petroules) * Several code cleanups, modernizations, fixed warnings and improved code formatting (Jake Petroules) * Make the repository significantly more organized. (Jake Petroules) * Xcode project: set organization name and class prefix. (Jake Petroules) * Link to Foundation and AppKit instead of Cocoa. (Jake Petroules) * Use new operatingSystemVersion API when available. (Jake Petroules) * Add .clang-format configuration file for source code formatting. (Jake Petroules) * Add a target to build Sparkle API documentation using Doxygen. (Jake Petroules) # 1.7.0 * Dropped support for macOS 10.6. Sparkle now supports 10.7 and newer (including 10.10 Yosemite) on 64-bit Intel Macs (the last 32-bit Mac was released in 2006). * Removed use of deprecated functions (Zachary Waldowski) * Switched to modern Obj-C runtime and new literals syntax * Removed pre-10.7 code. (C.W. Betts) * Use more Blocks/libdispatch code. (C.W. Betts) * Cleaned up and improved security of `generate_keys`/`sign_update` scripts # 1.6.1 * Removed archive password prompt (Kornel Lesiński) * (Re)fixes bug where URLs are naively double escaped (Andrew Madsen) * Fixed typo that caused crashes in BinaryDelta (Tamas Lustyik) * SUStandardVersionComparator.h is public (Vincent CARLIER) * Remove pre-10.6-specific code. (C.W. Betts) * Objective C 2 getters and setters. (C.W. Betts) * Define correct dependencies on locale scripts (Antonin Hildebrand) # 1.6.0 * Cleaned up and deleted redundant strings files (Kornel Lesiński) * Modern Objective C syntax, properties where possible. (C.W. Betts) * Make SUAppcastDelegate a formal protocol. (C.W. Betts) * Fixed default font in release notes WebView (Kornel Lesiński) * Configurable name for finish_installation.app (Kornel Lesiński) * Removed code for 10.4 (Kornel Lesiński) * Convert all strings files to UTF-8 (UTF-16 must die) (Kornel Lesiński) * Removing GC target (Matt Thomas) * finish_installation.app and pkg files will not removed when we use *.pkg installer and restart system in the installer (Takayama Fumihiko) * Select Korean and Slovak for Sparkle.strings localization (Shon Frazier) * Updated the Romanian translation (Gabe) * pt-BR localization polishing (BR Lingo) * update zh_CN (61) * Shut up some warnings & make build with newer Xcode (Uli Kusterer) * Less unsafety with format strings (Uli Kusterer) * New icon (Rick Fillion) * fixed a 'content rectangle not entirely onscreen' warning (Simone Manganelli) * updated sends system profile to use info.plist if user defaults key isn't present (Jamie Pinkham) * Support for notifications on some updater events (Doug Russell) * Allow the delegate to trigger a silent install and relaunch (Matt Stevens) * Support silent relaunches (Matt Stevens) * Increment the sudden termination counter if installing on quit (Matt Stevens) * Prompts the user to update after a week (rather than a day) if he doesn't quit the app (Andy Matuschak) * Adding appcast item element, tag (Andy Matuschak) * We have this check box that says "Automatically download and install updates in the future." But we only download them automatically. We still ask permission again before installing them. (Andy Matuschak) # 1.5.0-beta6 * Important Changes * Sparkle now requires DSA signatures on your updates. Check the documentation for more information on how to set that up if you don't already sign your updates. You can bypass this requirement if you deliver both your appcast and your updates over SSL. * Sparkle will no longer display release notes located at file:// URLs, since Javascript on such a page would be able to read files on your file system. * For security reasons, Sparkle will refuse to install updates which appear to "downgrade" the app. * SUUpdater now implements new keys: "automaticallyDownloadsUpdates", "lastUpdateCheckDate", and "sendsSystemProfile." * Fixed a bug that could prevent SUProbingUpdateDriver from working. * Fixed a bug that prevented the updaterWillRelaunchApplication: delegate method from getting called. * Fixed displaying release notes transmitted "loose" in the key. * Fixed Sparkle compilation on 10.4 systems. * Fixed a bug that could cause window confusion if an app changed its LSUIElement at runtime. * Added support for Sparkle 1.1's behavior of disabling updates when the check interval is 0. * Sparkle can now handle appending parameters to URLs which already have parameters. * If an update's sparkle:shortVersionString is the same as the host's CFBundleShortVersionString, the sparkle:version and CFBundleVersion will be presented in parentheticals. # 1.5.0-beta5 * Important Changes! * Made every Sparkle class private except for SUUpdater, SUAppcast, SUAppcastItem, and the SUVersionComparisonProtocol. * There is now a single SUUpdater singleton for every host bundle; instead of -[SUUpdater setHostBundle], you can use +[SUUpdater updaterForBundle]. * Redefined the (entire) delegate protocol accordingly. * Renamed -[SUUpdater updatePreferencesChanged] to -[SUUpdater resetUpdateCycle]. This provides better semantics for non-apps, which need to start the update cycle manually. * -[SUUpdater checkForUpdatesWithDriver] is private. If you were using SUProbingUpdateDriver, you can now use -[SUUpdater checkForUpdateInformation] for a similar effect. * All the user defaults keys are now private; instead, SUUpdater is KVC-compliant for automaticallyChecksForUpdates, updateCheckInterval, and feedURL. * Reduced the size of the English-only framework by 25%. * System profiling information is now only submitted to the server once per week; this will help normalize your statistics across users with different interval preferences. * The feedParametersForUpdater: delegate method now requires "displayKey" and "displayVersion" keys so that it can inform the user what's being sent. * Added a delegate method called pathToRelaunchForUpdater: which can be used for plugins to provide the path which should be used when relaunching the client after installing an update. * Added support for xml:lang to pick localized nodes in appcasts (for release notes, etc). * Fixed a bug which would cause the "checking for updates" window to not disappear in certain extraordinary error conditions. * Fixed a DSA signature checking bug for .tar.gz archives. * Sparkle now refuses to update on any read-only volume, not just dmgs. * Sparkle will clean up the host app's name and version before sending it as the user agent string; some non-ASCII characters were causing problems. * Added an Italian localization courtesy Michele Longhi. * Added a Swedish localization courtesy Daniel Bergman. * Fixes to the French localization courtesy Ronald Leroux and Yann Ricqueberg. * Fixes to the German localization courtesy Sven-S. Porst. * Fixes to the Russian localization courtesy Alexander Bykov and Anton Sotkov. * Fixed a number of issues related to archive format detection: I reverted back to extensions from UTIs. * Focus behavior fixes for LSUIElement apps. * The status window progress bar now animates even when indeterminate. * Major refactorings to improve functionality for non-app bundles. # 1.5.0-beta4 * Fixed a critical bug which prevented non-.dmgs from unarchiving properly. * Added reporting of 64-bit capability to the profiling system. # 1.5.0-beta3 * Added a new delegate method to SUUpdater.h to allow delegates to specify custom version comparators. * Added a German localization, courtesy the Camino localizer team: Dominik Tobschall, Tobias Stohr, and Friedemann Bochow. * Bug fixes: * Fixed a serious bug which could cause a server to be DDoS'd (or the host app to crash!) if an appcast fails to be parsed. * Fixed .tbz extraction if the archive was made with Stuffit. * Fixed support for .tar.bz2 and .tar.gz; Sparkle has to assume the archive is a tar when it sees "bz2" and "gz"; don't use those without tarring. * Fixed a typo which caused the shouldPromptForPermissionToCheckForUpdatesToHostBundle: method to not work in 1.5b2. * Fixed .zip extraction on Tiger (Apple changed the UTI between releases) * Fixed a crasher on Tiger. * Fixed display of the default app icon when the host app doesn't have an icon. * Sparkle now displays a sensible progress string and uses an indeterminate progress bar when the server doesn't report a file size. * Fixed some memory leaks. # 1.5.0-beta2 * Compatibility Issues: * Most of the delegate method selectors have changed to scale better. See SUUpdater.h for changes; you'll likely have to make changes if you implement any delegate methods. * If you're using .tar.gz or .tar.bz2 archives, name them ".tbz" or ".tgz" instead; Sparkle now uses UTIs for archive detection, and it's not smart about double extensions. * I'm no longer supporting 10.3. This may or may not work on Panther—probably not. * Sparkle's no longer built for ppc64 by default. If you want to ship that, feel free to build your own, but this saves a few hundred k. * Enhancements: * Sparkle now detects if the preferences for automatic update checks or the time interval change mid-cycle. * If your product is a non-.app, you need to clue Sparkle in on the change by calling [[SUUpdater sharedUpdater] updatePreferencesChanged]. * Added a cancel to the "checking for updates..." dialog. * Sparkle now cleans up all its litter in /tmp. * Made SUUpdater's delegate an IBOutlet so you can hook it up in IB. * Bug fixes: * Sparkle no longer crashes on non-GC hosts when the user cancels an update's downloads. * Sparkle no longer gets stuck in an inconsistent state or crashes when it can't parse the appcast on scheduled updates. * Added the sharedUpdater method to SUUpdater, as it should have been. * Fixed a bug where the "checking for updates..." window wouldn't go away if an error occurs while checking for updates. * Made the dual-mode build configuration actually use the .xcconfig which builds it with GC support. (oops!) * Fixed relaunching for prefpanes. * Sparkle no longer fails to install updates on Snow Leopard (though there's still an issue with trashing the old version of the app, but it seems to be a 10.6 bug) * Sparkle now handles redirects correctly under Tiger. * Fixed the installation path for non-.app bundles. * Fixed a bug which could crash Sparkle under non-English locales. * Fixed a weird race condition which could cause the relaunch tool to never notice that its target relaunched. * Fixed a bug where if the host app is inactive when an update occurs, the update alert sometimes doesn't become key. * Minor textual fixes. * Localizations: * Dutch: Maarten Van Coile * French: Yann Ricquebourg * Spanish: Ernesto Gomez Cereijo # 1.5.0-beta1 * The most important things to know: * The 10.3 support is untested at best; sketchy at worst. Test with it thoroughly before you use it. * Sparkle now asks for permission to update on second launch; don't be surprised at that. You can change that behavior with a delegate method; read SUUpdater.h for more info. * We no longer distinguish between "check on startup" and "scheduled updates"; everything is scheduled, with the default being every day. * The test application is using the new profiling features, but that's only for demonstration: these are off by default. More on this later. * There are no localizations yet. * New features: * Sparkle now supports .pkgs. Just name the .pkg the name of the app and put in the update archive. * Sparkle now sends optional demographic profiling information; set SUEnableSystemProfiling to YES in your Info.plist and check out the GET data sent to your webserver when fetching the appcast. More on this in the documentation. The test application has this on so you can see the behavior. * Sparkle now supports updating non-.apps. Just call -setHostBundle: on the global SUUpdater to let it know what you're trying to update. * Sparkle now supports garbage collection in the host app. Use "Sparkle-with-GC.framework" for that, but be aware it's 10.5-only. * Sparkle is now 64-bit compatible, compiling both ppc64 and x86_64. * Sparkle now supports a sparkle:minimumSystemVersion key you can set on appcast items. It does what you think it does. * Sparkle now checks to see if the host app is running from a disk image and refuses to update if it is. (10.4+ only) * Added support for entities in enclosure paths. * The file size output is now formatted prettily. * Sparkle now gives visual indication that it's checking for updates when the update's user initiated. ie: it pops up a status controller saying "checking for updates..." * Added support for an SUPublicDSAKeyFile, so people don't have to copy/paste their entire key into their Info.plist. Set this key in your Info.plist to the filename of the key in your Resources directory. * Added an actually maintainable codebase. * Changes: * Sparkle version comparison is now dramatically less stupid and verified by a bunch of unit tests. If something doesn't work the way you think it should, add a test to SUVersionComparisonTest.m * Added a minimum to the check interval so that developers don't accidentally release their apps into the wild with 60-second test check intervals and have DOS-attack-like results. It's an hour now for release mode; feel free to change it. * The relaunching process now uses a separate helper app, which is a much more robust method. * Changed CFBundleShortVersionString behavior: Sparkle no longer uses Apple's about box style of displaying ShortVersionString (CFBundleVersion) when the latter is available. * No more MD5 checking. Use DSA: it's actually secure. * The abomination that was SUStatusChecker is dead. Use SUProbingUpdateDriver instead. * Bugfixes: * Fixed a huge bug with fully-automatic updating: before, if the user chose to relaunch later, the app would be running from the trash for a while. Now the buttons are "install and relaunch" or "install later." * Sparkle forces Spotlight to reindex the updated app so that it won't keep pointing to the one in the trash. * Sparkle trims whitespace from around DSA signatures; this could cause crashes before. * Fixed a bug where the user choosing to skip a version would inhibit future automatic updates until the next launch. * Fixed a bug that could occur when the app has a localized CFBundleName. * .dmgs now work on Leopard. * The status controller's button now sizes appropriately to the localization. * Sparkle now works correctly with LSUIElement apps: it focuses them before displaying the update alert. * Sparkle now deletes failed partial downloads. * The update alert no longer floats above everything in the app. * Fixed varied and sundry memory leaks. * A ton of other things that I've forgotten or were too small to mention! # 1.1 * Optimized framework size: now only 1.4mb with all localizations and 384kb with only English (an English-only version is in the Extras folder). * Added a new SUStatusChecker class for programmatically determining if a new version is available (see the docs); thanks, Evan Schoenberg! * Added support for apps using SIGCHLD; thanks, Augie Fackler! * Added a zh_CN update from JT Lee * Added a Polish update from Piotr Chylinski * Fixed DMG support for images with /Applications symlinks. * Fixed a really stupid interval-checking bug that could cause repeated hits to the appcast. * Fixed a bug where the check interval would be inconsistent if a value of 0 was stored in the user defaults. # 1.0 * Additions: * Added real version comparison courtesy Kevin Ballard: Sparkle now knows that 0.89 < 1.0a3 < 1.0. * Added many localizations courtesy David Kocher's localization team. * Added a much better installation mechanism courtesy Allan Odgaard. * Added a user agent string to the RSS fetch request. * Added support for CFBundleShortVersionString in addition to CFBundleVersion, and support for a sparkle:shortVersionString attribute on the enclosure. * Added support for CFBundleDisplayName if available. * Changes: * Automatic updating is now allowed by default, but only if DSA signing is on. * Pressing Escape or closing the update alert now reminds the user later. * Now when there's a stored check interval, Sparkle doesn't check immediately on startup the first time the app is launched because the user hasn't consented to it yet. * The update alert now remembers its size and floats. * Bug Fixes: * Fixed installation of DMGs with multiple files enclosed. * Fixed a nasty memory leak. * Fixed a bug wherein having no value for allowing automatic updates would display a checkbox for the updates but would not honor it. * Fixed a bug in zip extraction that occurred in Panther. * Fixed release notes caching. * Fixed a bug wherein Sparkle refused to authenticate the installation if the user had cancelled authentication previously in that session. * Fixed a weird bug that would cause a second help menu to appear on first launch. * Fixed a bug that could occur when changing the scheduled check interval. * Fixed a bug wherein the host app could crash if the user clicked Remind Me Later before the release notes finished loading. * Fixed a bug wherein the behavior was undefined if the user manually initiated a check when an automatic one was already taking place. * Fixed wrapping on the description field in the update alert. # 1.0-beta3 * Fixed a nasty crasher that occurred often when the user was not connected to the internet. # 1.0-beta2 * Major Improvements: * Fully automatic updating! (see the Documentation: this is beta and off by default) * Added support for DSA signatures (see the Documentation). * Added support for MD5 sum verification. * Added Security.framework-based authentication for installing to privileged directories. * Huge refactoring of the codebase: there's now a Sparkle Xcode project, Sparkle is now a framework, and everything is modular / abstracted. And no more code-generated interface. * Minor Improvements: * A SUUpdaterWillRestartNotification is sent out before restarting now. * Added key equivalents to alert panel buttons. * Error handling is much prettier now: technical messages are not presented to the user anymore. * There's now a test app for developers to see what Sparkle's like before using it. * Wrote new, pretty, extremely thorough documentation. * Bug Fixes: * Relaunch behavior is much improved and shouldn't fail in huge apps anymore. * Fixed a bug wherein a failing tar command could crash the host app. * Sparkle now looks at InfoPlist.strings in addition to Info.plist. * Fixed some stupid typos. # 1.0-beta1 * Major New Features: * Sparkle now supports scheduled periodic updates—read the Readme for information on how to use it. * Sparkle now supports WebKit-based release notes (for CSS and full HTML), which it displays in the main update alert, not a separate panel. The Readme has much more information. Sparkle will, of course, fall back on NSTextView if the host app does not include WebKit. * Minor New Features: * Added support for .zip update archives. * Added support for .dmg update archives. * Implemented Remind Me Later to replace simple update cancellation. * Implemented Skip This Version functionality. * Added support for multiple feeds via the user defaults SUFeedURL key taking precedent over the one in Info.plist. * Added support for Sparkle's custom XML namespace, which is optional but may prove useful. See the Readme for more information. * Bug Fixes: * Sparkle will no longer enter an inconsistent state if the user tries to update again while one is already in progress. * Sparkle now uses CFBundleName to determine the application's name instead of the app's filename. * Sparkle no longer crashes if the user cancels during extraction. * Lots of code refactoring. # 0.1 * Initial Release ================================================ FILE: CODE_OF_CONDUCT.md ================================================ # Contributor Covenant Code of Conduct ## Our Pledge In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. ## Our Standards Examples of behavior that contributes to creating a positive environment include: * Using welcoming and inclusive language * Being respectful of differing viewpoints and experiences * Gracefully accepting constructive criticism * Focusing on what is best for the community * Showing empathy towards other community members Examples of unacceptable behavior by participants include: * The use of sexualized language or imagery and unwelcome sexual attention or advances * Trolling, insulting/derogatory comments, and personal or political attacks * Public or private harassment * Publishing others' private information, such as a physical or electronic address, without explicit permission * Other conduct which could reasonably be considered inappropriate in a professional setting ## Our Responsibilities Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. ## Scope This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. ## Enforcement Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at kornel+sparkle@geekhood.net. All complaints will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. ## Attribution This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] [homepage]: http://contributor-covenant.org [version]: http://contributor-covenant.org/version/1/4/ ================================================ FILE: Carthage-dev.json ================================================ {"1.27.1": "https://github.com/sparkle-project/Sparkle/releases/download/1.27.1/Sparkle-1.27.1.tar.xz", "2.0.0-beta.6": "https://github.com/sparkle-project/Sparkle/releases/download/2.0.0-beta.6/Sparkle-2.0.0-beta.6.tar.xz", "2.0.0-rc.1": "https://github.com/sparkle-project/Sparkle/releases/download/2.0.0-rc.1/Sparkle-2.0.0-rc.1.tar.xz", "2.0.0": "https://github.com/sparkle-project/Sparkle/releases/download/2.0.0/Sparkle-2.0.0.tar.xz", "2.1.0-beta.1": "https://github.com/sparkle-project/Sparkle/releases/download/2.1.0-beta.1/Sparkle-2.1.0-beta.1.tar.xz", "2.1.0-beta.2": "https://github.com/sparkle-project/Sparkle/releases/download/2.1.0-beta.2/Sparkle-2.1.0-beta.2.tar.xz", "2.1.0": "https://github.com/sparkle-project/Sparkle/releases/download/2.1.0/Sparkle-2.1.0.tar.xz", "2.2.0-beta.1": "https://github.com/sparkle-project/Sparkle/releases/download/2.2.0-beta.1/Sparkle-2.2.0-beta.1.tar.xz", "2.2.0-beta.2": "https://github.com/sparkle-project/Sparkle/releases/download/2.2.0-beta.2/Sparkle-2.2.0-beta.2.tar.xz", "2.2.0": "https://github.com/sparkle-project/Sparkle/releases/download/2.2.0/Sparkle-2.2.0.tar.xz", "2.2.1": "https://github.com/sparkle-project/Sparkle/releases/download/2.2.1/Sparkle-2.2.1.tar.xz", "2.2.2": "https://github.com/sparkle-project/Sparkle/releases/download/2.2.2/Sparkle-2.2.2.tar.xz", "2.3.0-beta.1": "https://github.com/sparkle-project/Sparkle/releases/download/2.3.0-beta.1/Sparkle-2.3.0-beta.1.tar.xz", "2.3.0-beta.2": "https://github.com/sparkle-project/Sparkle/releases/download/2.3.0-beta.2/Sparkle-2.3.0-beta.2.tar.xz", "2.3.0": "https://github.com/sparkle-project/Sparkle/releases/download/2.3.0/Sparkle-2.3.0.tar.xz", "2.3.1": "https://github.com/sparkle-project/Sparkle/releases/download/2.3.1/Sparkle-2.3.1.tar.xz", "2.3.2": "https://github.com/sparkle-project/Sparkle/releases/download/2.3.2/Sparkle-2.3.2.tar.xz", "2.4.0-beta.1": "https://github.com/sparkle-project/Sparkle/releases/download/2.4.0-beta.1/Sparkle-2.4.0-beta.1.tar.xz", "2.4.0-beta.2": "https://github.com/sparkle-project/Sparkle/releases/download/2.4.0-beta.2/Sparkle-2.4.0-beta.2.tar.xz", "2.4.0": "https://github.com/sparkle-project/Sparkle/releases/download/2.4.0/Sparkle-2.4.0.tar.xz", "2.4.1-beta.1": "https://github.com/sparkle-project/Sparkle/releases/download/2.4.1-beta.1/Sparkle-2.4.1-beta.1.tar.xz", "2.4.1": "https://github.com/sparkle-project/Sparkle/releases/download/2.4.1/Sparkle-2.4.1.tar.xz", "2.4.2": "https://github.com/sparkle-project/Sparkle/releases/download/2.4.2/Sparkle-2.4.2.tar.xz", "2.5.0-beta.1": "https://github.com/sparkle-project/Sparkle/releases/download/2.5.0-beta.1/Sparkle-2.5.0-beta.1.tar.xz", "2.5.0-beta.2": "https://github.com/sparkle-project/Sparkle/releases/download/2.5.0-beta.2/Sparkle-2.5.0-beta.2.tar.xz", "2.5.0": "https://github.com/sparkle-project/Sparkle/releases/download/2.5.0/Sparkle-2.5.0.tar.xz", "2.5.1": "https://github.com/sparkle-project/Sparkle/releases/download/2.5.1/Sparkle-2.5.1.tar.xz", "2.5.2": "https://github.com/sparkle-project/Sparkle/releases/download/2.5.2/Sparkle-2.5.2.tar.xz", "2.6.0-beta.1": "https://github.com/sparkle-project/Sparkle/releases/download/2.6.0-beta.1/Sparkle-2.6.0-beta.1.tar.xz", "2.6.0-beta.2": "https://github.com/sparkle-project/Sparkle/releases/download/2.6.0-beta.2/Sparkle-2.6.0-beta.2.tar.xz", "2.6.0": "https://github.com/sparkle-project/Sparkle/releases/download/2.6.0/Sparkle-2.6.0.tar.xz", "2.6.1": "https://github.com/sparkle-project/Sparkle/releases/download/2.6.1/Sparkle-2.6.1.tar.xz", "1.27.2": "https://github.com/sparkle-project/Sparkle/releases/download/1.27.2/Sparkle-1.27.2.tar.xz", "2.6.2": "https://github.com/sparkle-project/Sparkle/releases/download/2.6.2/Sparkle-2.6.2.tar.xz", "1.27.3": "https://github.com/sparkle-project/Sparkle/releases/download/1.27.3/Sparkle-1.27.3.tar.xz", "2.6.3": "https://github.com/sparkle-project/Sparkle/releases/download/2.6.3/Sparkle-2.6.3.tar.xz", "2.6.4": "https://github.com/sparkle-project/Sparkle/releases/download/2.6.4/Sparkle-2.6.4.tar.xz", "2.7.0-beta.1": "https://github.com/sparkle-project/Sparkle/releases/download/2.7.0-beta.1/Sparkle-2.7.0-beta.1.tar.xz", "2.7.0": "https://github.com/sparkle-project/Sparkle/releases/download/2.7.0/Sparkle-2.7.0.tar.xz", "2.7.1": "https://github.com/sparkle-project/Sparkle/releases/download/2.7.1/Sparkle-2.7.1.tar.xz", "2.7.2": "https://github.com/sparkle-project/Sparkle/releases/download/2.7.2/Sparkle-2.7.2.tar.xz", "2.7.3": "https://github.com/sparkle-project/Sparkle/releases/download/2.7.3/Sparkle-2.7.3.tar.xz", "2.8.0-beta.1": "https://github.com/sparkle-project/Sparkle/releases/download/2.8.0-beta.1/Sparkle-2.8.0-beta.1.tar.xz", "2.8.0-beta.2": "https://github.com/sparkle-project/Sparkle/releases/download/2.8.0-beta.2/Sparkle-2.8.0-beta.2.tar.xz", "2.8.0-beta.3": "https://github.com/sparkle-project/Sparkle/releases/download/2.8.0-beta.3/Sparkle-2.8.0-beta.3.tar.xz", "2.8.0": "https://github.com/sparkle-project/Sparkle/releases/download/2.8.0/Sparkle-2.8.0.tar.xz", "2.8.1": "https://github.com/sparkle-project/Sparkle/releases/download/2.8.1/Sparkle-2.8.1.tar.xz", "2.9.0-beta.1": "https://github.com/sparkle-project/Sparkle/releases/download/2.9.0-beta.1/Sparkle-2.9.0-beta.1.tar.xz", "2.9.0-beta.2": "https://github.com/sparkle-project/Sparkle/releases/download/2.9.0-beta.2/Sparkle-2.9.0-beta.2.tar.xz", "2.9.0": "https://github.com/sparkle-project/Sparkle/releases/download/2.9.0/Sparkle-2.9.0.tar.xz"} ================================================ FILE: Configurations/CommandLineTool-Debug.xcconfig ================================================ // Generate Appcast Debug #include "CommandLineTool-Shared.xcconfig" #include "ConfigSwiftDebug.xcconfig" ================================================ FILE: Configurations/CommandLineTool-Release.xcconfig ================================================ // Generate Appcast Release #include "CommandLineTool-Shared.xcconfig" #include "ConfigSwiftRelease.xcconfig" ================================================ FILE: Configurations/CommandLineTool-Shared.xcconfig ================================================ // Generate Appcast only #include "ConfigSwift.xcconfig" SWIFT_OBJC_BRIDGING_HEADER = $(PRODUCT_NAME)/Bridging-Header.h GCC_PREPROCESSOR_DEFINITIONS = $(GCC_PREPROCESSOR_DEFINITIONS) BUILDING_EXTERNALLY=1 ARCHS = $(ARCHS_COMMAND_LINE_TOOLS) ================================================ FILE: Configurations/ConfigCommon.xcconfig ================================================ // Common // Set to 1 or 0 to build and bundle the provided XPC Services in Sparkle framework // (Note for testing purposes for sandboxed apps, disabling all XPC services and then re-enabling them // may not work on the same login session due to a system caching issue. See comments for setting FRAMEWORK_VERSION for more info) SPARKLE_EMBED_INSTALLER_LAUNCHER_XPC_SERVICE = 1 // Embedding the downloader service also enables building legacy WebKit1 support SPARKLE_EMBED_DOWNLOADER_XPC_SERVICE = 1 SPARKLE_EMBED_INSTALLER_STATUS_XPC_SERVICE = 0 SPARKLE_EMBED_INSTALLER_CONNECTION_XPC_SERVICE = 0 // Set to 0 to disable building legacy 1.x SUUpdater stub SPARKLE_BUILD_LEGACY_SUUPDATER = 1 // Set to 0 to not build legacy DSA signing support // Don't disable this if you haven't migrated to EdDSA yet or if you support updating // other developer's applications that haven't migrated to EdDSA yet // If this is disabled, this requires an update to OTHER_SWIFT_FLAGS_COMMON below SPARKLE_BUILD_LEGACY_DSA_SUPPORT = 1 // Set to 0 to not build legacy DSA signing support for generate_appcast // Don't disable this if you still have items in your appcast feed that still need DSA signatures // If this is disabled, this requires an update to OTHER_SWIFT_FLAGS_COMMON below GENERATE_APPCAST_BUILD_LEGACY_DSA_SUPPORT = 1 // Set to 0 to not build legacy / xar based delta update support SPARKLE_BUILD_LEGACY_DELTA_SUPPORT = 1 // Set to 0 to not build bzip2 support for version 3 delta based updates SPARKLE_BUILD_BZIP2_DELTA_SUPPORT = 1 // Set to 0 to not build support for package (pkg) based installations // If this is disabled, this requires an update to OTHER_SWIFT_FLAGS_COMMON below SPARKLE_BUILD_PACKAGE_SUPPORT = 1 // Set to 0 to only build all non-UI bits (which means client uses a custom SPUUserDriver) SPARKLE_BUILD_UI_BITS = 1 // Set to 0 to not use and copy localization / string files to framework SPARKLE_COPY_LOCALIZATIONS = 1 // Compilation flags for Swift tools and unit tests // If SPARKLE_BUILD_LEGACY_DELTA_SUPPORT is set to 0, remove -DSPARKLE_BUILD_LEGACY_DSA_SUPPORT // If GENERATE_APPCAST_BUILD_LEGACY_DSA_SUPPORT is set to 0, remove -DGENERATE_APPCAST_BUILD_LEGACY_DSA_SUPPORT // If SPARKLE_BUILD_PACKAGE_SUPPORT is set to 0, remove -DSPARKLE_BUILD_PACKAGE_SUPPORT OTHER_SWIFT_FLAGS_COMMON = -DSPARKLE_BUILD_LEGACY_DSA_SUPPORT -DGENERATE_APPCAST_BUILD_LEGACY_DSA_SUPPORT -DSPARKLE_BUILD_PACKAGE_SUPPORT // Minimum supported macOS version MACOSX_DEPLOYMENT_TARGET = 10.13 // Archs to use for Sparkle // You can change this for example to add arm64e or remove x86_64 ARCHS = $(ARCHS_STANDARD) // Archs to use for command line tools (generate_appcast, sign_update, BinaryDelta, etc) // arm64e is not supported for these yet ARCHS_COMMAND_LINE_TOOLS = $(ARCHS_STANDARD) // Default Sparkle names SPARKLE_BUNDLE_IDENTIFIER = org.sparkle-project.Sparkle SPARKLE_RELAUNCH_TOOL_NAME = Autoupdate SPARKLE_INSTALLER_PROGRESS_TOOL_NAME = Updater SPARKLE_INSTALLER_PROGRESS_TOOL_BUNDLE_ID = $(SPARKLE_BUNDLE_IDENTIFIER).$(SPARKLE_INSTALLER_PROGRESS_TOOL_NAME) // Set these if you want to use custom name for the XPC Services // For the Installer one in particular, this name may show up in an authorization prompt // when installing an update that requires authorization. INSTALLER_CONNECTION_NAME = InstallerConnection INSTALLER_STATUS_NAME = InstallerStatus INSTALLER_LAUNCHER_NAME = Installer DOWNLOADER_NAME = Downloader // Set this to your own prefix if you want to use XPC Service bundle IDs with different prefix XPC_SERVICE_BUNDLE_ID_PREFIX = org.sparkle-project INSTALLER_CONNECTION_BUNDLE_ID = ${XPC_SERVICE_BUNDLE_ID_PREFIX}.InstallerConnection INSTALLER_STATUS_BUNDLE_ID = ${XPC_SERVICE_BUNDLE_ID_PREFIX}.InstallerStatus INSTALLER_LAUNCHER_BUNDLE_ID = ${XPC_SERVICE_BUNDLE_ID_PREFIX}.InstallerLauncher DOWNLOADER_BUNDLE_ID = ${XPC_SERVICE_BUNDLE_ID_PREFIX}.DownloaderService // The Downloader XPC Service is not sandboxed by default. // Uncomment line to enable Sandboxing for this service. // If this is done, you *must* set a custom XPC_SERVICE_BUNDLE_ID_PREFIX for your app above. // Otherwise sandboxed apps that use the same sandboxed Downloader Service may conflict with each other. //DOWNLOADER_SANDBOXED_ENTITLEMENTS = Downloader/Downloader.entitlements // If your app file on disk is named "MyApp 1.1b4", Sparkle usually updates it // in place, giving you an app named 1.1b4 that is actually 1.2. Turn the // following on to always reset the name back to "MyApp" // If you are using this option to change the name of your app, you should // disable the option again when you no longer need it // Note this option is only supported for regular application updates and not plug-ins SPARKLE_NORMALIZE_INSTALLED_APPLICATION_NAME = 0 SPARKLE_ICON_NAME = AppIcon // If you change any of these version details, you must increase CURRENT_PROJECT_VERSION // These variables must have a space after the '=' too SPARKLE_VERSION_MAJOR = 2 SPARKLE_VERSION_MINOR = 9 SPARKLE_VERSION_PATCH = 0 // This should be in SemVer format or empty, ie. "-beta.1" // These variables must have a space after the '=' too SPARKLE_VERSION_SUFFIX = CURRENT_PROJECT_VERSION = 2053 MARKETING_VERSION = $(SPARKLE_VERSION_MAJOR).$(SPARKLE_VERSION_MINOR).$(SPARKLE_VERSION_PATCH)$(SPARKLE_VERSION_SUFFIX) ALWAYS_SEARCH_USER_PATHS = NO ENABLE_STRICT_OBJC_MSGSEND = YES GCC_SYMBOLS_PRIVATE_EXTERN = YES GCC_INLINES_ARE_PRIVATE_EXTERN = YES PRODUCT_NAME = ${TARGET_NAME} PRODUCT_BUNDLE_IDENTIFIER = org.sparkle-project.Sparkle.${PRODUCT_NAME:rfc1034identifier} GCC_PREPROCESSOR_DEFINITIONS_COMMON = SPU_OBJC_DIRECT=__attribute__((objc_direct)) SPU_OBJC_DIRECT_MEMBERS=__attribute__((objc_direct_members)) SPARKLE_NORMALIZE_INSTALLED_APPLICATION_NAME=$(SPARKLE_NORMALIZE_INSTALLED_APPLICATION_NAME) SPARKLE_BUILD_UI_BITS=$(SPARKLE_BUILD_UI_BITS) SPARKLE_COPY_LOCALIZATIONS=$(SPARKLE_COPY_LOCALIZATIONS) SPARKLE_BUILD_LEGACY_SUUPDATER=$(SPARKLE_BUILD_LEGACY_SUUPDATER) SPARKLE_BUILD_PACKAGE_SUPPORT=$(SPARKLE_BUILD_PACKAGE_SUPPORT) SPARKLE_BUILD_LEGACY_DELTA_SUPPORT=$(SPARKLE_BUILD_LEGACY_DELTA_SUPPORT) SPARKLE_BUILD_BZIP2_DELTA_SUPPORT=$(SPARKLE_BUILD_BZIP2_DELTA_SUPPORT) SPARKLE_BUILD_LEGACY_DSA_SUPPORT=$(SPARKLE_BUILD_LEGACY_DSA_SUPPORT) GENERATE_APPCAST_BUILD_LEGACY_DSA_SUPPORT=$(GENERATE_APPCAST_BUILD_LEGACY_DSA_SUPPORT) SPARKLE_BUNDLE_IDENTIFIER=\"$(SPARKLE_BUNDLE_IDENTIFIER)\" CURRENT_PROJECT_VERSION=\"$(CURRENT_PROJECT_VERSION)\" MARKETING_VERSION=\"$(MARKETING_VERSION)\" SPARKLE_RELAUNCH_TOOL_NAME=\"$(SPARKLE_RELAUNCH_TOOL_NAME)\" SPARKLE_INSTALLER_PROGRESS_TOOL_NAME=\"$(SPARKLE_INSTALLER_PROGRESS_TOOL_NAME)\" SPARKLE_INSTALLER_PROGRESS_TOOL_BUNDLE_ID=\"$(SPARKLE_INSTALLER_PROGRESS_TOOL_BUNDLE_ID)\" SPARKLE_ICON_NAME=\"$(SPARKLE_ICON_NAME)\" INSTALLER_LAUNCHER_NAME=\"${INSTALLER_LAUNCHER_NAME}\" INSTALLER_LAUNCHER_BUNDLE_ID=\"${INSTALLER_LAUNCHER_BUNDLE_ID}\" INSTALLER_LAUNCHER_XPC_SERVICE_EMBEDDED=$(SPARKLE_EMBED_INSTALLER_LAUNCHER_XPC_SERVICE) INSTALLER_CONNECTION_NAME=\"${INSTALLER_CONNECTION_NAME}\" INSTALLER_CONNECTION_BUNDLE_ID=\"${INSTALLER_CONNECTION_BUNDLE_ID}\" INSTALLER_CONNECTION_XPC_SERVICE_EMBEDDED=$(SPARKLE_EMBED_INSTALLER_CONNECTION_XPC_SERVICE) INSTALLER_STATUS_NAME=\"${INSTALLER_STATUS_NAME}\" INSTALLER_STATUS_BUNDLE_ID=\"${INSTALLER_STATUS_BUNDLE_ID}\" INSTALLER_STATUS_XPC_SERVICE_EMBEDDED=$(SPARKLE_EMBED_INSTALLER_STATUS_XPC_SERVICE) DOWNLOADER_NAME=\"${DOWNLOADER_NAME}\" DOWNLOADER_BUNDLE_ID=\"${DOWNLOADER_BUNDLE_ID}\" DOWNLOADER_XPC_SERVICE_EMBEDDED=$(SPARKLE_EMBED_DOWNLOADER_XPC_SERVICE) CODE_SIGN_IDENTITY = - SDKROOT = macosx ENABLE_HARDENED_RUNTIME = YES CLANG_ENABLE_OBJC_ARC = YES GCC_ENABLE_PASCAL_STRINGS = NO GCC_NO_COMMON_BLOCKS = YES COMBINE_HIDPI_IMAGES = NO // Enable warnings CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = 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_ASSIGN_ENUM = YES CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES CLANG_WARN_DOCUMENTATION_COMMENTS = YES CLANG_WARN_EMPTY_BODY = YES CLANG_WARN_IMPLICIT_SIGN_CONVERSION = YES CLANG_WARN_OBJC_EXPLICIT_OWNERSHIP_TYPE = YES CLANG_WARN_OBJC_IMPLICIT_ATOMIC_PROPERTIES = YES CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES CLANG_WARN_OBJC_MISSING_PROPERTY_SYNTHESIS = YES CLANG_WARN_OBJC_REPEATED_USE_OF_WEAK = YES CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES CLANG_WARN_SUSPICIOUS_IMPLICIT_CONVERSION = YES CLANG_WARN_UNREACHABLE_CODE = YES GCC_TREAT_IMPLICIT_FUNCTION_DECLARATIONS_AS_ERRORS = YES GCC_TREAT_INCOMPATIBLE_POINTER_TYPE_WARNINGS_AS_ERRORS = YES GCC_WARN_64_TO_32_BIT_CONVERSION = YES GCC_WARN_ABOUT_MISSING_FIELD_INITIALIZERS = YES GCC_WARN_ABOUT_MISSING_NEWLINE = YES GCC_WARN_ABOUT_MISSING_PROTOTYPES = YES GCC_WARN_ABOUT_RETURN_TYPE = YES GCC_WARN_FOUR_CHARACTER_CONSTANTS = YES GCC_WARN_HIDDEN_VIRTUAL_FUNCTIONS = YES GCC_WARN_HIDDEN_VIRTUAL_FUNCTIONS = YES GCC_WARN_INITIALIZER_NOT_FULLY_BRACKETED = YES GCC_WARN_MULTIPLE_DEFINITION_TYPES_FOR_SELECTOR = YES GCC_WARN_NON_VIRTUAL_DESTRUCTOR = YES GCC_WARN_PEDANTIC = YES GCC_WARN_SHADOW = YES GCC_WARN_SIGN_COMPARE = YES GCC_WARN_STRICT_SELECTOR_MATCH = YES GCC_WARN_UNDECLARED_SELECTOR = YES GCC_WARN_UNKNOWN_PRAGMAS = YES GCC_WARN_UNUSED_FUNCTION = YES GCC_WARN_UNUSED_LABEL = YES GCC_WARN_UNUSED_PARAMETER = YES GCC_WARN_UNUSED_VARIABLE = YES // Turn on all warnings, then disable a few which are almost impossible to avoid WARNING_CFLAGS = -Wall -Weverything -Wno-unused-macros -Wno-gnu-statement-expression -Wno-auto-import -Wno-gnu-zero-variadic-macro-arguments -Wno-format-non-iso -Wno-direct-ivar-access -Wno-declaration-after-statement -Wno-gnu-conditional-omitted-operand -Wno-switch-default -Wno-missing-include-dirs -Werror=undef // Prevents spurious warnings based on dependency scan considering Sparkle itself a dependency DIAGNOSE_MISSING_TARGET_DEPENDENCIES = NO ================================================ FILE: Configurations/ConfigCommonCoverage.xcconfig ================================================ #include "ConfigCommonDebug.xcconfig" GCC_PREPROCESSOR_DEFINITIONS = $(GCC_PREPROCESSOR_DEFINITIONS_COMMON) OTHER_SWIFT_FLAGS = $(OTHER_SWIFT_FLAGS_COMMON) CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_COMMA = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; GCC_GENERATE_TEST_COVERAGE_FILES = YES GCC_INSTRUMENT_PROGRAM_FLOW_ARCS = YES ================================================ FILE: Configurations/ConfigCommonDebug.xcconfig ================================================ #include "ConfigCommon.xcconfig" // Debug only CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_COMMA = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; GCC_OPTIMIZATION_LEVEL = 0 DEBUG_INFORMATION_FORMAT = dwarf ENABLE_TESTABILITY = YES GCC_GENERATE_DEBUGGING_SYMBOLS = YES GCC_PREPROCESSOR_DEFINITIONS = $(GCC_PREPROCESSOR_DEFINITIONS_COMMON) DEBUG=1 OTHER_SWIFT_FLAGS = $(OTHER_SWIFT_FLAGS_COMMON) ONLY_ACTIVE_ARCH = YES COPY_PHASE_STRIP = NO ================================================ FILE: Configurations/ConfigCommonRelease.xcconfig ================================================ #include "ConfigCommon.xcconfig" // Release only CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_COMMA = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; GCC_OPTIMIZATION_LEVEL = s DEBUG_INFORMATION_FORMAT = dwarf-with-dsym GCC_GENERATE_DEBUGGING_SYMBOLS = YES GCC_PREPROCESSOR_DEFINITIONS = $(GCC_PREPROCESSOR_DEFINITIONS_COMMON) DEBUG=0 OTHER_SWIFT_FLAGS = $(OTHER_SWIFT_FLAGS_COMMON) DEAD_CODE_STRIPPING = YES GCC_TREAT_WARNINGS_AS_ERRORS = NO GCC_WARN_UNINITIALIZED_AUTOS = YES COPY_PHASE_STRIP = NO DEPLOYMENT_POSTPROCESSING = YES ================================================ FILE: Configurations/ConfigDownloader.xcconfig ================================================ // Downloader INFOPLIST_FILE = Downloader/Info.plist WRAPPER_EXTENSION = xpc PRODUCT_BUNDLE_IDENTIFIER = ${DOWNLOADER_BUNDLE_ID} PRODUCT_NAME = ${DOWNLOADER_NAME} CODE_SIGN_ENTITLEMENTS = $(DOWNLOADER_SANDBOXED_ENTITLEMENTS) GCC_PREPROCESSOR_DEFINITIONS = $(GCC_PREPROCESSOR_DEFINITIONS) BUILDING_SPARKLE_SOURCES_EXTERNALLY=1 CLANG_MODULES_AUTOLINK = NO ================================================ FILE: Configurations/ConfigDownloaderDebug.xcconfig ================================================ // Downloader Debug #include "ConfigDownloader.xcconfig" ================================================ FILE: Configurations/ConfigFramework.xcconfig ================================================ // Framework only DYLIB_INSTALL_NAME_BASE = @rpath DYLIB_COMPATIBILITY_VERSION = 1.6 DYLIB_CURRENT_VERSION = $(SPARKLE_VERSION_MAJOR).$(SPARKLE_VERSION_MINOR).$(SPARKLE_VERSION_PATCH) WRAPPER_EXTENSION = framework // Sparkle 2 used to support developers embedding XPC Services in their own app bundle // However the Sparkle framework now bundles these XPC Services. Unfortunately this causes // a cache conflict in the system from upgrading from previous to newer versions of Sparkle, and // XPC Services inside Sparkle may not launch (a system reboot would fix this though). // The system cache seems to depend on the Sparkle file path, // so as a better workaround we have changed the FRAMEWORK_VERSION from A to B FRAMEWORK_VERSION = B INFOPLIST_FILE = Sparkle/Sparkle-Info.plist GCC_PREPROCESSOR_DEFINITIONS = $(inherited) BUILDING_SPARKLE=1 SKIP_INSTALL = YES DEFINES_MODULE = YES PRODUCT_BUNDLE_IDENTIFIER = ${SPARKLE_BUNDLE_IDENTIFIER} STRINGS_FILE_OUTPUT_ENCODING = binary // As long as we don't ourselves depend upon any Frameworks that are incompatible // with Application Extensions, then we should allow frameworks that require // compatibility to be able to link with us. APPLICATION_EXTENSION_API_ONLY = YES ENABLE_MODULE_VERIFIER = YES MODULEMAP_PRIVATE_FILE = Sparkle/Sparkle.private.modulemap ================================================ FILE: Configurations/ConfigFrameworkDebug.xcconfig ================================================ #include "ConfigFramework.xcconfig" // Unit tests need access to non-public classes GCC_SYMBOLS_PRIVATE_EXTERN = NO ================================================ FILE: Configurations/ConfigFrameworkRelease.xcconfig ================================================ #include "ConfigFramework.xcconfig" // Strip all non-global symbols (including debug symbols) STRIP_STYLE = non-global ================================================ FILE: Configurations/ConfigInstallerConnection.xcconfig ================================================ // InstallerConnection INFOPLIST_FILE = InstallerConnection/Info.plist WRAPPER_EXTENSION = xpc PRODUCT_BUNDLE_IDENTIFIER = ${INSTALLER_CONNECTION_BUNDLE_ID} PRODUCT_NAME = ${INSTALLER_CONNECTION_NAME} GCC_PREPROCESSOR_DEFINITIONS = $(GCC_PREPROCESSOR_DEFINITIONS) BUILDING_SPARKLE_SOURCES_EXTERNALLY=1 CLANG_MODULES_AUTOLINK = NO ================================================ FILE: Configurations/ConfigInstallerConnectionDebug.xcconfig ================================================ // InstallerConnection Debug #include "ConfigInstallerConnection.xcconfig" ================================================ FILE: Configurations/ConfigInstallerLauncher.xcconfig ================================================ // Installer Launcher INFOPLIST_FILE = InstallerLauncher/Info.plist WRAPPER_EXTENSION = xpc PRODUCT_BUNDLE_IDENTIFIER = ${INSTALLER_LAUNCHER_BUNDLE_ID} PRODUCT_NAME = ${INSTALLER_LAUNCHER_NAME} GCC_PREPROCESSOR_DEFINITIONS = $(GCC_PREPROCESSOR_DEFINITIONS) BUILDING_SPARKLE_SOURCES_EXTERNALLY=1 CLANG_MODULES_AUTOLINK = NO ================================================ FILE: Configurations/ConfigInstallerLauncherDebug.xcconfig ================================================ // Installer Launcher Debug #include "ConfigInstallerLauncher.xcconfig" ================================================ FILE: Configurations/ConfigInstallerProgress.xcconfig ================================================ // Installer Progress only INFOPLIST_FILE = Sparkle/InstallerProgress/InstallerProgress-Info.plist PRODUCT_NAME = $(SPARKLE_INSTALLER_PROGRESS_TOOL_NAME) PRODUCT_BUNDLE_IDENTIFIER = $(SPARKLE_INSTALLER_PROGRESS_TOOL_BUNDLE_ID) GCC_PREPROCESSOR_DEFINITIONS = $(GCC_PREPROCESSOR_DEFINITIONS) BUILDING_SPARKLE=0 BUILDING_SPARKLE_SOURCES_EXTERNALLY=1 SKIP_INSTALL = YES CLANG_ENABLE_MODULES = NO ================================================ FILE: Configurations/ConfigInstallerStatus.xcconfig ================================================ // InstallerStatus INFOPLIST_FILE = InstallerStatus/Info.plist WRAPPER_EXTENSION = xpc PRODUCT_BUNDLE_IDENTIFIER = ${INSTALLER_STATUS_BUNDLE_ID} PRODUCT_NAME = ${INSTALLER_STATUS_NAME} GCC_PREPROCESSOR_DEFINITIONS = $(GCC_PREPROCESSOR_DEFINITIONS) BUILDING_SPARKLE=0 BUILDING_SPARKLE_SOURCES_EXTERNALLY=1 CLANG_MODULES_AUTOLINK = NO ================================================ FILE: Configurations/ConfigInstallerStatusDebug.xcconfig ================================================ // InstallerStatus Debug #include "ConfigInstallerStatus.xcconfig" ================================================ FILE: Configurations/ConfigRelaunch.xcconfig ================================================ // Relaunch Tool only PRODUCT_NAME = $(SPARKLE_RELAUNCH_TOOL_NAME) SKIP_INSTALL = YES CLANG_ENABLE_MODULES = NO GCC_PREPROCESSOR_DEFINITIONS = $(GCC_PREPROCESSOR_DEFINITIONS) BUILDING_SPARKLE_SOURCES_EXTERNALLY=1 ================================================ FILE: Configurations/ConfigSparkleTool.xcconfig ================================================ // sparkle command line tool only INFOPLIST_FILE = sparkle-cli/Info.plist PRODUCT_BUNDLE_IDENTIFIER = org.sparkle-project.sparkle-cli PRODUCT_NAME = sparkle LD_RUNPATH_SEARCH_PATHS = @executable_path/../Frameworks/ ENABLE_HARDENED_RUNTIME = NO ================================================ FILE: Configurations/ConfigSwift.xcconfig ================================================ SWIFT_VERSION = 5 SWIFT_SWIFT3_OBJC_INFERENCE = Off; ================================================ FILE: Configurations/ConfigSwiftDebug.xcconfig ================================================ SWIFT_OPTIMIZATION_LEVEL = -Onone ================================================ FILE: Configurations/ConfigSwiftRelease.xcconfig ================================================ SWIFT_OPTIMIZATION_LEVEL = -O SWIFT_COMPILATION_MODE = wholemodule ================================================ FILE: Configurations/ConfigTestApp.xcconfig ================================================ // Test Application INFOPLIST_FILE = TestApplication/TestApplication-Info.plist WRAPPER_EXTENSION = app ASSETCATALOG_COMPILER_APPICON_NAME = $(SPARKLE_ICON_NAME) LD_RUNPATH_SEARCH_PATHS = @executable_path/../Frameworks PRODUCT_BUNDLE_IDENTIFIER = org.sparkle-project.SparkleTestApp CODE_SIGN_ENTITLEMENTS = TestApplication/Sparkle-Test-App.entitlements ENABLE_HARDENED_RUNTIME = NO ================================================ FILE: Configurations/ConfigTestAppDebug.xcconfig ================================================ // Test Application Debug #include "ConfigTestApp.xcconfig" ================================================ FILE: Configurations/ConfigTestAppHelper.xcconfig ================================================ // Test Application Helper INFOPLIST_FILE = TestAppHelper/Info.plist WRAPPER_EXTENSION = xpc LD_RUNPATH_SEARCH_PATHS = @executable_path/../../../../Frameworks PRODUCT_BUNDLE_IDENTIFIER = org.sparkle-project.TestAppHelper ENABLE_HARDENED_RUNTIME = NO ================================================ FILE: Configurations/ConfigTestAppHelperDebug.xcconfig ================================================ // Test Application Helper Debug #include "ConfigTestAppHelper.xcconfig" ================================================ FILE: Configurations/ConfigUITest.xcconfig ================================================ // UI Test only #include "ConfigSwift.xcconfig" INFOPLIST_FILE = UITests/UITests-Info.plist WRAPPER_EXTENSION = xctest ENABLE_HARDENED_RUNTIME = NO FRAMEWORK_SEARCH_PATHS = $(inherited) $(DEVELOPER_FRAMEWORKS_DIR) LD_RUNPATH_SEARCH_PATHS = @loader_path/../Frameworks TEST_TARGET_NAME = Sparkle Test App USES_XCTRUNNER = YES ================================================ FILE: Configurations/ConfigUITestCoverage.xcconfig ================================================ #include "ConfigUITest.xcconfig" #include "ConfigSwiftDebug.xcconfig" GCC_GENERATE_TEST_COVERAGE_FILES = NO GCC_INSTRUMENT_PROGRAM_FLOW_ARCS = NO ================================================ FILE: Configurations/ConfigUITestDebug.xcconfig ================================================ #include "ConfigUITest.xcconfig" #include "ConfigSwiftDebug.xcconfig" ================================================ FILE: Configurations/ConfigUITestRelease.xcconfig ================================================ #include "ConfigUITest.xcconfig" #include "ConfigSwiftRelease.xcconfig" ================================================ FILE: Configurations/ConfigUnitTest.xcconfig ================================================ // Unit Test only #include "ConfigSwift.xcconfig" INFOPLIST_FILE = Tests/SparkleTests-Info.plist WRAPPER_EXTENSION = xctest OTHER_CFLAGS = $(inherited) -iframework"$(DEVELOPER_FRAMEWORKS_DIR)" -iframework"$(PLATFORM_DIR)/Developer/Library/Frameworks" GCC_SYMBOLS_PRIVATE_EXTERN = NO WARNING_CFLAGS = $(inherited) -Wno-variadic-macros -Wno-gnu-zero-variadic-macro-arguments FRAMEWORK_SEARCH_PATHS = $(inherited) $(DEVELOPER_FRAMEWORKS_DIR) LD_RUNPATH_SEARCH_PATHS = @loader_path/../Frameworks CLANG_ENABLE_MODULES = YES SWIFT_OBJC_BRIDGING_HEADER = Tests/Sparkle Unit Tests-Bridging-Header.h GCC_PREPROCESSOR_DEFINITIONS = $(GCC_PREPROCESSOR_DEFINITIONS) BUILDING_SPARKLE_TESTS=1 BUILDING_SPARKLE_SOURCES_EXTERNALLY=1 ================================================ FILE: Configurations/ConfigUnitTestCoverage.xcconfig ================================================ #include "ConfigUnitTest.xcconfig" #include "ConfigSwiftDebug.xcconfig" GCC_GENERATE_TEST_COVERAGE_FILES = NO GCC_INSTRUMENT_PROGRAM_FLOW_ARCS = NO ================================================ FILE: Configurations/ConfigUnitTestDebug.xcconfig ================================================ #include "ConfigUnitTest.xcconfig" #include "ConfigSwiftDebug.xcconfig" ================================================ FILE: Configurations/ConfigUnitTestRelease.xcconfig ================================================ #include "ConfigUnitTest.xcconfig" #include "ConfigSwiftRelease.xcconfig" ================================================ FILE: Configurations/bsdiff-Debug.xcconfig ================================================ // Copyright © 2019 Sparkle Project. All rights reserved. #include "bsdiff-Shared.xcconfig" ================================================ FILE: Configurations/bsdiff-Release.xcconfig ================================================ // Copyright © 2019 Sparkle Project. All rights reserved. #include "bsdiff-Shared.xcconfig" // Disable asserts for performance GCC_PREPROCESSOR_DEFINITIONS = $(GCC_PREPROCESSOR_DEFINITIONS) NDEBUG=1 ================================================ FILE: Configurations/bsdiff-Shared.xcconfig ================================================ // Copyright © 2019 Sparkle Project. All rights reserved. // Deployment SKIP_INSTALL = YES // Packaging EXECUTABLE_PREFIX = lib PRODUCT_NAME = $(TARGET_NAME) // Search Paths ALWAYS_SEARCH_USER_PATHS = NO // Disable Warnings - this is a third-party project, and we'll trust that the maintainers know what they're doing. WARNING_CFLAGS = $(inherited) -Wno-comma // bsdiff is used by command line tools too ARCHS = $(inherited) $(ARCHS_COMMAND_LINE_TOOLS) ================================================ FILE: Configurations/ed25519-Debug.xcconfig ================================================ // Copyright © 2019 Sparkle Project. All rights reserved. #include "ed25519-Shared.xcconfig" ================================================ FILE: Configurations/ed25519-Release.xcconfig ================================================ // Copyright © 2019 Sparkle Project. All rights reserved. #include "ed25519-Shared.xcconfig" ================================================ FILE: Configurations/ed25519-Shared.xcconfig ================================================ // Copyright © 2019 Sparkle Project. All rights reserved. // Deployment SKIP_INSTALL = YES // Packaging EXECUTABLE_PREFIX = lib PRODUCT_NAME = $(TARGET_NAME) // Search Paths ALWAYS_SEARCH_USER_PATHS = NO // Disable Warnings - this is a third-party project, and we'll trust that the maintainers know what they're doing. WARNING_CFLAGS = $(inherited) -Wno-cast-qual -Wno-conversion -Wno-sign-conversion // Disable dead stores analyzer warning CLANG_ANALYZER_DEADCODE_DEADSTORES = NO // ed25519 is used by command line tools too ARCHS = $(inherited) $(ARCHS_COMMAND_LINE_TOOLS) ================================================ FILE: Configurations/generate_latest_changes.py ================================================ #!/usr/bin/env python3 import os, sys # Ignore the first version line starting with # x.y.z.. # and print everything until the second version line starting with # x.y.z.. hit_first_changelog_note = False with open("CHANGELOG", "r") as changelog_file: sys.stdout.write("Changes:\n\n") for line in changelog_file: if line.startswith("#"): if hit_first_changelog_note: # We are done with printing changes break else: # We haven't hit an important changelog line yet, so continue continue if not hit_first_changelog_note and len(line.strip()) == 0: continue hit_first_changelog_note = True sys.stdout.write(line) ================================================ FILE: Configurations/link-tools.sh ================================================ #!/bin/sh # If Carthage is trying to build us, it won't preserve code signing information from our bundled tools properly # Building Sparkle from source with Carthage is thus not supported if [ "$CARTHAGE" = "YES" ]; then echo "Error: Building Sparkle from source using Carthage is not supported. Please visit https://sparkle-project.org/documentation/ for proper Carthage integration." exit 1 fi # Create symlinks to our helper tools in Sparkle framework bundle # so URLForAuxiliaryExecutable: will pick up the tools. Doing this is supported in the Code Signing in Depth guide. FRAMEWORK_PATH="${TARGET_BUILD_DIR}"/"${FULL_PRODUCT_NAME}" ln -h -f -s "Versions/Current/""${SPARKLE_RELAUNCH_TOOL_NAME}" "${FRAMEWORK_PATH}"/"${SPARKLE_RELAUNCH_TOOL_NAME}" ln -h -f -s "Versions/Current/""${SPARKLE_INSTALLER_PROGRESS_TOOL_NAME}".app "${FRAMEWORK_PATH}"/"${SPARKLE_INSTALLER_PROGRESS_TOOL_NAME}".app ================================================ FILE: Configurations/make-release-package.sh ================================================ #!/bin/bash set -e # Tests the code signing validity of the extracted products within the provided path. # This guards against our archives being corrupt / created incorrectly. function verify_code_signatures() { verification_directory="$1" check_aux_apps="$2" if [[ -z "$verification_directory" ]]; then echo "Provided verification directory does not exist" >&2 exit 1 fi # Search the current directory for all instances of the framework to verify them (XCFrameworks can have multiple copies of a framework for different platforms). find "${verification_directory}" -name "Sparkle.framework" -type d -exec codesign --verify -vvv --deep {} \; if [ "$check_aux_apps" = true ] ; then codesign --verify -vvv --deep "${verification_directory}/Sparkle Test App.app" fi codesign --verify -vvv --deep "${verification_directory}/bin/BinaryDelta" codesign --verify -vvv --deep "${verification_directory}/bin/generate_appcast" codesign --verify -vvv --deep "${verification_directory}/bin/sign_update" codesign --verify -vvv --deep "${verification_directory}/bin/generate_keys" } if [ "$ACTION" = "" ] ; then rm -rf "$CONFIGURATION_BUILD_DIR/staging" rm -rf "$CONFIGURATION_BUILD_DIR/staging-spm" rm -f "Sparkle-$MARKETING_VERSION.tar.xz" rm -f "Sparkle-$MARKETING_VERSION.tar.bz2" rm -f "Sparkle-for-Swift-Package-Manager.zip" mkdir -p "$CONFIGURATION_BUILD_DIR/staging" mkdir -p "$CONFIGURATION_BUILD_DIR/staging-spm" cp "$PROJECT_DIR/CHANGELOG" "$PROJECT_DIR/LICENSE" "$PROJECT_DIR/INSTALL" "$PROJECT_DIR/Resources/SampleAppcast.xml" "$CONFIGURATION_BUILD_DIR/staging" cp "$PROJECT_DIR/CHANGELOG" "$PROJECT_DIR/LICENSE" "$PROJECT_DIR/INSTALL" "$PROJECT_DIR/Resources/SampleAppcast.xml" "$CONFIGURATION_BUILD_DIR/staging-spm" cp -R "$PROJECT_DIR/bin" "$CONFIGURATION_BUILD_DIR/staging" cp "$CONFIGURATION_BUILD_DIR/BinaryDelta" "$CONFIGURATION_BUILD_DIR/staging/bin" cp "$CONFIGURATION_BUILD_DIR/generate_appcast" "$CONFIGURATION_BUILD_DIR/staging/bin" cp "$CONFIGURATION_BUILD_DIR/generate_keys" "$CONFIGURATION_BUILD_DIR/staging/bin" cp "$CONFIGURATION_BUILD_DIR/sign_update" "$CONFIGURATION_BUILD_DIR/staging/bin" cp -R "$CONFIGURATION_BUILD_DIR/Sparkle Test App.app" "$CONFIGURATION_BUILD_DIR/staging" cp -R "$CONFIGURATION_BUILD_DIR/Sparkle.framework" "$CONFIGURATION_BUILD_DIR/staging" cp -R "$CONFIGURATION_BUILD_DIR/Sparkle.xcframework" "$CONFIGURATION_BUILD_DIR/staging-spm" mkdir -p "$CONFIGURATION_BUILD_DIR/staging/Symbols" # Only copy dSYMs for Release builds, but don't check for the presence of the actual files # because missing dSYMs in a release build SHOULD trigger a build failure if [ "$CONFIGURATION" = "Release" ] ; then cp -R "$CONFIGURATION_BUILD_DIR/BinaryDelta.dSYM" "$CONFIGURATION_BUILD_DIR/staging/Symbols" cp -R "$CONFIGURATION_BUILD_DIR/generate_appcast.dSYM" "$CONFIGURATION_BUILD_DIR/staging/Symbols" cp -R "$CONFIGURATION_BUILD_DIR/generate_keys.dSYM" "$CONFIGURATION_BUILD_DIR/staging/Symbols" cp -R "$CONFIGURATION_BUILD_DIR/sign_update.dSYM" "$CONFIGURATION_BUILD_DIR/staging/Symbols" cp -R "$CONFIGURATION_BUILD_DIR/Sparkle Test App.app.dSYM" "$CONFIGURATION_BUILD_DIR/staging/Symbols" cp -R "$CONFIGURATION_BUILD_DIR/Sparkle.framework.dSYM" "$CONFIGURATION_BUILD_DIR/staging/Symbols" cp -R "$CONFIGURATION_BUILD_DIR/Autoupdate.dSYM" "$CONFIGURATION_BUILD_DIR/staging/Symbols" cp -R "$CONFIGURATION_BUILD_DIR/Updater.app.dSYM" "$CONFIGURATION_BUILD_DIR/staging/Symbols" cp -R "$CONFIGURATION_BUILD_DIR/${INSTALLER_LAUNCHER_NAME}.xpc.dSYM" "$CONFIGURATION_BUILD_DIR/staging/Symbols" cp -R "$CONFIGURATION_BUILD_DIR/${DOWNLOADER_NAME}.xpc.dSYM" "$CONFIGURATION_BUILD_DIR/staging/Symbols" fi cp -R "$CONFIGURATION_BUILD_DIR/staging/bin" "$CONFIGURATION_BUILD_DIR/staging-spm" cd "$CONFIGURATION_BUILD_DIR/staging" rm -rf "/tmp/sparkle-extract" mkdir -p "/tmp/sparkle-extract" # Sorted file list groups similar files together, which improves tar compression find . \! -type d | rev | sort | rev | tar --no-xattrs -cJvf "../Sparkle-$MARKETING_VERSION.tar.xz" --files-from=- # Copy archived distribution for CI cp -f "../Sparkle-$MARKETING_VERSION.tar.xz" "../sparkle_dist.tar.xz" # Extract archive for testing binary validity tar -xf "../Sparkle-$MARKETING_VERSION.tar.xz" -C "/tmp/sparkle-extract" # Test code signing validity of the extracted products # This guards against our archives being corrupt / created incorrectly verify_code_signatures "/tmp/sparkle-extract" true rm -rf "/tmp/sparkle-extract" rm -rf "$CONFIGURATION_BUILD_DIR/staging" # Get latest git tag cd "$PROJECT_DIR" latest_git_tag=$( git describe --tags --abbrev=0 || true ) if [ -n "$latest_git_tag" ] ; then # Generate zip containing the xcframework for SPM rm -rf "/tmp/sparkle-spm-extract" mkdir -p "/tmp/sparkle-spm-extract" cd "$CONFIGURATION_BUILD_DIR/staging-spm" # rm -rf "$CONFIGURATION_BUILD_DIR/Sparkle.xcarchive" ditto -c -k --zlibCompressionLevel 9 --rsrc . "../Sparkle-for-Swift-Package-Manager.zip" # Test code signing validity of the extracted Swift package # This guards against our archives being corrupt / created incorrectly ditto -x -k "../Sparkle-for-Swift-Package-Manager.zip" "/tmp/sparkle-spm-extract" verify_code_signatures "/tmp/sparkle-spm-extract" false rm -rf "/tmp/sparkle-spm-extract" rm -rf "$CONFIGURATION_BUILD_DIR/staging-spm" cd "$PROJECT_DIR" # Check semantic versioning if [[ $latest_git_tag =~ ^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)(-((0|[1-9][0-9]*|[0-9]*[a-zA-Z-][0-9a-zA-Z-]*)(\.(0|[1-9][0-9]*|[0-9]*[a-zA-Z-][0-9a-zA-Z-]*))*))?(\\+([0-9a-zA-Z-]+(\.[0-9a-zA-Z-]+)*))?$ ]]; then echo "Tag $latest_git_tag follows semantic versioning" else echo "ERROR: Tag $latest_git_tag does not follow semantic versioning! SPM will not be able to resolve the repository" >&2 exit 1 fi # Generate new Package manifest, podspec, and carthage files cd "$CONFIGURATION_BUILD_DIR" cp "$PROJECT_DIR/Package.swift" "$CONFIGURATION_BUILD_DIR" cp "$PROJECT_DIR/Sparkle.podspec" "$CONFIGURATION_BUILD_DIR" cp "$PROJECT_DIR/Carthage-dev.json" "$CONFIGURATION_BUILD_DIR" fi if [ -z "$latest_git_tag" ] ; then echo "warning: No git repository found so skipping updating package management files" elif [ "$XCODE_VERSION_MAJOR" -ge "1200" ]; then # is equivalent to shasum -a 256 FILE spm_checksum=$(swift package compute-checksum "Sparkle-for-Swift-Package-Manager.zip") rm -rf ".build" sed -E -i '' -e "/let tag/ s/\".+\"/\"$latest_git_tag\"/" -e "/let version/ s/\".+\"/\"$MARKETING_VERSION\"/" -e "/let checksum/ s/[[:xdigit:]]{64}/$spm_checksum/" "Package.swift" cp "Package.swift" "$PROJECT_DIR" echo "Package.swift updated with the following values:" echo "Version: $MARKETING_VERSION" echo "Tag: $latest_git_tag" echo "Checksum: $spm_checksum" sed -E -i '' -e "/s\.version.+=/ s/\".+\"/\"$MARKETING_VERSION\"/" "Sparkle.podspec" "$PROJECT_DIR/Configurations/update-carthage.py" "Carthage-dev.json" "$MARKETING_VERSION" cp "Sparkle.podspec" "$PROJECT_DIR" # Note the Carthage-dev.json file will finally be copied to the website repo in Carthage/Sparkle.json in the end cp "Carthage-dev.json" "$PROJECT_DIR" echo "Sparkle.podspec and Carthage-dev.json updated with following values:" echo "Version: $MARKETING_VERSION" else echo "warning: Xcode version $XCODE_VERSION_ACTUAL does not support computing checksums for Swift Packages. Please update the Package manifest manually." fi rm -rf "$CONFIGURATION_BUILD_DIR/staging-spm" fi ================================================ FILE: Configurations/make-xcframework.sh ================================================ #!/bin/bash # Deleting old products rm -rd "$BUILT_PRODUCTS_DIR/Sparkle.xcarchive" rm -rd "$BUILT_PRODUCTS_DIR/Sparkle.xcframework" xcodebuild archive -scheme Sparkle -archivePath "$BUILT_PRODUCTS_DIR/Sparkle.xcarchive" BUILD_LIBRARY_FOR_DISTRIBUTION=YES SKIP_INSTALL=NO if [ $XCODE_VERSION_MAJOR -ge "1200" ]; then xcodebuild -create-xcframework -framework "$BUILT_PRODUCTS_DIR/Sparkle.xcarchive/Products/Library/Frameworks/Sparkle.framework" -debug-symbols "$BUILT_PRODUCTS_DIR/Sparkle.xcarchive/dSYMs/Sparkle.framework.dSYM" -debug-symbols "$BUILT_PRODUCTS_DIR/Sparkle.xcarchive/dSYMs/Autoupdate.dSYM" -debug-symbols "$BUILT_PRODUCTS_DIR/Sparkle.xcarchive/dSYMs/Updater.app.dSYM" -debug-symbols "$BUILT_PRODUCTS_DIR/Sparkle.xcarchive/dSYMs/$INSTALLER_LAUNCHER_NAME.xpc.dSYM" -debug-symbols "$BUILT_PRODUCTS_DIR/Sparkle.xcarchive/dSYMs/$DOWNLOADER_NAME.xpc.dSYM" -output "$BUILT_PRODUCTS_DIR/Sparkle.xcframework" else echo "warning: Your Xcode version does not support bundling dSYMs in XCFrameworks directly. You should copy them manually into the XCFramework." echo "note: cp '$BUILT_PRODUCTS_DIR/Sparkle.xcarchive/dSYMs/Sparkle.framework.dSYM' '$BUILT_PRODUCTS_DIR/Sparkle.xcframework/your_architecture/dSYMs'" xcodebuild -create-xcframework -framework "$BUILT_PRODUCTS_DIR/Sparkle.xcarchive/Products/Library/Frameworks/Sparkle.framework" -output "$BUILT_PRODUCTS_DIR/Sparkle.xcframework" fi ================================================ FILE: Configurations/release-move-tag.sh ================================================ #!/bin/bash set -e # Convenience script to automatically commit Package.swift after updating the checksum and move the latest tag latest_git_tag=$( git describe --tags --abbrev=0 || true ) # gets the latest tag name if [ -z "$latest_git_tag" ] ; then exit 0 fi commits_since_tag=$(git rev-list ${latest_git_tag}.. --count) function move_tag() { long_message=$(git tag -n99 -l $latest_git_tag) # gets corresponding message long_message=${long_message/$latest_git_tag} # trims tag name long_message="$(echo -e "${long_message}" | sed -e 's/^[[:space:]]*//')" # trim leading whitespace git add Package.swift Sparkle.podspec Carthage-dev.json git commit -m "Update Package management files for version ${latest_git_tag}" git tag -fa $latest_git_tag -m "${long_message}" echo "Package.swift and Sparkle.podspec committed and tag '$latest_git_tag' moved." } if [ "$commits_since_tag" -gt 0 ]; then # If there have been commits since the latest tag, it's highly likely that we did not intend to do a full release echo "WARNING: $commits_since_tag commit(s) since tag '$latest_git_tag'. Did you tag a new version?" echo "Package management files have not been committed and tag has not been moved." elif [ "$CI" == true ]; then move_tag else # TODO: add sanity check to see if version is actually being updated or not? read -p "Do you want to commit changes to Package.swift, Sparkle.podspec, Carthage-dev.json and force move tag '$latest_git_tag'? (required for official release) [Y/n]" -n 1 -r echo # (optional) move to a new line if [[ $REPLY =~ ^[Yy]$ ]]; then move_tag else echo "Package.swift, Sparkle.podspec, and Carthage-dev.json have not been committed and tag has not been moved." fi fi ================================================ FILE: Configurations/set-git-version-info.sh ================================================ #!/bin/sh set -e if ! which -s git ; then exit 0 fi if [ -z "$PROJECT_DIR" ] || \ [ -z "$BUILT_PRODUCTS_DIR" ] || \ [ -z "$INFOPLIST_PATH" ] || \ [ -z "$MARKETING_VERSION" ]; then echo "$0: Must be run from Xcode!" 1>&2 exit 1 fi version="$MARKETING_VERSION" # Get version in format 1.x.x-commits-hash gitversion=$( cd "$PROJECT_DIR"; git describe --tags --match '[12].*' || true ) if [ -z "$gitversion" ] ; then echo "$0: Can't find a Git hash!" 1>&2 exit 0 fi # remove everything before the second last "-" to keep the hash part only versionsuffix=$( echo "${gitversion}" | sed -E 's/.+((-[^.]+){2})$/\1/' ) if [ "$versionsuffix" != "$gitversion" ]; then version="$version$versionsuffix" fi # and use it to set the CFBundleShortVersionString value export PATH="$PATH:/usr/libexec" if [ -f "$BUILT_PRODUCTS_DIR/$INFOPLIST_PATH" ] ; then oldversion=$(PlistBuddy -c "Print :CFBundleShortVersionString" "$BUILT_PRODUCTS_DIR/$INFOPLIST_PATH") fi if [ "$version" != "$oldversion" ] ; then PlistBuddy -c "Set :CFBundleShortVersionString '$version'" \ "$BUILT_PRODUCTS_DIR/$INFOPLIST_PATH" fi ================================================ FILE: Configurations/strip-framework.sh ================================================ #!/bin/sh FRAMEWORK_PATH="${TARGET_BUILD_DIR}"/"${FULL_PRODUCT_NAME}" # Remove any unused XPC Services removedservices=0 if [[ "$SPARKLE_EMBED_INSTALLER_LAUNCHER_XPC_SERVICE" -eq 0 ]]; then rm -rf "${FRAMEWORK_PATH}"/"Versions/"${FRAMEWORK_VERSION}"/XPCServices"/"${INSTALLER_LAUNCHER_NAME}.xpc" removedservices=$((removedservices+1)) fi if [[ "$SPARKLE_EMBED_DOWNLOADER_XPC_SERVICE" -eq 0 ]]; then rm -rf "${FRAMEWORK_PATH}"/"Versions/"${FRAMEWORK_VERSION}"/XPCServices"/"${DOWNLOADER_NAME}.xpc" removedservices=$((removedservices+1)) fi if [[ "$SPARKLE_EMBED_INSTALLER_STATUS_XPC_SERVICE" -eq 0 ]]; then rm -rf "${FRAMEWORK_PATH}"/"Versions/"${FRAMEWORK_VERSION}"/XPCServices"/"${INSTALLER_STATUS_NAME}.xpc" removedservices=$((removedservices+1)) fi if [[ "$SPARKLE_EMBED_INSTALLER_CONNECTION_XPC_SERVICE" -eq 0 ]]; then rm -rf "${FRAMEWORK_PATH}"/"Versions/"${FRAMEWORK_VERSION}"/XPCServices"/"${INSTALLER_CONNECTION_NAME}.xpc" removedservices=$((removedservices+1)) fi if [[ "$removedservices" -eq 4 ]]; then rm -rf "${FRAMEWORK_PATH}"/"Versions/"${FRAMEWORK_VERSION}"/XPCServices" rm -rf "${FRAMEWORK_PATH}"/"XPCServices" fi # Remove any unused nibs if [[ "$SPARKLE_BUILD_UI_BITS" -eq 0 ]]; then rm -rf "${FRAMEWORK_PATH}"/"Versions/"${FRAMEWORK_VERSION}"/Resources"/"SUStatus.nib" rm -rf "${FRAMEWORK_PATH}"/"Versions/"${FRAMEWORK_VERSION}"/Resources"/"Base.lproj/SUUpdateAlert.nib" rm -rf "${FRAMEWORK_PATH}"/"Versions/"${FRAMEWORK_VERSION}"/Resources"/"Base.lproj/SUUpdatePermissionPrompt.nib" rm -rf "${FRAMEWORK_PATH}"/"Versions/"${FRAMEWORK_VERSION}"/Resources"/"ReleaseNotesColorStyle.css" fi # Remove localization files if requested if [[ "$SPARKLE_COPY_LOCALIZATIONS" -eq 0 ]]; then for dir in "${FRAMEWORK_PATH}"/"Versions/"${FRAMEWORK_VERSION}"/Resources"/*; do base=$(basename "$dir") if [[ "$base" =~ .*".lproj" ]]; then if [[ "$base" = "Base.lproj" ]]; then rm -rf "$dir/Sparkle.strings" # Remove Base.lproj if it's empty and the nibs have been stripped out already rmdir "$dir" else rm -rf "$dir" fi fi done fi ================================================ FILE: Configurations/update-carthage.py ================================================ #!/usr/bin/env python3 import os, sys, json if len(sys.argv) < 3: print("Usage: path-to-carthage-file release-tag") sys.exit(1) carthage_file = sys.argv[1] release_tag = sys.argv[2] with open(carthage_file, "r") as json_file: data = json.load(json_file) if release_tag in data: sys.exit(0) with open(carthage_file, "w") as json_file: data[release_tag] = "https://github.com/sparkle-project/Sparkle/releases/download/" + release_tag + "/Sparkle-" + release_tag + ".tar.xz" json.dump(data, json_file) ================================================ FILE: Documentation/.gitignore ================================================ html ================================================ FILE: Documentation/API_README.markdown ================================================ # Sparkle 2 API Reference These are the primary classes and protocols in Sparkle 2 you may be interested in: - `SPUStandardUpdaterController` for creating a standard updater (encapsulates a `SPUUpdater` and `SPUStandardUserDriver`) - `SPUUpdater` for invoking update checks and retrieving updater properties. - `SPUUpdaterDelegate` for delegation methods to control the behavior of `SPUUpdater`. - `SPUUserDriver` for making custom user interfaces. If you are migrating from Sparkle 1, please refer to `SPUStandardUpdaterController` and `SPUUpdater`. Please also visit the [Basic Setup](https://sparkle-project.org/documentation/) guide which shows how to instantiate an updater in a nib or how to create one programmatically. ================================================ FILE: Documentation/Design Practices.md ================================================ # Design Practices ## XPC Services XPC services in Sparkle are all optional, so the code involved in the services needs to be usable directly from the framework as well. For this to work well, if the class used in the XPC service takes a delegate, it must not be weakly referenced, so the retain cycle will have to be broken explicitly (via an explicit added invalidate method). dealloc also must not be implemented (do cleanup in custom invalidate method). As one may tell, two of the services are simply proxies (InstallerConnection, InstallerStatus) -- we now recommend most developers to set up a temporary exception with their bundle ID rather than use these two services. The protocols used in XPC services must also not adopt other protocols (i.e, one protocol inheriting from another protocol). This is because the XPC protocol decoder on older supported systems doesn't properly handle this case, and won't be able to find that methods exist on a class. ## Singletons Singletons and other global mutable variables have been either removed entirely or completely avoided with an exception to backwards compatibility. They have no place in a well architectured framework. `SPUUpdater` doesn't maintain singleton instances and can now be properly deconstructed. Note because a caller is not expected to explicitly invalidate an updater, this means that the updater needs to avoid getting into a retain cycle. Intermediate clasess were created for the update cycle and schedule timer to avoid just this. One may argue that we shouldn't allow multiple live updaters running at the same bundle simultaneously, but I disagree and I think that is missing the point. It also does not account for updaters running external to the process. For example, it may be perfectly reasonable to start an update from `sparkle-cli` that defers the installation until quit, and have the application that is being updated be able to resume that installation and relaunch immediately. The original `SUUpdater` may have also been created to assist plug-ins and other non-app bundles. My advice there is in order to be truly safe, you must not inject a framework like Sparkle into the host application anyway. An external tool that is bundled like `sparkle-cli` may be more appropriate to use here. ## Extensibility Sparkle 2.0 does not support subclassing classes internal (not exported) to Sparkle anymore. Doing so would be almost impossible to maintain into the future. Subclassing in general has been banned. Composition is preferred everywhere, even amongst the internal update drivers which were rewritten to follow a protocol oriented approach. The reason why composition is preferred is because it's easier to follow the flow of logic. I hope the user driver API gives enough extensibility without someone wanting to create another fork. ## Delegation Newer classes, other than assisting backwards compatibility, that support delegation don't pass the delegator around anymore. Doing so has some [bad consequences](https://zgcoder.net/ramblings/avoid-passing-the-delegator) and makes code hard to maintain. Optional delegate methods that have return types need to be optional or have known default values for primitive types. You may notice that the delegate and user driver are not accessible as properties from `SPUUpdater`. This is intentional. The methods that belong to these types aren't meaningful to any caller except from internal classes. ## Decoupling Two software components should not directly know about each other. Preferably they wouldn't know about each other at all, but if they must, they can use the delegation pattern with a declared protocol. See `Documentation/graph-of-sparkle.png` for a graph of how the code looks like currently. This was generated via [objc_dep](https://github.com/nst/objc_dep) (great tool). Note that there are no red edges which would mean that two nodes know of each other. ## Attributes & Code Size Instance variables and instance variable access should be used for private members (declared in `@implementation` block) whenever possible over properties. Preferring instance variables over properties for internal usage can significantly reduce code size. Instance variables should also be ordered by having the larger sized data members declared first. `SPU_OBJC_DIRECT_MEMBERS` should be used for any internal class in Sparkle and `SPU_OBJC_DIRECT` should be used for any other internal methods to Sparkle that doesn't need to utilize the Obj-C runtime to reduce code size. Note these attributes should not be used for *any* class or method that is exported to the developer (this includes private headers / APIs we carefully decided to expose too). For internal methods and classes that are also used by our Swift tools or unit tests, we may not expose them as direct specifically when building for those targets. `nonatomic` should really be used wherever possible with regards to obj-c properties (`atomic` is a bad default). `readonly` should be used wherever possible as well, which also implies that only ivar access should be used in initializers. `NS_ASSUME_NONNULL_BEGIN` and `NS_ASSUME_NONNULL_END` should be used around new headers whenever possible. AppKit prevention guards should be used for any non-UI class whenever possible. Sparkle has several feature flags in ConfigCommon.xcconfig (e.g. `SPARKLE_BUILD_LEGACY_SUUPDATER`, `SPARKLE_BUILD_LEGACY_DSA_SUPPORT`, `SPARKLE_BUILD_UI_BITS`, etc). This allows disabling any combination of these features and building Sparkle with a more minimal feature set. These flags (with the exception for stripping UI bits, localizations, or XPC Services) are for disabling features that are legacy or not recommended to use for most applications. Note when altering these flags, `OTHER_SWIFT_FLAGS_COMMON` may need to be updated appropriately too. ================================================ FILE: Documentation/Installation.md ================================================ # Details on Installer IPC & Security ## Important components: * The bundle to update & replace * The application to listen for termination & to relaunch. This can be the same bundle as the one being updated, but this doesn't have to be the case (eg: the bundle being updated could be a plug-in that is hosted by another application). * The updater that lives in Sparkle.framework - this schedules & downloads updates and starts the installer. This updater's life can be tied to the application's lifetime, but this doesn't have to be the case. * The installer (Autoupdate) which does all the extraction, validation, and installation work. This program is submitted by the updater and is run as a launchd agent or daemon, depending on the type of installation and if elevated permissions are needed to perform the update. * The progress or agent application which hosts the installation status service and is also responsible for showing UI if the installation takes a sufficiently long period of time, as well as responsible for launching the new update. ## After downloading the update: ### Launching the Installer SPUCoreBasedUpdateDriver invokes SPUInstallerDriver's extraction method which invokes SUInstallerLauncher's launch installer method. An XPC Service may be used to launch the installer if necessary for Sandboxed applications. First, if the updater driver that invoked the launcher doesn't allow for interaction (in particular, the automatic/silent update driver does not allow interaction), then the launcher fails with a hint to try again with interaction enabled (in particular, with a UI based update driver). If this happens, the updater keeps a reference to the downloaded update and appcast item that is used for resuming with a UI based update driver later. The launcher in this case is telling that authorization can only be done if interaction is allowed from the driver, so the user can enter their password to start up the installer as root to install over a location that requires such privileges. The launcher then looks for the installer and agent application. If we are inside the XPC Service, a relative path back to the framework auxiliary directory is computed otherwise the bundle's auxiliary directory path is retrieved. Once those paths are found, the launcher copies the progress agent application to a temporary cache location. The installer is not copied because it is a plain single executable that does not rely on external relative dependencies, whereas the agent application relies on bundle resources. This is important to consider because the old application may be moved around and removed when the installation process occurs. Leaving the installer inside its bundle also could leave it in a place the user doesn't necessarily have write privileges to. The installer is first submitted. If it requires root privileges, it will ask for an admin user name and password. How it determines if root privileges are necessary is based on: * If the installation type from the appcast is a guided or regular package based installer, then root privileges are necessary because the system installer utility has to run as root. * Otherwise the installation type is a normal application update, and root privileges are only needed if write permission or changing the owner is currently insufficient. The installer makes sure, after extraction, that the expected installation type from the appcast matches the type of installation found within the archive. So this hint from the appcast is not just trusted implicitly. If the user cancels submitting the installer on authorization (if it's required), the launcher aborts the installation process silently. If everything goes alright, the progress agent application is submitted next. If either of the submissions fail, the installation fails and aborts, otherwise the launcher succeeds and its job is done. Note before the submissions are done, the jobs are removed from launchd, and the jobs are submitted in a way that act as "one-time" jobs. If they fail or succeed, the jobs aren't restarted, and the jobs aren't installed in a system location for launchd to attempt launching again. The most important argument passed to the installer is the host bundle identifier of the bundle to update. This bundle identifier is used so that the installer and updater know what Mach service to connect to for IPC. Mostly everything else to the installer is sent via a XPC connection. The host bundle path is passed to the progress agent, which is just used for obtaining the bundle identifier, and for UI purposes (eg: displaying app icon and name when the app needs to show progress). The type of domain the installer is running in (user vs system) is also passed to the progress agent application so it can know which domain to use to connect to the installer. ### Timeouts Upon starting the progress agent, it tries to establish a connection to the installer and send a "hello" message expecting a reply back. If a reply isn't received back within a certain threshold, the agent application aborts (unless it's already planning to terminate in the near future after relaunching the new update). If the installer doesn't receive the "hello" message from the agent in a certain threshold, the installer aborts. If the installer doesn't receive the installation input data from the data in a certain threshold also, it aborts there too. If the installer aborts and the agent is connected to the installer, the agent will abort as well. When the updater attempts to set up a connection to the installer, after a certain threshold, if the installation hasn't progressed from not beginning (i.e, extraction hasn't begun), then the updater aborts the installation. ### Installation Data After the installer launches, the updater creates a connection to the installer and sends installation data to the installer using the `SPUInstallationData` message. This data includes the bundle application path to relaunch, the path to the bundle to update and replace, Ed(DSA) signature from the appcast item, decryption password for dmg if available, the path to the downloaded directory and name of the downloaded item, and the type of expected installation. The installer takes note of all this information, but it moves the downloaded item into a update directory it chooses for itself. This is for two reasons: 1. If the installer is running at a higher level than the process that submitted it (eg: sandboxed -> user, or user -> root), it make sense from a security perspective to move the file into its own support directory. This could prevent an attacker from manipulating the download while the installer is using it. 2. The updater, or another updater running, or someone else probably won't accidently remove the downloaded item if the installer decides to 'own' it. After the installer moves the downloaded item, it makes sure the item is not a symbolic link. If it is a non-regular file, the installer aborts, causing the updater to abort as well. Note this check is done after the move rather than before due to potential race conditions an attacker could create. ### Update Extraction After the installer receives the input installation data, it starts extracting the update. The installer first sends a `SPUExtractionStarted` message. If the unarchiver requires EdDSA validation before extraction (binary delta archives do) or if the expected installation type is a package type, then before starting the unarchiver, validation is checked to make sure the signature for the archive is valid. If the signature is not valid, then a `SPUArchiveExtractionFailed` message is sent to the updater (see below on what happens after that). We can't require validation before extracting for normal application updates because they allow changes to EdDSA keys. Delta updates are fine however because if those fail, then a full update can be tried. After extraction starts, one or more `SPUExtractedArchiveWithProgress` messages indicating the unarchival progress are sent back to the updater. On failure, the installer will send `SPUArchiveExtractionFailed`. In the updater, if the update is a delta update and extraction fails, then the full archive is downloaded and we go back to the "Sending Installation Data" section to re-send the data and begin extraction again. If the update is not a delta update and extraction fails on the other hand, then the updater aborts causing the installer to abort as well. ### Post Validation If the unarchiving succeeds, a `SPUValidationStarted` message is sent back to the updater, and the installer begins post validating the update. If validation was already done before extraction, just the code signature is checked on the extracted bundle to make sure it's valid (if there's a code signature on the bundle). This is just a consistency check to make sure the developer didn't make a careless error. If validation fails, the installer aborts, causing the updater to abort the update as well. Otherwise, the installer sends a message `SPUInstallationStartedStage1` to the updater and begins the installation. ### Retrieving Process Identifier If the installer hasn't made a connection to the agent yet within the time threshold, the installer waits until a connection is made. Otherwise, the installer asks the agent for the process identifier of the application bundle that it wants to wait for termination. If the installer doesn't get a response within a certain threshold, the installer aborts. When the installer gets the process identifier, it begins the installation. At this point on, the installer will try to perform the installation even if the connection to the updater or the connection to the agent invalidates. ### Starting the Installation The installer figures out what kind of installer to use (regular, guided pkg) based on the type of file inside the archive. If the type of file doesn't match the expected installation type from the input installation data, then the installer aborts causing the updater to abort as well. Once the type of installer is found, the first stage of installation is performed: * Regular application installer 1st stage: Makes sure this update is not a downgrade and do all initial installation work if possible (such as clearing quarantine, changing owner/group, updating modification date, invoking GateKeeper scan). This initial work is not done if the bundle has to transfer to another volume; in this case, this work will be done in a later stage. * Guided Package installer 1st stage: Does nothing. If the first stage fails, the installer aborts causing the updater to abort the update. Otherwise a `SPUInstallationFinishedStage1` message is sent back to the updater along with some data. This data includes whether the application bundle to relaunch is currently terminated, and whether the installation at later stages can be performed silently (that is, with no user interaction allowed). If we reach here, only the interactive package installer can't be performed silently (however interactive pkg installer is no longer supported). The installer then listens and waits for the target application to relaunch terminates. If it is already terminated, then it resumes to stage 2 and 3 of the installation immediately on the assumption that the installer does not have permission to show UI interaction to the user. Thus if the installer has to show user interaction here and hasn't received an OK from the updater (it won't if the target application is already terminated), the install will fail. If the target is already terminated, the installer will also assume that the target should not be relaunched after installation. ### Installation Waiting Period The updater receives `SPUInstallationFinishedStage1` message. The updater sends a message `SPUSentUpdateAppcastItemData` with the appcast data in case the updater may request for it later (due to installer resumability, discussed later). It also reads if the target has already been terminated (implying that the installer will continue installing the update immediately), and if the installation will be done silently. For UI based update drivers, the updater tells the user driver to show that the application is ready to be relaunched - the user can continue to install & relaunch the app. The user driver is only alerted however if the installation isn't happening immediately (that is, if the target application to relaunch is still alive). The user driver can decide whether to a) install b) install & relaunch or c) delay installation. If installation is delayed, it can be resumed later, or if the target application terminates, the installer will try to continue installation if it is capable to without user interaction. For automatic based drivers, if the update is not going to be installed immediately and if it can be installed silently, the updater's delegate has a choice to handle the immediate installation of the update. If the delegate handles the installation, it can invoke a block that will trigger the automatic update driver to tell the installer to resume to stage 2 as detailed in step the "Continue to Installation" section - except without displaying any user interface and by relaunching the application afterwards. If the delegate handles the immediate installation, the automatic update driver will not abort, it will just leave the driver running until the installer requests for the app to be terminated later. This means the update can't be resumed later and the user driver won't be involved. Otherwise if the updater delegate doesn't handle immediate installation for automatic based drivers (assuming still the update is not going to be installed immediately), the update driver is aborted; the installer will still wait for the target to terminate however. If the update cannot be silently installed or if the update is marked as critical from the appcast, the update procedure is actually 'resumed' as a scheduled UI based update driver immediately. The update driver can also be 'resumed' later when the user initiates for an update manually or when a long duration (I think a week) has passed by without the user terminating the application. Note automatic based drivers are unable to do a resume, so only UI based ones can. If an update driver is resumed (which cannot happen if the target application is already terminated by the way), then the updater first requests the installer for the appcast item data that the installer received before. The updater does this by creating a temporary distinct connection for the purpose of querying for the installation status. The connection will give up if a short timeout passes. If the updater fails to retrieve resume data, it assumes that there's no update to resume and will start back from the beginning. The updater can use this data for showing release notes, etc. Note the updater and target application don't have to live in the same process, and the updater could choose to terminate and resume later as a new process - so having the installer keep the appcast item data is nice. Afterwards the resumed update driver then allows the user driver to decide whether to a) install the update now, b) install & relaunch the update, or to c) delay the update installation and abort the update driver. Note we are now back to the same options discussed earlier. ### Continue to Installation If the user driver decides to install the update, it sends a `SPUResumeInstallationToStage2` message to the installer and supplies whether the update should be relaunched, and whether user interface can be displayed. The user driver specifies that the user interface can be displayed as long as the updater delegate allows interaction. If the updater's delegate handles immediate installation in the automatic based update driver, UI cannot be displayed there too. The installer receives `SPUResumeInstallationToStage2` and reads whether it should relaunch the target application and whether it can show UI (thus be allowed to show user interaction). The installer then resumes to stage 2 of the installation if it has not been performed already (that is if the target app already terminated). Note if the installer doesn't receive this message before the target application terminates, then the installer will not relaunch or show UI and resume stage 2 & 3 by itself. Again not displaying UI is only an issue for interactive based package installers which are no longer supported. If the 2nd stage succeeds, the installer sends a `SPUInstallationFinishedStage2` message back to the updater, including if the target application has already terminated at this time. If the target application has not already been terminated, the installer sends a request to the progress agent to *terminate* the application. By *terminate*, we mean sending an Apple quit event, allowing the application or user to possibly cancel or delay termination. The updater receives a `SPUInstallationFinishedStage2` message, and reads if the target application had already been terminated. If the target application has not already terminated, the updater tells the user driver to show that the application is being sent a termination request. In the case termination of the application is delayed or canceled, the user driver may re-try sending a `SPUResumeInstallationToStage2` message to the installer. This time the installer will recognize it already completed stage 2. If it hasn't proceeded onto stage 3 it will send another quit event to the application. This re-try can be done multiple times. ### Showing Progress When the target application is terminated, if the updater allowed the installer to show UI progress and the installation type doesn't show progress on its own (only interactive package installer shows progress on its own, but these are no longer supported), then the installer sends a `SPUUpdaterAlivePing` message to the updater. If the updater is still alive by now and receives the message, the updater will then send back a `SPUUpdaterAlivePong` message. This lets the installer know that the updater is still active after the target application is terminated, and whether the installer should later be responsible for displaying updater progress or not if a short time passes by, and the installation is still not finished. If the updater is still not alive, then the installer should be responsible for showing progress if otherwise allowed. If the installer decides to show progress, it sends a message to the progress agent to show progress UI. Under most circumstances, the installation will finish faster than this point is reached however (exceptions may be guided package installers and updates over a remote network mount). If the connection to the updater is still connected after this short time passes (eg: like with sparkle-cli), then it's the updater's job to show progress instead. ### Finishing the Installation The installer starts stage 3 of the installation after the target application is terminated as well. The third stage does the final installation for updating and replacing the new updated bundle. If the third stage fails, then the installer aborts, causing the updater driver to abort if it's still running. The target application is not ever relaunched on failure. Otherwise if the third stage succeeds, the installer sends a message to the agent to stop showing progress. The agent uses this hint to acknowledge that it should stop broadcasting the status info service for the updater. If the connection to the updater is still alive, a `SPUInstallationFinishedStage3` message is sent back to the updater and the updater driver silently aborts the update. The installer then sends a message to the agent to relaunch the new application if the installer was requested to relaunch it. This also signals to the agent that it will terminate shortly. If the installer decides not to relaunch the update, the agent will terminate when its connection to the installer invalidates. The installer lastly does cleanup work by removing its update directory it was using and exits. When the progress agent exits, it cleans up by removing itself from disk. The installer doesn't do that because the installer isn't copied to another location in the first place. ## Notes: * Performing an update as the root user is not currently supported. The command line driver (minorly) and the agent application link to AppKit, and running the agent as a different user (i.e, dropping from root -> logged in user) is not particularly the most elegant idea. * We use IPC in such a way that the installer process does not trust the updater process, which is why the installer does extraction, validation, and installation -- all in a single process. The launcher portion of code (which could be running as a XPC service) also does not trust the updater for determining the paths to the installer and agent tool. This portion of code does not check the code signing signature of the installer or agent though for reasons explained in the code. * We have timeouts and ping/pong messages in various cases because we don't want to trust the other end very much for communicating back quickly. On older systems in my testing (eg: 10.8), connection invalidation events don't even get sent to the other end when interacting with launchd, possibly due a bug that is outputted in Console.app. * When we terminate the application before doing final installation work, we send an Apple quit event to all running application instances we find under the agent's GUI login session. This does not include running instances from other logged in sessions -- that may require authenticating all the time, so trade off is not worthwhile. Also currently the installer only handles a single process identifier to watch for until termination, and certainly doesn't handle the case if more instances are launched afterwards. Note all of this only applies to instances being launched from the same bundle path, and the consequence may rarely be updating a bundle when a running instance is still left alive. * SMJobBless() would be interesting to explore and perhaps more suitable for sparkle-cli than ordinary application updaters because using this API is less appropriate for one-off installations. Furthermore, the installer would have to change to persist rather than be a one-time install, and may have to handle multiple connections for installing multiple bundles simultaneously. ================================================ FILE: Documentation/Security.md ================================================ # Security First, some references I've found to be quite useful: * WWDC 2010 video on "Creating Secure Applications" - [asciiwwdc](http://asciiwwdc.com/2010/sessions/204?q=security). * [EvenBetterAuthorizationExample](https://developer.apple.com/library/mac/samplecode/EvenBetterAuthorizationSample/Introduction/Intro.html) sample code by Apple for showing off privileged authorization as well as in sandboxed applications. * Apple's [Daemons and Agents Technote](https://developer.apple.com/library/mac/technotes/tn2083/_index.html). Outdated and before XPC existed, but insightful. Sparkle 2.0 puts a huge emphasis on splitting Sparkle into several components to achieve privilege separation. * User Driver (Application; AppKit permitted) * Updater Scheduler (Framework) * XPC Services (Instead of some portions of Framework) * Progress Agent (Application; AppKit permitted) * Installer (Agent and Daemon safe) These achieve privilege separation, because at least theoretically, they can all be placed into different processes from one another (except for the user driver & updater scheduler). For XPC Services, it's significant to understand they can be used independent of sandboxing (but this is strongly discouraged). Besides privilege separation, they also have an impact on fault tolerance and termination. We have code that detects whether or not XPC services are available and enabled by the main application bundle. This is simpler and more efficient than attempting to create a connection and wait for a timeout. We don't have *any* checks for seeing if the "current process" is sandboxed; doing so is a rather brittle. The XPC Services are important, not the sandboxing. I said above that XPC services are looked up in the main bundle. One may think this assumption doesn't hold for helpers or plug-ins inside applications. My response to that is they probably don't need the services, and they probably shouldn't be injecting Sparkle inside the host application anyway due to unintended conflicts and other consequences. A more appropriate approach may be to bundle a separate tool such as the sparkle command line utility. The `InstallerLauncher` XPC service needs a `JoinExistingSession` key set to `YES` otherwise authorization will not work properly, and even less so on older systems. It took me forever to debug this, so it's worth mentioning. When developing Sparkle 2.0, I still had a huge hole in the privilege separation even after supporting XPC Services and sandboxing. I fixed that by removing references to `AuthorizationExecuteWithPrivileges` and submitting a launchd agent/daemon for the installer. This is overlooked, but absolutely essential for our model to be secure. When communicating to a launchd job, we've several options to use for IPC such as BSD Sockets, Mach ports, XPC (note: XPC is an IPC API and is != "XPC Services"), and file IO. The technote I linked above describes why Mach ports have user namespace issues and why BSD sockets may be preferred over them. However, now, XPC which allows you to choose which domain to look up is the simplest, best, and most modern choice. It's crucial in any event we don't have a daemon (running in system domain) try to *connect* to something in the user domain. Our launchd tasks should also be connected to rather than the other way around. As for communication via file IO, I just wouldn't go anywhere close to trusting such a model mixing in system and user running processes. For sending Obj-C objects across the wire, we use `SPUSecureCoding` because Cocoa doesn't support sending objects securely out of the box unless working with XPC Services. It's very important the objects implement `NSSecureCoding` and whitelist types that are expected to be decoded, as well as whitelist types inside collections before decoding them. The installer handles extraction, validation, and installation of the update. This was the main reason why other sandboxed-capable forks were rejected from being secure. The point to be stressed anyway is that these all need to be done in the same process; they can't be handed off by some other process. This is also why removing references to `AuthorizationExecuteWithPrivileges` is crucial as well. Note that the installer does *not* handle downloading. That is handled by the updater framework. This is ideal because downloading can be done without disrupting the user, allowing it to be done silently without presenting an authorization dialog. The downloading portion of code can also be stuck into a XPC service with just an entitlement for allowing incoming connections. This also means for secure installations the installer cannot know (or trust) what protocol the update was downloaded from (i.e: http vs https). A good installer will not care, which is why applying updates without a EdDSA signature/key is now deprecated (reminder that Apple's code signature checks are not intended for complete integrity). The XPC Services and Autoupdate, if code signed, also check if the clients are code signed with the same team ID and may reject connections or reject policies if this is not the case. This is a "backup" check because we still do not want endpoints to open up and allow arbitrary operations across different levels of security boundaries, even if apps are not code signed. ================================================ FILE: Downloader/Downloader.entitlements ================================================ com.apple.security.app-sandbox com.apple.security.network.client ================================================ FILE: Downloader/Info.plist ================================================ CFBundleDevelopmentRegion en CFBundleExecutable $(EXECUTABLE_NAME) CFBundleIdentifier $(PRODUCT_BUNDLE_IDENTIFIER) CFBundleInfoDictionaryVersion 6.0 CFBundleName $(PRODUCT_NAME) CFBundlePackageType XPC! CFBundleShortVersionString $(MARKETING_VERSION) CFBundleSignature ???? CFBundleVersion ${CURRENT_PROJECT_VERSION} NSAppTransportSecurity NSAllowsArbitraryLoads NSHumanReadableCopyright Copyright © 2016 Sparkle Project. All rights reserved. XPCService ServiceType Application RunLoopType NSRunLoop ================================================ FILE: Downloader/SPUDownloader.h ================================================ // // SPUDownloader.h // Downloader // // Created by Mayur Pawashe on 4/1/16. // Copyright © 2016 Sparkle Project. All rights reserved. // #import #import "SPUDownloaderProtocol.h" @protocol SPUDownloaderDelegate; // This object implements the protocol which we have defined. It provides the actual behavior for the service. It is 'exported' by the service to make it available to the process hosting the service over an NSXPCConnection. SPU_OBJC_DIRECT_MEMBERS @interface SPUDownloader : NSObject // Due to XPC remote object reasons, this delegate is strongly referenced // Invoke cleanup when done with this instance - (instancetype)initWithDelegate:(id )delegate; @end ================================================ FILE: Downloader/SPUDownloader.m ================================================ // // SPUDownloader.m // Downloader // // Created by Mayur Pawashe on 4/1/16. // Copyright © 2016 Sparkle Project. All rights reserved. // #import "SPUDownloader.h" #import "SPUDownloaderDelegate.h" #import "SPULocalCacheDirectory.h" #import "SPUDownloadData.h" #import "SPUDownloadDataPrivate.h" #import "SUErrors.h" #include "AppKitPrevention.h" typedef NS_ENUM(NSUInteger, SPUDownloadMode) { SPUDownloadModePersistent, SPUDownloadModeTemporary }; static NSString *SUDownloadingReason = @"Downloading update related file"; @interface SPUDownloader () @end @implementation SPUDownloader { NSURLSessionTask *_sessionTask; NSURLSession *_downloadSession; NSString *_bundleIdentifier; NSString *_desiredFilename; // Delegate is intentionally strongly referenced; see header id _delegate; SPUDownloadMode _mode; BOOL _disabledAutomaticTermination; BOOL _receivedExpectedBytes; } - (instancetype)initWithDelegate:(id )delegate { self = [super init]; if (self != nil) { _delegate = delegate; } return self; } - (void)startDownloadWithRequest:(NSURLRequest *)request SPU_OBJC_DIRECT { if (request == nil) { NSString *message = @"The download request must not be nil"; NSError *error = [NSError errorWithDomain:SUSparkleErrorDomain code:SUDownloadError userInfo:@{ NSLocalizedDescriptionKey: message }]; [_delegate downloaderDidFailWithError:error]; return; } // Prevent any unwanted URL schemes (e.g. file://) NSString *scheme = request.URL.scheme; if (scheme == nil) { NSString *message = @"The download request scheme must not be nil"; NSError *error = [NSError errorWithDomain:SUSparkleErrorDomain code:SUDownloadError userInfo:@{ NSLocalizedDescriptionKey: message }]; [_delegate downloaderDidFailWithError:error]; return; } if ([scheme caseInsensitiveCompare:@"http"] != NSOrderedSame && [scheme caseInsensitiveCompare:@"https"] != NSOrderedSame) { NSString *message = [NSString stringWithFormat:@"The download request URL must use http or https (%@)", request.URL]; NSError *error = [NSError errorWithDomain:SUSparkleErrorDomain code:SUDownloadError userInfo:@{ NSLocalizedDescriptionKey: message }]; [_delegate downloaderDidFailWithError:error]; return; } _downloadSession = [NSURLSession sessionWithConfiguration:[NSURLSessionConfiguration defaultSessionConfiguration] delegate:self delegateQueue:[NSOperationQueue mainQueue]]; switch (_mode) { case SPUDownloadModePersistent: _sessionTask = [_downloadSession downloadTaskWithRequest:request]; break; case SPUDownloadModeTemporary: { __weak __typeof__(self) weakSelf = self; _sessionTask = [_downloadSession dataTaskWithRequest:request completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) { [weakSelf temporaryDownloadDidFinishWithData:data response:response error:error]; }]; break; } } [_sessionTask resume]; } // Don't implement dealloc - make the client call cleanup, which is the only way to remove the reference cycle from the delegate anyway - (void)startPersistentDownloadWithRequest:(NSURLRequest *)request bundleIdentifier:(NSString *)bundleIdentifier desiredFilename:(NSString *)desiredFilename { dispatch_async(dispatch_get_main_queue(), ^{ if (self->_sessionTask == nil && self->_delegate != nil) { // Prevent service from automatically terminating while downloading the update asynchronously without any reply blocks [[NSProcessInfo processInfo] disableAutomaticTermination:SUDownloadingReason]; self->_disabledAutomaticTermination = YES; self->_mode = SPUDownloadModePersistent; self->_desiredFilename = desiredFilename; self->_bundleIdentifier = [bundleIdentifier copy]; [self startDownloadWithRequest:request]; } }); } - (void)startTemporaryDownloadWithRequest:(NSURLRequest *)request { dispatch_async(dispatch_get_main_queue(), ^{ if (self->_sessionTask == nil && self->_delegate != nil) { // Prevent service from automatically terminating while downloading the update asynchronously without any reply blocks [[NSProcessInfo processInfo] disableAutomaticTermination:SUDownloadingReason]; self->_disabledAutomaticTermination = YES; self->_mode = SPUDownloadModeTemporary; [self startDownloadWithRequest:request]; } }); } - (void)enableAutomaticTermination SPU_OBJC_DIRECT { if (_disabledAutomaticTermination) { [[NSProcessInfo processInfo] enableAutomaticTermination:SUDownloadingReason]; _disabledAutomaticTermination = NO; } } - (NSString *)rootPersistentDownloadCachePathForBundleIdentifier:(NSString *)bundleIdentifier SPU_OBJC_DIRECT { // Note: The installer verifies this "PersistentDownloads" path component return [[SPULocalCacheDirectory cachePathForBundleIdentifier:bundleIdentifier] stringByAppendingPathComponent:@"PersistentDownloads"]; } - (void)removeDownloadDirectoryWithDownloadToken:(NSString *)downloadToken bundleIdentifier:(NSString *)bundleIdentifier { // Only take the directory name (from the download token) and compute most of the base path ourselves // This way we do not have to send/trust an absolute path // The downloader instance that creates this temp directory isn't necessarily the same as the one // that clears it (eg upon skipping an already downloaded update), so we can't just preserve it here too dispatch_async(dispatch_get_main_queue(), ^{ if (bundleIdentifier != nil && downloadToken != nil) { NSString *rootPersistentDownloadCachePath = [self rootPersistentDownloadCachePathForBundleIdentifier:bundleIdentifier]; if (rootPersistentDownloadCachePath != nil) { NSString *sanitizedDownloadToken = downloadToken.lastPathComponent; NSString *tempDir = [rootPersistentDownloadCachePath stringByAppendingPathComponent:sanitizedDownloadToken]; [[NSFileManager defaultManager] removeItemAtPath:tempDir error:NULL]; } } }); } - (void)_cleanup SPU_OBJC_DIRECT { [self enableAutomaticTermination]; [_sessionTask cancel]; [_downloadSession finishTasksAndInvalidate]; _sessionTask = nil; _downloadSession = nil; _delegate = nil; } - (void)cleanup:(void (^)(void))completionHandler { dispatch_async(dispatch_get_main_queue(), ^{ [self _cleanup]; if (completionHandler != NULL) { completionHandler(); } }); } static bool SPUValidateStatusCodeAndFailIfInvalid(NSURLResponse * _Nullable response, NSURL *url, id delegate) { NSInteger statusCode = [response isKindOfClass:[NSHTTPURLResponse class]] ? ((NSHTTPURLResponse *)response).statusCode : 200; if ((statusCode < 200) || (statusCode >= 400)) { NSString *message = [NSString stringWithFormat:@"A network error occurred while downloading %@. %@ (%ld)", url.absoluteString, [NSHTTPURLResponse localizedStringForStatusCode:statusCode], (long)statusCode]; NSError *error = [NSError errorWithDomain:SUSparkleErrorDomain code:SUDownloadError userInfo:@{ NSLocalizedDescriptionKey: message }]; [delegate downloaderDidFailWithError:error]; return false; } else { return true; } } - (void)temporaryDownloadDidFinishWithData:(NSData * _Nullable)data response:(NSURLResponse * _Nullable)response error:(NSError * _Nullable)error SPU_OBJC_DIRECT { if (!SPUValidateStatusCodeAndFailIfInvalid(response, _sessionTask.originalRequest.URL, _delegate)) { return; } SPUDownloadData *downloadData = nil; if (data != nil) { NSURL *responseURL = response.URL; if (responseURL == nil) { responseURL = _sessionTask.currentRequest.URL; } if (responseURL == nil) { responseURL = _sessionTask.originalRequest.URL; } assert(responseURL != nil); downloadData = [[SPUDownloadData alloc] initWithData:(NSData * _Nonnull)data URL:responseURL textEncodingName:response.textEncodingName MIMEType:response.MIMEType]; } _sessionTask = nil; if (downloadData != nil) { [_delegate downloaderDidFinishWithTemporaryDownloadData:downloadData]; } else { NSMutableDictionary *userInfo = [@{NSLocalizedDescriptionKey: @"Failed to download temporary data."} mutableCopy]; if (error != nil) { userInfo[NSUnderlyingErrorKey] = error; } [_delegate downloaderDidFailWithError:[NSError errorWithDomain:SUSparkleErrorDomain code:SUDownloadError userInfo:userInfo]]; } [self _cleanup]; } - (void)URLSession:(NSURLSession *)__unused session downloadTask:(NSURLSessionDownloadTask *)downloadTask didFinishDownloadingToURL:(NSURL *)location { if (!SPUValidateStatusCodeAndFailIfInvalid(downloadTask.response, downloadTask.originalRequest.URL, _delegate)) { return; } // Remove our old caches path so we don't start accumulating files in there NSString *rootPersistentDownloadCachePath = [self rootPersistentDownloadCachePathForBundleIdentifier:_bundleIdentifier]; [SPULocalCacheDirectory removeOldItemsInDirectory:rootPersistentDownloadCachePath]; NSString *tempDir = [SPULocalCacheDirectory createUniqueDirectoryInDirectory:rootPersistentDownloadCachePath]; if (tempDir == nil) { // Okay, something's really broken with this user's file structure. [_sessionTask cancel]; _sessionTask = nil; NSError *error = [NSError errorWithDomain:SUSparkleErrorDomain code:SUTemporaryDirectoryError userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Can't make a temporary directory for the update download at %@.", tempDir] }]; [_delegate downloaderDidFailWithError:error]; } else { NSString *downloadFileName = _desiredFilename; NSString *downloadFileNameDirectory = [tempDir stringByAppendingPathComponent:downloadFileName]; NSError *createError = nil; if (![[NSFileManager defaultManager] createDirectoryAtPath:downloadFileNameDirectory withIntermediateDirectories:NO attributes:nil error:&createError]) { NSError *error = [NSError errorWithDomain:SUSparkleErrorDomain code:SUTemporaryDirectoryError userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Can't make a download file name %@ directory inside temporary directory for the update download at %@.", downloadFileName, downloadFileNameDirectory] }]; [_delegate downloaderDidFailWithError:error]; } else { NSString *name = _sessionTask.response.suggestedFilename; if (!name) { name = location.lastPathComponent; // This likely contains nothing useful to identify the file (e.g. CFNetworkDownload_87LVIz.tmp) } NSString *toPath = [downloadFileNameDirectory stringByAppendingPathComponent:name]; NSString *fromPath = location.path; // suppress moveItemAtPath: non-null warning NSError *error = nil; if ([[NSFileManager defaultManager] moveItemAtPath:fromPath toPath:toPath error:&error]) { // Create a bookmark for the download // Don't pass any options (we don't want a persistent security scoped bookmark) NSURL *downloadURL = [NSURL fileURLWithPath:toPath isDirectory:NO]; NSError *bookmarkError = nil; NSData *bookmarkData = [downloadURL bookmarkDataWithOptions:(NSURLBookmarkCreationOptions)0 includingResourceValuesForKeys:@[] relativeToURL:nil error:&bookmarkError]; if (bookmarkData == nil) { [_delegate downloaderDidFailWithError:bookmarkError]; } else { // The download token may be provided later to the downloader for removing a download // and its temporary directory NSString *downloadToken = tempDir.lastPathComponent; [_delegate downloaderDidSetDownloadBookmarkData:bookmarkData downloadToken:downloadToken]; _sessionTask = nil; [_delegate downloaderDidFinishWithTemporaryDownloadData:nil]; } } else { [_delegate downloaderDidFailWithError:error]; } } } } - (void)URLSession:(NSURLSession *)__unused session downloadTask:(NSURLSessionDownloadTask *)__unused downloadTask didWriteData:(int64_t)bytesWritten totalBytesWritten:(int64_t)__unused totalBytesWritten totalBytesExpectedToWrite:(int64_t)totalBytesExpectedToWrite { if (_mode != SPUDownloadModePersistent) { return; } if (totalBytesExpectedToWrite > 0 && !_receivedExpectedBytes) { _receivedExpectedBytes = YES; [_delegate downloaderDidReceiveExpectedContentLength:totalBytesExpectedToWrite]; } if (bytesWritten >= 0) { [_delegate downloaderDidReceiveDataOfLength:(uint64_t)bytesWritten]; } } - (void)URLSession:(NSURLSession *)__unused session task:(NSURLSessionTask *)__unused task didCompleteWithError:(NSError *)error { _sessionTask = nil; if (error != nil) { [_delegate downloaderDidFailWithError:error]; } } @end ================================================ FILE: Downloader/SPUDownloaderDelegate.h ================================================ // // SPUDownloaderDelegate.h // Sparkle // // Created by Mayur Pawashe on 4/1/16. // Copyright © 2016 Sparkle Project. All rights reserved. // #import NS_ASSUME_NONNULL_BEGIN @class SPUDownloadData; @protocol SPUDownloaderDelegate // This is only invoked for persistent downloads - (void)downloaderDidSetDownloadBookmarkData:(NSData *)downloadBookmarkData downloadToken:(NSString *)downloadToken; // Under rare cases, this may be called more than once, in which case the current progress should be reset back to 0 // This is only invoked for persistent downloads - (void)downloaderDidReceiveExpectedContentLength:(int64_t)expectedContentLength; // This is only invoked for persistent downloads - (void)downloaderDidReceiveDataOfLength:(uint64_t)length; // downloadData is nil if this is a persisent download, otherwise it's non-nil if it's a temporary download - (void)downloaderDidFinishWithTemporaryDownloadData:(SPUDownloadData * _Nullable)downloadData; - (void)downloaderDidFailWithError:(NSError *)error; @end NS_ASSUME_NONNULL_END ================================================ FILE: Downloader/SPUDownloaderProtocol.h ================================================ // // SPUDownloaderProtocol.h // PersistentDownloader // // Created by Mayur Pawashe on 4/1/16. // Copyright © 2016 Sparkle Project. All rights reserved. // #import NS_ASSUME_NONNULL_BEGIN // The protocol that this service will vend as its API. This header file will also need to be visible to the process hosting the service. @protocol SPUDownloaderProtocol - (void)startPersistentDownloadWithRequest:(NSURLRequest *)request bundleIdentifier:(NSString *)bundleIdentifier desiredFilename:(NSString *)desiredFilename; - (void)startTemporaryDownloadWithRequest:(NSURLRequest *)request; - (void)removeDownloadDirectoryWithDownloadToken:(NSString *)downloadToken bundleIdentifier:(NSString *)bundleIdentifier; - (void)cleanup:(void (^)(void))completionHandler; @end NS_ASSUME_NONNULL_END ================================================ FILE: Downloader/main.m ================================================ // // main.m // PersistentDownloader // // Created by Mayur Pawashe on 4/1/16. // Copyright © 2016 Sparkle Project. All rights reserved. // #import #import "SPUDownloader.h" #import "SPUDownloaderDelegate.h" #import "SULog.h" #import "SUCodeSigningVerifier.h" @interface ServiceDelegate : NSObject @end @implementation ServiceDelegate - (BOOL)listener:(NSXPCListener *)__unused listener shouldAcceptNewConnection:(NSXPCConnection *)newConnection { // This method is where the NSXPCListener configures, accepts, and resumes a new incoming NSXPCConnection. // Validate connection of clients to avoid any client from using the service. // Ideally we would also prevent clients connecting that have an outgoing network connection enabled, // but this is not easy to enforce. // This is a policy, not a security critical enforcement. { NSError *validationError = nil; SUValidateConnectionStatus validationStatus = [SUCodeSigningVerifier validateConnection:newConnection error:&validationError]; switch (validationStatus) { case SUValidateConnectionStatusSetCodeSigningRequirementSuccess: break; case SUValidateConnectionStatusSetNoRequirementSuccess: break; case SUValidateConnectionStatusAPIFailure: case SUValidateConnectionStatusCodeSigningRequirementFailure: case SUValidateConectionNoSupportedValidationMethodFailure: SULog(SULogLevelError, @"Error: Downloader XPC Service is rejecting new connection due to failing validation of XPC connection with status %lu and error: %@", validationStatus, validationError.localizedDescription); [newConnection invalidate]; return NO; } } // Configure the connection. // First, set the interface that the exported object implements. newConnection.exportedInterface = [NSXPCInterface interfaceWithProtocol:@protocol(SPUDownloaderProtocol)]; // Then set remote object interface newConnection.remoteObjectInterface = [NSXPCInterface interfaceWithProtocol:@protocol(SPUDownloaderDelegate)]; // Next, set the object that the connection exports. All messages sent on the connection to this service will be sent to the exported object to handle. The connection retains the exported object. SPUDownloader *exportedObject = [[SPUDownloader alloc] initWithDelegate:newConnection.remoteObjectProxy]; newConnection.exportedObject = exportedObject; // Resuming the connection allows the system to deliver more incoming messages. [newConnection resume]; // Returning YES from this method tells the system that you have accepted this connection. If you want to reject the connection for some reason, call -invalidate on the connection and return NO. return YES; } @end int main(int __unused argc, const char * __unused argv[]) { // Create the delegate for the service. ServiceDelegate *delegate = [ServiceDelegate new]; // Set up the one NSXPCListener for this service. It will handle all incoming connections. NSXPCListener *listener = [NSXPCListener serviceListener]; listener.delegate = delegate; // Resuming the serviceListener starts this service. This method does not return. [listener resume]; return 0; } ================================================ FILE: INSTALL ================================================ For integration and usage, please visit Sparkle's Documentation: https://sparkle-project.org/documentation/ For integrating XPC Services in a Sandboxed Application, please visit: https://sparkle-project.org/documentation/sandboxing/ ================================================ FILE: InstallerConnection/Info.plist ================================================ CFBundleDevelopmentRegion en CFBundleExecutable $(EXECUTABLE_NAME) CFBundleIdentifier $(PRODUCT_BUNDLE_IDENTIFIER) CFBundleInfoDictionaryVersion 6.0 CFBundleName $(PRODUCT_NAME) CFBundlePackageType XPC! CFBundleShortVersionString $(MARKETING_VERSION) CFBundleSignature ???? CFBundleVersion ${CURRENT_PROJECT_VERSION} NSHumanReadableCopyright Copyright © 2016 Sparkle Project. All rights reserved. XPCService ServiceType Application ================================================ FILE: InstallerConnection/SUInstallerCommunicationProtocol.h ================================================ // // SUInstallerCommunicationProtocol.h // Sparkle // // Created by Mayur Pawashe on 7/9/16. // Copyright © 2016 Sparkle Project. All rights reserved. // #import NS_ASSUME_NONNULL_BEGIN @protocol SUInstallerCommunicationProtocol - (void)handleMessageWithIdentifier:(int32_t)identifier data:(NSData *)data; @end NS_ASSUME_NONNULL_END ================================================ FILE: InstallerConnection/SUInstallerConnection.h ================================================ // // SUInstallerConnection.h // InstallerConnection // // Created by Mayur Pawashe on 7/9/16. // Copyright © 2016 Sparkle Project. All rights reserved. // #import #import "SUInstallerConnectionProtocol.h" NS_ASSUME_NONNULL_BEGIN // This object implements the protocol which we have defined. It provides the actual behavior for the service. It is 'exported' by the service to make it available to the process hosting the service over an NSXPCConnection. SPU_OBJC_DIRECT_MEMBERS @interface SUInstallerConnection : NSObject // Due to XPC reasons, this delegate is strongly referenced, until it's invalidated - (instancetype)initWithDelegate:(id)delegate remote:(BOOL)remote; - (instancetype)init NS_UNAVAILABLE; @end NS_ASSUME_NONNULL_END ================================================ FILE: InstallerConnection/SUInstallerConnection.m ================================================ // // SUInstallerConnection.m // InstallerConnection // // Created by Mayur Pawashe on 7/9/16. // Copyright © 2016 Sparkle Project. All rights reserved. // #import "SUInstallerConnection.h" #include "AppKitPrevention.h" static NSString *SUInstallerConnectionKeepAliveReason = @"Installer Connection Keep Alive"; @interface SUInstallerConnection () @end @implementation SUInstallerConnection { NSXPCConnection *_connection; // Intentionally not weak for XPC reasons id _delegate; void (^_invalidationBlock)(void); BOOL _disabledAutomaticTermination; BOOL _remote; } - (instancetype)initWithDelegate:(id)delegate remote:(BOOL)remote { self = [super init]; if (self != nil) { _delegate = delegate; _remote = remote; if (remote) { // If we are a XPC service, protect it from being terminated until the invalidation handler is set _disabledAutomaticTermination = YES; [[NSProcessInfo processInfo] disableAutomaticTermination:SUInstallerConnectionKeepAliveReason]; } } return self; } - (void)enableAutomaticTermination SPU_OBJC_DIRECT { if (_disabledAutomaticTermination) { [[NSProcessInfo processInfo] enableAutomaticTermination:SUInstallerConnectionKeepAliveReason]; _disabledAutomaticTermination = NO; } } - (void)_setInvalidationHandler:(void (^)(void))invalidationHandler SPU_OBJC_DIRECT { _invalidationBlock = [invalidationHandler copy]; // No longer needed because of invalidation callback [self enableAutomaticTermination]; } - (void)setInvalidationHandler:(void (^)(void))invalidationHandler { if (_remote) { dispatch_async(dispatch_get_main_queue(), ^{ [self _setInvalidationHandler:invalidationHandler]; }); } else { [self _setInvalidationHandler:invalidationHandler]; } } - (void)_setServiceName:(NSString *)serviceName systemDomain:(BOOL)systemDomain SPU_OBJC_DIRECT { NSXPCConnectionOptions options = systemDomain ? NSXPCConnectionPrivileged : (NSXPCConnectionOptions)0; NSXPCConnection *connection = [[NSXPCConnection alloc] initWithMachServiceName:serviceName options:options]; connection.exportedInterface = [NSXPCInterface interfaceWithProtocol:@protocol(SUInstallerCommunicationProtocol)]; connection.exportedObject = _delegate; connection.remoteObjectInterface = [NSXPCInterface interfaceWithProtocol:@protocol(SUInstallerCommunicationProtocol)]; _connection = connection; __weak __typeof__(self) weakSelf = self; _connection.interruptionHandler = ^{ dispatch_async(dispatch_get_main_queue(), ^{ __typeof__(self) strongSelf = weakSelf; if (strongSelf != nil) { [strongSelf->_connection invalidate]; } }); }; _connection.invalidationHandler = ^{ dispatch_async(dispatch_get_main_queue(), ^{ __typeof__(self) strongSelf = weakSelf; if (strongSelf != nil) { strongSelf->_connection = nil; [strongSelf _invalidate]; } }); }; [_connection resume]; } - (void)setServiceName:(NSString *)serviceName systemDomain:(BOOL)systemDomain { if (_remote) { dispatch_async(dispatch_get_main_queue(), ^{ [self _setServiceName:serviceName systemDomain:systemDomain]; }); } else { [self _setServiceName:serviceName systemDomain:systemDomain]; } } - (void)handleMessageWithIdentifier:(int32_t)identifier data:(NSData *)data { if (_remote) { dispatch_async(dispatch_get_main_queue(), ^{ [(id)self->_connection.remoteObjectProxy handleMessageWithIdentifier:identifier data:data]; }); } else { [(id)_connection.remoteObjectProxy handleMessageWithIdentifier:identifier data:data]; } } - (void)_invalidate SPU_OBJC_DIRECT { if (_invalidationBlock != nil) { _invalidationBlock(); _invalidationBlock = nil; } // Break the retain cycle _delegate = nil; [self enableAutomaticTermination]; } // This method can be called from us or a remote - (void)invalidate { dispatch_async(dispatch_get_main_queue(), ^{ [self->_connection invalidate]; self->_connection = nil; [self _invalidate]; }); } @end ================================================ FILE: InstallerConnection/SUInstallerConnectionProtocol.h ================================================ // // SUInstallerConnectionProtocol.h // InstallerConnection // // Created by Mayur Pawashe on 7/9/16. // Copyright © 2016 Sparkle Project. All rights reserved. // #import #import "SUInstallerCommunicationProtocol.h" NS_ASSUME_NONNULL_BEGIN @protocol SUInstallerConnectionProtocol // This method is declared in SUInstallerCommunicationProtocol too // the XPC decoder on macOS 10.8 doesn't follow protocols that adopt other protocols, which is why this protocol doesn't adopt SUInstallerCommunicationProtocol - (void)handleMessageWithIdentifier:(int32_t)identifier data:(NSData *)data; - (void)setInvalidationHandler:(void (^)(void))invalidationHandler; - (void)setServiceName:(NSString *)serviceName systemDomain:(BOOL)systemDomain; - (void)invalidate; @end NS_ASSUME_NONNULL_END ================================================ FILE: InstallerConnection/SUXPCInstallerConnection.h ================================================ // // SUXPCInstallerConnection.h // Sparkle // // Created by Mayur Pawashe on 7/10/16. // Copyright © 2016 Sparkle Project. All rights reserved. // #if INSTALLER_CONNECTION_XPC_SERVICE_EMBEDDED #import #import "SUInstallerConnectionProtocol.h" NS_ASSUME_NONNULL_BEGIN SPU_OBJC_DIRECT_MEMBERS @interface SUXPCInstallerConnection : NSObject // Due to XPC reasons, this delegate is strongly referenced, until it's invalidated - (instancetype)initWithDelegate:(id)delegate; @end NS_ASSUME_NONNULL_END #endif ================================================ FILE: InstallerConnection/SUXPCInstallerConnection.m ================================================ // // SUXPCInstallerConnection.m // Sparkle // // Created by Mayur Pawashe on 7/10/16. // Copyright © 2016 Sparkle Project. All rights reserved. // #if INSTALLER_CONNECTION_XPC_SERVICE_EMBEDDED #import "SUXPCInstallerConnection.h" #include "AppKitPrevention.h" @implementation SUXPCInstallerConnection { NSXPCConnection *_connection; // Intentionally not weak for XPC reasons id _delegate; void (^_invalidationBlock)(void); } - (instancetype)initWithDelegate:(id)delegate { self = [super init]; if (self != nil) { _connection = [[NSXPCConnection alloc] initWithServiceName:@INSTALLER_CONNECTION_BUNDLE_ID]; _connection.remoteObjectInterface = [NSXPCInterface interfaceWithProtocol:@protocol(SUInstallerConnectionProtocol)]; __weak __typeof__(self) weakSelf = self; _connection.invalidationHandler = ^{ [weakSelf invokeInvalidation]; }; _connection.interruptionHandler = ^{ __typeof__(self) strongSelf = weakSelf; if (strongSelf != nil) { [strongSelf invokeInvalidation]; [strongSelf->_connection invalidate]; } }; _delegate = delegate; _connection.exportedInterface = [NSXPCInterface interfaceWithProtocol:@protocol(SUInstallerCommunicationProtocol)]; _connection.exportedObject = _delegate; [_connection resume]; } return self; } - (void)setInvalidationHandler:(void (^)(void))invalidationHandler { _invalidationBlock = [invalidationHandler copy]; __weak __typeof__(self) weakSelf = self; [(id)_connection.remoteObjectProxy setInvalidationHandler:^{ [weakSelf invokeInvalidation]; }]; } - (void)setServiceName:(NSString *)serviceName systemDomain:(BOOL)systemDomain { [(id)_connection.remoteObjectProxy setServiceName:serviceName systemDomain:systemDomain]; } - (void)handleMessageWithIdentifier:(int32_t)identifier data:(NSData *)data { [(id)_connection.remoteObjectProxy handleMessageWithIdentifier:identifier data:data]; } - (void)invalidate { [(id)_connection.remoteObjectProxy invalidate]; [_connection invalidate]; _connection = nil; } - (void)invokeInvalidation SPU_OBJC_DIRECT { if (_invalidationBlock != nil) { _invalidationBlock(); _invalidationBlock = nil; } // Break our retain cycle _delegate = nil; } @end #endif ================================================ FILE: InstallerConnection/main.m ================================================ // // main.m // InstallerConnection // // Created by Mayur Pawashe on 7/9/16. // Copyright © 2016 Sparkle Project. All rights reserved. // #import #import "SUInstallerConnection.h" #import "SUInstallerCommunicationProtocol.h" #include "AppKitPrevention.h" @interface ServiceDelegate : NSObject @end @implementation ServiceDelegate - (BOOL)listener:(NSXPCListener *)__unused listener shouldAcceptNewConnection:(NSXPCConnection *)newConnection { // This method is where the NSXPCListener configures, accepts, and resumes a new incoming NSXPCConnection. // Configure the connection. // First, set the interface that the exported object implements. newConnection.exportedInterface = [NSXPCInterface interfaceWithProtocol:@protocol(SUInstallerConnectionProtocol)]; newConnection.remoteObjectInterface = [NSXPCInterface interfaceWithProtocol:@protocol(SUInstallerCommunicationProtocol)]; SUInstallerConnection *exportedObject = [[SUInstallerConnection alloc] initWithDelegate:newConnection.remoteObjectProxy remote:YES]; newConnection.exportedObject = exportedObject; // Resuming the connection allows the system to deliver more incoming messages. [newConnection resume]; return YES; } @end int main(int __unused argc, const char * __unused argv[]) { // Create the delegate for the service. ServiceDelegate *delegate = [ServiceDelegate new]; // Set up the one NSXPCListener for this service. It will handle all incoming connections. NSXPCListener *listener = [NSXPCListener serviceListener]; listener.delegate = delegate; // Resuming the serviceListener starts this service. This method does not return. [listener resume]; return 0; } ================================================ FILE: InstallerLauncher/Info.plist ================================================ CFBundleDevelopmentRegion en CFBundleExecutable $(EXECUTABLE_NAME) CFBundleIdentifier $(PRODUCT_BUNDLE_IDENTIFIER) CFBundleInfoDictionaryVersion 6.0 CFBundleName $(PRODUCT_NAME) CFBundlePackageType XPC! CFBundleShortVersionString $(MARKETING_VERSION) CFBundleSignature ???? CFBundleVersion ${CURRENT_PROJECT_VERSION} NSHumanReadableCopyright Copyright © 2016 Sparkle Project. All rights reserved. XPCService ServiceType Application JoinExistingSession ================================================ FILE: InstallerLauncher/SUInstallerLauncher+Private.h ================================================ // // SUInstallerLauncher+Private.h // SUInstallerLauncher+Private // // Created by Mayur Pawashe on 8/21/21. // Copyright © 2021 Sparkle Project. All rights reserved. // #ifndef SUInstallerLauncher_Private_h #define SUInstallerLauncher_Private_h #if defined(BUILDING_SPARKLE_SOURCES_EXTERNALLY) // Ignore incorrect warning #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wquoted-include-in-framework-header" #import "SUExport.h" #import "SPUInstallationType.h" #pragma clang diagnostic pop #else #import // Chances are clients will need this too #import #endif @class NSString; /** Private API for determining if the system needs authorization access to update a bundle path This API is not supported when used directly from a Sandboxed applications and will always return @c YES in that case. @param bundlePath The bundle path to test if authorization is needed when performing an update that replaces this bundle. @return @c YES if Sparkle thinks authorization is needed to update the @c bundlePath, otherwise @c NO. */ SU_EXPORT BOOL SPUSystemNeedsAuthorizationAccessForBundlePath(NSString *bundlePath); #endif /* SUInstallerLauncher_Private_h */ ================================================ FILE: InstallerLauncher/SUInstallerLauncher.h ================================================ // // SUInstallerLauncher.h // InstallerLauncher // // Created by Mayur Pawashe on 4/1/16. // Copyright © 2016 Sparkle Project. All rights reserved. // #import #import "SUInstallerLauncherProtocol.h" // Non-sandboxed XPC service used for launching our installer // This is necessary for sandboxed applications @interface SUInstallerLauncher : NSObject @end ================================================ FILE: InstallerLauncher/SUInstallerLauncher.m ================================================ // // SUInstallerLauncher.m // InstallerLauncher // // Created by Mayur Pawashe on 4/1/16. // Copyright © 2016 Sparkle Project. All rights reserved. // #import "SUInstallerLauncher.h" #import "SUInstallerLauncher+Private.h" #import "SUFileManager.h" #import "SULog.h" #import "SPUMessageTypes.h" #import "SPULocalCacheDirectory.h" #import "SPUInstallationType.h" #import "SUHost.h" #import #import // We only fetch bundle icon for Installer Launcher XPC Service #if defined(BUILDING_SPARKLE_SOURCES_EXTERNALLY) && BUILDING_SPARKLE_SOURCES_EXTERNALLY #define FETCH_BUNDLE_ICON_FOR_AUTH 1 #else #define FETCH_BUNDLE_ICON_FOR_AUTH 0 #endif #if FETCH_BUNDLE_ICON_FOR_AUTH #import #else #include "AppKitPrevention.h" #endif @implementation SUInstallerLauncher - (BOOL)submitProgressToolAtPath:(NSString *)progressToolPath withHostBundle:(NSBundle *)hostBundle inSystemDomainForInstaller:(BOOL)inSystemDomainForInstaller SPU_OBJC_DIRECT { SUFileManager *fileManager = [[SUFileManager alloc] init]; NSURL *progressToolURL = [NSURL fileURLWithPath:progressToolPath]; NSError *quarantineError = nil; if (![fileManager releaseItemFromQuarantineAtRootURL:progressToolURL error:&quarantineError]) { // This may or may not be a fatal error depending on if the process is sandboxed or not SULog(SULogLevelError, @"Failed to release quarantine on installer at %@ with error %@", progressToolPath, quarantineError); } NSString *executablePath = [[NSBundle bundleWithURL:progressToolURL] executablePath]; assert(executablePath != nil); NSString *hostBundlePath = hostBundle.bundlePath; assert(hostBundlePath != nil); NSString *hostBundleIdentifier = hostBundle.bundleIdentifier; assert(hostBundleIdentifier != nil); NSArray *arguments = @[executablePath, hostBundlePath, @(inSystemDomainForInstaller).stringValue]; // The progress tool can only be ran as the logged in user, not as root CFStringRef domain = kSMDomainUserLaunchd; NSString *label = [NSString stringWithFormat:@"%@-sparkle-progress", hostBundleIdentifier]; AuthorizationRef auth = NULL; Boolean submittedJob = false; OSStatus createStatus = AuthorizationCreate(NULL, kAuthorizationEmptyEnvironment, kAuthorizationFlagDefaults, &auth); if (createStatus == errAuthorizationSuccess) { // Try to remove the job from launchd if it is already running // We could invoke SMJobCopyDictionary() first to see if the job exists, but I'd rather avoid // using it because the headers indicate it may be removed one day without any replacement CFErrorRef removeError = NULL; #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdeprecated-declarations" if (!SMJobRemove(domain, (__bridge CFStringRef)(label), auth, true, &removeError)) { #pragma clang diagnostic pop if (removeError != NULL) { // It's normal for a job to not be found, so this is not an interesting error if (CFErrorGetCode(removeError) != kSMErrorJobNotFound) { SULog(SULogLevelError, @"Remove error: %@", removeError); } CFRelease(removeError); } } // If we are running as the root user, there is no need to explicitly set the UserName / GroupName keys // because we are submitting under the user domain, which should automatically use the the console user. NSMutableDictionary *jobDictionary = [[NSMutableDictionary alloc] init]; jobDictionary[@"Label"] = label; jobDictionary[@"ProgramArguments"] = arguments; jobDictionary[@"EnableTransactions"] = @NO; jobDictionary[@"RunAtLoad"] = @YES; jobDictionary[@"Nice"] = @0; jobDictionary[@"ProcessType"] = @"Interactive"; jobDictionary[@"LaunchOnlyOnce"] = @YES; jobDictionary[@"MachServices"] = @{SPUStatusInfoServiceNameForBundleIdentifier(hostBundleIdentifier) : @YES}; CFErrorRef submitError = NULL; #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdeprecated-declarations" // SMJobSubmit is deprecated but is the only way to submit a non-permanent // helper and allows us to submit to user domain without requiring authorization submittedJob = SMJobSubmit(domain, (__bridge CFDictionaryRef)(jobDictionary), auth, &submitError); #pragma clang diagnostic pop if (!submittedJob) { if (submitError != NULL) { SULog(SULogLevelError, @"Submit progress error: %@", submitError); CFRelease(submitError); } } AuthorizationFree(auth, kAuthorizationFlagDefaults); } return (submittedJob == true); } - (SUInstallerLauncherStatus)submitInstallerAtPath:(NSString *)installerPath withHostBundle:(NSBundle *)hostBundle iconBundlePath:(NSString *)iconBundlePath updaterIdentifier:(NSString *)updaterIdentifier userName:(NSString *)userName homeDirectory:(NSString *)homeDirectory mainBundleName:(NSString *)mainBundleName inSystemDomain:(BOOL)systemDomain rootUser:(BOOL)rootUser SPU_OBJC_DIRECT { // No need to release the quarantine for this utility // In fact, we shouldn't because the tool may be located at a path we should not be writing too. NSString *hostBundleIdentifier = hostBundle.bundleIdentifier; assert(hostBundleIdentifier != nil); // The first argument has to be the path to the program, and the second is a host identifier so that the installer knows what mach services to host // The third and forth arguments are for home directory and user name which only pkg installer scripts may need // We intentionally do not pass any more arguments. Anything else should be done via IPC. // This is compatible to SMJobBless() which does not allow arguments // Even though we aren't using that function for now, it'd be wise to not decrease compatibility to it NSArray *arguments = @[installerPath, hostBundleIdentifier, homeDirectory, userName]; AuthorizationRef auth = NULL; OSStatus createStatus = AuthorizationCreate(NULL, kAuthorizationEmptyEnvironment, kAuthorizationFlagDefaults, &auth); if (createStatus != errAuthorizationSuccess) { auth = NULL; SULog(SULogLevelError, @"Failed to create authorization reference: %d", createStatus); } BOOL canceledAuthorization = NO; BOOL failedToUseSystemDomain = NO; if (auth != NULL && systemDomain && !rootUser) { // See Apple's 'EvenBetterAuthorizationSample' sample code and // https://developer.apple.com/library/mac/technotes/tn2095/_index.html#//apple_ref/doc/uid/DTS10003110-CH1-SECTION7 // We can set a custom right name for authenticating as an administrator // Using this right rather than using something like kSMRightModifySystemDaemons allows us to present a better worded prompt // Note the right name is cached, so if we want to change the authorization // prompt, we may need to change the right name. I have found no good way around this :| SUHost *host = [[SUHost alloc] initWithBundle:hostBundle]; NSString *hostName = host.name; // Figure out the authorization prompt and right name // We create the authorization prompt here so that a bad client cannot pass in a completely arbitrary prompt message // We also forgo localization for this prompt at the moment because with the right name being cached, updating localizations will be undesirable. // Furthermore, we can avoid adding localization files to the Installer XPC Service (if that one is being used). NSString *sparkleAuthTag = @"sparkle2-auth"; // this needs to change if auth wording changes NSString *rightNameString; NSString *authorizationPrompt; if ([hostBundleIdentifier isEqualToString:updaterIdentifier]) { // Application bundle is likely updating itself rightNameString = [NSString stringWithFormat:@"%@.%@", hostBundleIdentifier, sparkleAuthTag]; authorizationPrompt = [NSString stringWithFormat:@"%1$@ wants permission to update.", hostName]; } else { // Updater is likely updating a bundle that is not itself rightNameString = [NSString stringWithFormat:@"%@.%@.%@", updaterIdentifier, hostBundleIdentifier, sparkleAuthTag]; authorizationPrompt = [NSString stringWithFormat:@"%1$@ wants permission to update %2$@.", mainBundleName, hostName]; } const char *rightName = rightNameString.UTF8String; assert(rightName != NULL); OSStatus getRightResult = AuthorizationRightGet(rightName, NULL); if (getRightResult == errAuthorizationDenied) { if (AuthorizationRightSet(auth, rightName, (__bridge CFTypeRef _Nonnull)(@(kAuthorizationRuleAuthenticateAsAdmin)), (__bridge CFStringRef _Nullable)(authorizationPrompt), NULL, NULL) != errAuthorizationSuccess) { SULog(SULogLevelError, @"Failed to make auth right set"); } } AuthorizationItem right = { .name = rightName, .valueLength = 0, .value = NULL, .flags = 0 }; AuthorizationRights rights = { .count = 1, .items = &right }; AuthorizationFlags flags = (AuthorizationFlags)(kAuthorizationFlagExtendRights | kAuthorizationFlagInteractionAllowed); AuthorizationEnvironment authorizationEnvironment = {.count = 0, .items = NULL}; #if FETCH_BUNDLE_ICON_FOR_AUTH NSString *tempIconDestinationPath = nil; AuthorizationItem iconAuthorizationItem = {.name = kAuthorizationEnvironmentIcon, .valueLength = 0, .value = NULL, .flags = 0}; char iconPathBuffer[] = "/tmp/XXXXXX.png"; // If an icon bundle path is specified, write out a representation of the icon's data to a temporary file. // Then use that image data for the authorization prompt if (iconBundlePath != nil) { NSImage *icon = [[NSWorkspace sharedWorkspace] iconForFile:iconBundlePath]; // Creating a bitmap representation at a specific size is much cheaper than asking for icon's TIFFRepresentation // On older OS's we must create a 32x32 image otherwise it won't be scaled correctly in the dialog // On newer OS's we can use a slightly higher resolution image NSInteger imageDimensions; if (@available(macOS 10.15, *)) { imageDimensions = 64; } else { imageDimensions = 32; } NSBitmapImageRep *iconBitmapRep = [[NSBitmapImageRep alloc] initWithBitmapDataPlanes:NULL pixelsWide:imageDimensions pixelsHigh:imageDimensions bitsPerSample:8 samplesPerPixel:4 hasAlpha:YES isPlanar:NO colorSpaceName:NSCalibratedRGBColorSpace bytesPerRow:0 bitsPerPixel:0]; [NSGraphicsContext saveGraphicsState]; NSGraphicsContext.currentContext = [NSGraphicsContext graphicsContextWithBitmapImageRep:iconBitmapRep]; [icon drawInRect:NSMakeRect(0.0, 0.0, (CGFloat)imageDimensions, (CGFloat)imageDimensions)]; [NSGraphicsContext restoreGraphicsState]; NSData *pngData = [iconBitmapRep representationUsingType:NSBitmapImageFileTypePNG properties:@{}]; if (pngData != nil) { // Use /tmp rather than NSTemporaryDirectory() or SUFileManager's temp directory function because we want: // a) no spaces in the path (SU/NSFileManager fails here) // b) short file path that does not exceed a small threshold (NSTemporaryDirectory() fails here) only apply to older systems (eg: macOS 10.8) // The file also needs to be placed in a system readable place such as /tmp // See https://github.com/sparkle-project/Sparkle/issues/347#issuecomment-149523848 for more info int tempIconFile = mkstemps(iconPathBuffer, strlen(".png")); if (tempIconFile == -1) { SULog(SULogLevelError, @"Failed to open temp icon from path buffer with error: %d", errno); } else { close(tempIconFile); tempIconDestinationPath = [[NSFileManager defaultManager] stringWithFileSystemRepresentation:iconPathBuffer length:strlen(iconPathBuffer)]; if (tempIconDestinationPath != nil) { if (![pngData writeToFile:tempIconDestinationPath atomically:NO]) { SULog(SULogLevelError, @"Failed to write icon image data to %@", tempIconDestinationPath); } else { iconAuthorizationItem.valueLength = strlen(iconPathBuffer); iconAuthorizationItem.value = iconPathBuffer; authorizationEnvironment.count = 1; authorizationEnvironment.items = &iconAuthorizationItem; } } } } } #endif // This should prompt up the authorization dialog if necessary OSStatus copyStatus = AuthorizationCopyRights(auth, &rights, &authorizationEnvironment, flags, NULL); if (copyStatus != errAuthorizationSuccess) { failedToUseSystemDomain = YES; if (copyStatus == errAuthorizationCanceled) { canceledAuthorization = YES; } else { SULog(SULogLevelError, @"Failed copying system domain rights: %d", copyStatus); } } #if FETCH_BUNDLE_ICON_FOR_AUTH if (tempIconDestinationPath != nil) { [[NSFileManager defaultManager] removeItemAtPath:tempIconDestinationPath error:NULL]; } #endif } Boolean submittedJob = false; if (!canceledAuthorization && !failedToUseSystemDomain && auth != NULL) { CFStringRef domain = (systemDomain ? kSMDomainSystemLaunchd : kSMDomainUserLaunchd); NSString *label = [NSString stringWithFormat:@"%@-sparkle-updater", hostBundleIdentifier]; // Try to remove the job from launchd if it is already running // We could invoke SMJobCopyDictionary() first to see if the job exists, but I'd rather avoid // using it because the headers indicate it may be removed one day without any replacement CFErrorRef removeError = NULL; #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdeprecated-declarations" if (!SMJobRemove(domain, (__bridge CFStringRef)(label), auth, true, &removeError)) { #pragma clang diagnostic pop if (removeError != NULL) { // It's normal for a job to not be found, so this is not an interesting error if (CFErrorGetCode(removeError) != kSMErrorJobNotFound) { SULog(SULogLevelError, @"Remove job error: %@", removeError); } CFRelease(removeError); } } NSDictionary *jobDictionary = @{@"Label" : label, @"ProgramArguments" : arguments, @"EnableTransactions" : @NO, @"RunAtLoad" : @YES, @"Nice" : @0, @"ProcessType": @"Interactive", @"LaunchOnlyOnce": @YES, @"MachServices" : @{SPUInstallerServiceNameForBundleIdentifier(hostBundleIdentifier) : @YES, SPUProgressAgentServiceNameForBundleIdentifier(hostBundleIdentifier) : @YES}}; CFErrorRef submitError = NULL; #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdeprecated-declarations" // SMJobSubmit is deprecated but is the only way to submit a non-permanent // helper and allows us to submit to user domain without requiring authorization submittedJob = SMJobSubmit(domain, (__bridge CFDictionaryRef)(jobDictionary), auth, &submitError); #pragma clang diagnostic pop if (!submittedJob) { if (submitError != NULL) { SULog(SULogLevelError, @"Submit error: %@", submitError); CFRelease(submitError); } } } if (auth != NULL) { AuthorizationFree(auth, kAuthorizationFlagDefaults); } SUInstallerLauncherStatus status; if (submittedJob == true) { status = SUInstallerLauncherSuccess; } else if (canceledAuthorization) { status = SUInstallerLauncherCanceled; } else { status = SUInstallerLauncherFailure; } return status; } - (NSString *)pathForBundledTool:(NSString *)toolName extension:(NSString *)extension fromBundle:(NSBundle *)bundle SPU_OBJC_DIRECT { // If the path extension is empty, we don't want to add a "." at the end NSString *nameWithExtension = (extension.length > 0) ? [toolName stringByAppendingPathExtension:extension] : toolName; assert(nameWithExtension != nil); NSURL *auxiliaryToolURL; if ([bundle.bundleURL.pathExtension isEqualToString:@"xpc"]) { // Paranoid check to get full bundle URL NSURL *fullURL = bundle.bundleURL.URLByResolvingSymlinksInPath; auxiliaryToolURL = [fullURL.URLByDeletingLastPathComponent.URLByDeletingLastPathComponent URLByAppendingPathComponent:nameWithExtension]; } else { auxiliaryToolURL = [bundle URLForAuxiliaryExecutable:nameWithExtension]; } if (auxiliaryToolURL == nil) { SULog(SULogLevelError, @"Error: Cannot retrieve path for auxiliary tool: %@", nameWithExtension); return nil; } NSURL *resolvedAuxiliaryToolURL = [auxiliaryToolURL URLByResolvingSymlinksInPath]; if (resolvedAuxiliaryToolURL == nil) { SULog(SULogLevelError, @"Error: Cannot retrieve resolved path for auxiliary tool path: %@", auxiliaryToolURL.path); return nil; } return resolvedAuxiliaryToolURL.path; } BOOL SPUSystemNeedsAuthorizationAccessForBundlePath(NSString *bundlePath) { NSFileManager *fileManager = [NSFileManager defaultManager]; BOOL hasWritability = [fileManager isWritableFileAtPath:bundlePath] && [fileManager isWritableFileAtPath:[bundlePath stringByDeletingLastPathComponent]]; BOOL needsAuthorization; if (!hasWritability) { needsAuthorization = YES; } else { // Just because we have writability access does not mean we can set the correct owner/group // Test if we can set the owner/group on a temporarily created file // If we can, then we can probably perform an update without authorization NSString *tempFilename = @"permission_test" ; SUFileManager *suFileManager = [[SUFileManager alloc] init]; NSURL *tempDirectoryURL = [suFileManager makeTemporaryDirectoryAppropriateForDirectoryURL:[NSURL fileURLWithPath:NSTemporaryDirectory()] error:NULL]; if (tempDirectoryURL == nil) { // I don't imagine this ever happening but in case it does, requesting authorization may be the better option needsAuthorization = YES; } else { NSURL *tempFileURL = [tempDirectoryURL URLByAppendingPathComponent:tempFilename]; if (![[NSData data] writeToURL:tempFileURL atomically:NO]) { // Obvious indicator we may need authorization needsAuthorization = YES; } else { needsAuthorization = ![suFileManager changeOwnerAndGroupOfItemAtRootURL:tempFileURL toMatchURL:[NSURL fileURLWithPath:bundlePath] error:NULL]; } [suFileManager removeItemAtURL:tempDirectoryURL error:NULL]; } } return needsAuthorization; } static BOOL SPUUsesSystemDomainForBundlePath(NSString *path, BOOL rootUser #if SPARKLE_BUILD_PACKAGE_SUPPORT , NSString *installationType #endif ) { if (!rootUser) { #if SPARKLE_BUILD_PACKAGE_SUPPORT if ([installationType isEqualToString:SPUInstallationTypeGuidedPackage]) { return YES; } else #endif { return SPUSystemNeedsAuthorizationAccessForBundlePath(path); } } else { // If we are the root user we use the system domain even if we don't need escalated authorization. // Note interactive package installations are not supported as root. return YES; } } // Note: do not pass information which can we compute ourselves here, such as paths to the installer and progress agent tools - (void)launchInstallerWithHostBundlePath:(NSString *)hostBundlePath mainBundlePath:(NSString *)mainBundlePath installationType:(NSString *)installationType allowingDriverInteraction:(BOOL)allowingDriverInteraction completion:(void (^)(SUInstallerLauncherStatus, BOOL))completionHandler { dispatch_async(dispatch_get_main_queue(), ^{ if (completionHandler == NULL) { SULog(SULogLevelError, @"Error: Failed to retrieve completionHandler (nil)"); return; } if (hostBundlePath == nil) { SULog(SULogLevelError, @"Error: Failed to retrieve hostBundlePath (nil)"); completionHandler(SUInstallerLauncherFailure, NO); return; } if (mainBundlePath == nil) { SULog(SULogLevelError, @"Error: Failed to retrieve mainBundlePath (nil)"); completionHandler(SUInstallerLauncherFailure, NO); return; } NSBundle *mainBundle = [NSBundle bundleWithPath:mainBundlePath]; if (mainBundle == nil) { SULog(SULogLevelError, @"Error: Failed to retrieve mainBundle from path: %@", mainBundlePath); completionHandler(SUInstallerLauncherFailure, NO); return; } SUHost *mainBundleHost = [[SUHost alloc] initWithBundle:mainBundle]; // We use SUHost.name property, so an application // or Sparkle helper application can override its name with SUBundleName key NSString *mainBundleName = mainBundleHost.name; // This updaterIdentifier is just used for authorization prompt NSString *updaterIdentifier; NSString *mainBundleIdentifier = mainBundle.bundleIdentifier; if (mainBundleIdentifier != nil) { updaterIdentifier = mainBundleIdentifier; } else { // Should be unlikely updaterIdentifier = mainBundleHost.name; } if (installationType == nil) { SULog(SULogLevelError, @"Error: Failed to retrieve installationType (nil)"); completionHandler(SUInstallerLauncherFailure, NO); return; } // We could do a sort of preflight Authorization test instead of testing if we are running as root, // but I think this is not necessarily a better approach. We have to chown() the launcher cache directory later on, // and that is not necessarily related to a preflight test. It's more related to being ran under a root / different user from the active GUI session BOOL rootUser = (geteuid() == 0); BOOL inSystemDomain = SPUUsesSystemDomainForBundlePath(hostBundlePath, rootUser #if SPARKLE_BUILD_PACKAGE_SUPPORT , installationType #endif ); NSBundle *hostBundle = [NSBundle bundleWithPath:hostBundlePath]; if (hostBundle == nil) { SULog(SULogLevelError, @"InstallerLauncher failed to create bundle at %@", hostBundlePath); SULog(SULogLevelError, @"Please make sure InstallerLauncher is not sandboxed and do not sign your app by passing --deep. Check: codesign -d --entitlements :- \"%@\"", NSBundle.mainBundle.bundlePath); SULog(SULogLevelError, @"More information regarding sandboxing: https://sparkle-project.org/documentation/sandboxing/"); completionHandler(SUInstallerLauncherFailure, inSystemDomain); return; } // if we need to use the system authorization from non-root and we aren't allowed interaction, then try sometime later when interaction is allowed if (inSystemDomain && !rootUser && !allowingDriverInteraction) { completionHandler(SUInstallerLauncherAuthorizeLater, inSystemDomain); return; } NSString *hostBundleIdentifier = hostBundle.bundleIdentifier; assert(hostBundleIdentifier != nil); // We could be inside the InstallerLauncher XPC bundle or in the Sparkle.framework bundle if no XPC service is used NSBundle *ourBundle = [NSBundle bundleForClass:[self class]]; // Note we do not have to copy this tool out of the bundle it's in because it's a utility with no dependencies. // Furthermore, we can keep the tool at a place that may not necessarily be writable. NSString *installerPath = [self pathForBundledTool:@""SPARKLE_RELAUNCH_TOOL_NAME extension:@"" fromBundle:ourBundle]; if (installerPath == nil) { SULog(SULogLevelError, @"Error: Cannot submit installer because the installer could not be located"); completionHandler(SUInstallerLauncherFailure, inSystemDomain); return; } // We do however have to copy the progress tool app somewhere safe due to its external dependencies NSString *progressToolResourcePath = [self pathForBundledTool:@""SPARKLE_INSTALLER_PROGRESS_TOOL_NAME extension:@"app" fromBundle:ourBundle]; if (progressToolResourcePath == nil) { SULog(SULogLevelError, @"Error: Cannot submit progress tool because the progress tool could not be located"); completionHandler(SUInstallerLauncherFailure, inSystemDomain); return; } NSString *userName; NSString *homeDirectory; uid_t uid = 0; gid_t gid = 0; if (!rootUser) { // Normal path homeDirectory = NSHomeDirectory(); assert(homeDirectory != nil); userName = NSUserName(); assert(userName != nil); } else { // As the root user we need to obtain the user name and home directory reflecting // the user's console session. CFStringRef userNameRef = SCDynamicStoreCopyConsoleUser(NULL, &uid, &gid); if (userNameRef == NULL) { SULog(SULogLevelError, @"Failed to retrieve user name from the console user"); completionHandler(SUInstallerLauncherFailure, inSystemDomain); return; } userName = (NSString *)CFBridgingRelease(userNameRef); homeDirectory = NSHomeDirectoryForUser(userName); if (homeDirectory == nil) { SULog(SULogLevelError, @"Failed to retrieve home directory for user: %@", userName); completionHandler(SUInstallerLauncherFailure, inSystemDomain); return; } } // It may be tempting here to validate/match the signature of the installer and progress tool, however this is not very reliable // We can't compare the signature of this framework/XPC service (depending how it's run) to the host bundle because // they could be different (eg: take a look at sparkle-cli). We also can't easily tell if the signature of the service/framework is the same as the bundle it's inside. // The service/framework also need not even be signed in the first place. We'll just assume for now the original bundle hasn't been tampered with NSString *cachePath = rootUser ? [SPULocalCacheDirectory cachePathForBundleIdentifier:hostBundleIdentifier userName:userName] : [SPULocalCacheDirectory cachePathForBundleIdentifier:hostBundleIdentifier]; NSString *rootLauncherCachePath = [cachePath stringByAppendingPathComponent:@"Launcher"]; [SPULocalCacheDirectory removeOldItemsInDirectory:rootLauncherCachePath]; NSDictionary *fileAttributes = rootUser ? @{NSFileOwnerAccountID: @(uid), NSFileGroupOwnerAccountID: @(gid)} : nil; NSString *launcherCachePath = [SPULocalCacheDirectory createUniqueDirectoryInDirectory:rootLauncherCachePath intermediateDirectoryFileAttributes:fileAttributes]; if (launcherCachePath == nil) { SULog(SULogLevelError, @"Failed to create cache directory for progress tool in %@", rootLauncherCachePath); completionHandler(SUInstallerLauncherFailure, inSystemDomain); return; } SUFileManager *fileManager = [[SUFileManager alloc] init]; if (rootUser) { // Ensure the console user has ownership of the launcher cache directory // Otherwise the updater may not launch and not be able to clean up itself NSError *changeOwnerAndGroupError = nil; if (![fileManager changeOwnerAndGroupOfItemAtURL:[NSURL fileURLWithPath:launcherCachePath] ownerID:uid groupID:gid error:&changeOwnerAndGroupError]) { SULog(SULogLevelError, @"Failed to change owner and group for launcher cache directory: %@", changeOwnerAndGroupError); completionHandler(SUInstallerLauncherFailure, inSystemDomain); return; } } NSString *progressToolPath = [launcherCachePath stringByAppendingPathComponent:@""SPARKLE_INSTALLER_PROGRESS_TOOL_NAME@".app"]; NSError *copyError = nil; // SUFileManager is more reliable for copying files around if (![fileManager copyItemAtURL:[NSURL fileURLWithPath:progressToolResourcePath] toURL:[NSURL fileURLWithPath:progressToolPath] error:©Error]) { SULog(SULogLevelError, @"Failed to copy progress tool to cache: %@", copyError); completionHandler(SUInstallerLauncherFailure, inSystemDomain); return; } SUInstallerLauncherStatus installerStatus = [self submitInstallerAtPath:installerPath withHostBundle:hostBundle iconBundlePath:mainBundle.bundlePath updaterIdentifier:updaterIdentifier userName:userName homeDirectory:homeDirectory mainBundleName:mainBundleName inSystemDomain:inSystemDomain rootUser:rootUser]; BOOL submittedProgressTool = NO; if (installerStatus == SUInstallerLauncherSuccess) { submittedProgressTool = [self submitProgressToolAtPath:progressToolPath withHostBundle:hostBundle inSystemDomainForInstaller:inSystemDomain]; if (!submittedProgressTool) { SULog(SULogLevelError, @"Failed to submit progress tool job"); } } else if (installerStatus == SUInstallerLauncherFailure) { SULog(SULogLevelError, @"Failed to submit installer job"); SULog(SULogLevelError, @"If your application is sandboxed please follow steps at: https://sparkle-project.org/documentation/sandboxing/"); } if (installerStatus == SUInstallerLauncherCanceled) { completionHandler(installerStatus, inSystemDomain); } else { completionHandler(submittedProgressTool ? SUInstallerLauncherSuccess : SUInstallerLauncherFailure, inSystemDomain); } }); } @end ================================================ FILE: InstallerLauncher/SUInstallerLauncherProtocol.h ================================================ // // SUInstallerLauncherProtocol.h // InstallerLauncher // // Created by Mayur Pawashe on 4/1/16. // Copyright © 2016 Sparkle Project. All rights reserved. // #import #import "SUInstallerLauncherStatus.h" @protocol SUInstallerLauncherProtocol - (void)launchInstallerWithHostBundlePath:(NSString *)hostBundlePath mainBundlePath:(NSString *)mainBundlePath installationType:(NSString *)installationType allowingDriverInteraction:(BOOL)allowingDriverInteraction completion:(void (^)(SUInstallerLauncherStatus, BOOL))completionHandler; @end ================================================ FILE: InstallerLauncher/SUInstallerLauncherStatus.h ================================================ // // SUInstallerLauncherStatus.h // Sparkle // // Created by Mayur Pawashe on 7/18/16. // Copyright © 2016 Sparkle Project. All rights reserved. // #import typedef NS_ENUM(NSUInteger, SUInstallerLauncherStatus) { SUInstallerLauncherSuccess = 0, SUInstallerLauncherCanceled = 1, SUInstallerLauncherAuthorizeLater = 3, SUInstallerLauncherFailure = 4 }; ================================================ FILE: InstallerLauncher/main.m ================================================ // // main.m // InstallerLauncher // // Created by Mayur Pawashe on 4/1/16. // Copyright © 2016 Sparkle Project. All rights reserved. // #import #import "SUInstallerLauncher.h" #import "SULog.h" #import "SUCodeSigningVerifier.h" @interface ServiceDelegate : NSObject @end @implementation ServiceDelegate - (BOOL)listener:(NSXPCListener *)__unused listener shouldAcceptNewConnection:(NSXPCConnection *)newConnection { // This method is where the NSXPCListener configures, accepts, and resumes a new incoming NSXPCConnection. // Validate connection of clients to avoid any client from using the service. // This is a policy, not a security critical enforcement. { NSError *validationError = nil; SUValidateConnectionStatus validationStatus = [SUCodeSigningVerifier validateConnection:newConnection error:&validationError]; switch (validationStatus) { case SUValidateConnectionStatusSetCodeSigningRequirementSuccess: break; case SUValidateConnectionStatusSetNoRequirementSuccess: break; case SUValidateConnectionStatusAPIFailure: case SUValidateConnectionStatusCodeSigningRequirementFailure: case SUValidateConectionNoSupportedValidationMethodFailure: SULog(SULogLevelError, @"Error: Installer XPC Service is rejecting new connection due to failing validation of XPC connection with status %lu and error: %@", validationStatus, validationError.localizedDescription); [newConnection invalidate]; return NO; } } // Configure the connection. // First, set the interface that the exported object implements. newConnection.exportedInterface = [NSXPCInterface interfaceWithProtocol:@protocol(SUInstallerLauncherProtocol)]; // Next, set the object that the connection exports. All messages sent on the connection to this service will be sent to the exported object to handle. The connection retains the exported object. SUInstallerLauncher *exportedObject = [SUInstallerLauncher new]; newConnection.exportedObject = exportedObject; // Resuming the connection allows the system to deliver more incoming messages. [newConnection resume]; // Returning YES from this method tells the system that you have accepted this connection. If you want to reject the connection for some reason, call -invalidate on the connection and return NO. return YES; } @end int main(int __unused argc, const char * __unused argv[]) { // Create the delegate for the service. ServiceDelegate *delegate = [ServiceDelegate new]; // Set up the one NSXPCListener for this service. It will handle all incoming connections. NSXPCListener *listener = [NSXPCListener serviceListener]; listener.delegate = delegate; // Resuming the serviceListener starts this service. This method does not return. [listener resume]; return 0; } ================================================ FILE: InstallerStatus/Info.plist ================================================ CFBundleDevelopmentRegion en CFBundleExecutable $(EXECUTABLE_NAME) CFBundleIdentifier $(PRODUCT_BUNDLE_IDENTIFIER) CFBundleInfoDictionaryVersion 6.0 CFBundleName $(PRODUCT_NAME) CFBundlePackageType XPC! CFBundleShortVersionString $(MARKETING_VERSION) CFBundleSignature ???? CFBundleVersion ${CURRENT_PROJECT_VERSION} NSHumanReadableCopyright Copyright © 2016 Sparkle Project. All rights reserved. XPCService ServiceType Application ================================================ FILE: InstallerStatus/SUInstallerStatus.h ================================================ // // SUInstallerStatus.h // InstallerStatus // // Created by Mayur Pawashe on 7/10/16. // Copyright © 2016 Sparkle Project. All rights reserved. // #import #import "SUInstallerStatusProtocol.h" // This object implements the protocol which we have defined. It provides the actual behavior for the service. It is 'exported' by the service to make it available to the process hosting the service over an NSXPCConnection. SPU_OBJC_DIRECT_MEMBERS @interface SUInstallerStatus : NSObject - (instancetype)initWithRemote:(BOOL)remote; - (instancetype)init NS_UNAVAILABLE; @end ================================================ FILE: InstallerStatus/SUInstallerStatus.m ================================================ // // SUInstallerStatus.m // InstallerStatus // // Created by Mayur Pawashe on 7/10/16. // Copyright © 2016 Sparkle Project. All rights reserved. // #import "SUInstallerStatus.h" #include "AppKitPrevention.h" @implementation SUInstallerStatus { NSXPCConnection *_connection; void (^_invalidationBlock)(void); BOOL _remote; } - (instancetype)initWithRemote:(BOOL)remote { self = [super init]; if (self != nil) { _remote = remote; } return self; } - (void)setInvalidationHandler:(void (^)(void))invalidationHandler { if (_remote) { dispatch_async(dispatch_get_main_queue(), ^{ self->_invalidationBlock = [invalidationHandler copy]; }); } else { _invalidationBlock = [invalidationHandler copy]; } } - (void)_setServiceName:(NSString *)serviceName SPU_OBJC_DIRECT { NSXPCConnection *connection = [[NSXPCConnection alloc] initWithMachServiceName:serviceName options:(NSXPCConnectionOptions)0]; connection.remoteObjectInterface = [NSXPCInterface interfaceWithProtocol:@protocol(SUStatusInfoProtocol)]; _connection = connection; __weak __typeof__(self) weakSelf = self; _connection.interruptionHandler = ^{ dispatch_async(dispatch_get_main_queue(), ^{ __typeof__(self) strongSelf = weakSelf; if (strongSelf != nil) { [strongSelf->_connection invalidate]; } }); }; _connection.invalidationHandler = ^{ dispatch_async(dispatch_get_main_queue(), ^{ __typeof__(self) strongSelf = weakSelf; if (strongSelf != nil) { strongSelf->_connection = nil; [strongSelf _invokeInvalidationBlock]; } }); }; [_connection resume]; } - (void)setServiceName:(NSString *)serviceName { if (_remote) { dispatch_async(dispatch_get_main_queue(), ^{ [self _setServiceName:serviceName]; }); } else { [self _setServiceName:serviceName]; } } - (void)probeStatusInfoWithReply:(void (^)(NSData * _Nullable installationInfoData))reply { if (_remote) { dispatch_async(dispatch_get_main_queue(), ^{ [(id)self->_connection.remoteObjectProxy probeStatusInfoWithReply:reply]; }); } else { [(id)_connection.remoteObjectProxy probeStatusInfoWithReply:reply]; } } - (void)probeStatusConnectivityWithReply:(void (^)(void))reply { if (_remote) { dispatch_async(dispatch_get_main_queue(), ^{ [(id)self->_connection.remoteObjectProxy probeStatusConnectivityWithReply:reply]; }); } else { [(id)_connection.remoteObjectProxy probeStatusConnectivityWithReply:reply]; } } - (void)_invokeInvalidationBlock SPU_OBJC_DIRECT { if (_invalidationBlock != nil) { _invalidationBlock(); _invalidationBlock = nil; } } // This method can be called from us or a remote - (void)invalidate { dispatch_async(dispatch_get_main_queue(), ^{ [self->_connection invalidate]; self->_connection = nil; [self _invokeInvalidationBlock]; }); } @end ================================================ FILE: InstallerStatus/SUInstallerStatusProtocol.h ================================================ // // SUInstallerStatusProtocol.h // InstallerStatus // // Created by Mayur Pawashe on 7/10/16. // Copyright © 2016 Sparkle Project. All rights reserved. // #import #import "SUStatusInfoProtocol.h" NS_ASSUME_NONNULL_BEGIN // The protocol that this service will vend as its API. This header file will also need to be visible to the process hosting the service. @protocol SUInstallerStatusProtocol // Even though this is declared in SUStatusInfoProtocol, we should declare it here because macOS 10.8 doesn't traverse adopted protocols, // which is why this protocol doesn't adopt SUStatusInfoProtocol - (void)probeStatusInfoWithReply:(void (^)(NSData * _Nullable installationInfoData))reply; // Even though this is declared in SUStatusInfoProtocol, we should declare it here because macOS 10.8 doesn't traverse adopted protocols, // which is why this protocol doesn't adopt SUStatusInfoProtocol - (void)probeStatusConnectivityWithReply:(void (^)(void))reply; - (void)setInvalidationHandler:(void (^)(void))invalidationHandler; - (void)setServiceName:(NSString *)serviceName; - (void)invalidate; @end NS_ASSUME_NONNULL_END ================================================ FILE: InstallerStatus/SUXPCInstallerStatus.h ================================================ // // SUXPCInstallerStatus.h // Sparkle // // Created by Mayur Pawashe on 7/10/16. // Copyright © 2016 Sparkle Project. All rights reserved. // #if INSTALLER_STATUS_XPC_SERVICE_EMBEDDED #import #import "SUInstallerStatusProtocol.h" @interface SUXPCInstallerStatus : NSObject @end #endif ================================================ FILE: InstallerStatus/SUXPCInstallerStatus.m ================================================ // // SUXPCInstallerStatus.m // Sparkle // // Created by Mayur Pawashe on 7/10/16. // Copyright © 2016 Sparkle Project. All rights reserved. // #if INSTALLER_STATUS_XPC_SERVICE_EMBEDDED #import "SUXPCInstallerStatus.h" #include "AppKitPrevention.h" @implementation SUXPCInstallerStatus { NSXPCConnection *_connection; void (^_invalidationBlock)(void); } - (instancetype)init { self = [super init]; if (self != nil) { _connection = [[NSXPCConnection alloc] initWithServiceName:@INSTALLER_STATUS_BUNDLE_ID]; _connection.remoteObjectInterface = [NSXPCInterface interfaceWithProtocol:@protocol(SUInstallerStatusProtocol)]; __weak __typeof__(self) weakSelf = self; _connection.invalidationHandler = ^{ [weakSelf invokeInvalidation]; }; _connection.interruptionHandler = ^{ __typeof__(self) strongSelf = weakSelf; if (strongSelf != nil) { [strongSelf invokeInvalidation]; [strongSelf->_connection invalidate]; } }; [_connection resume]; } return self; } - (void)setInvalidationHandler:(void (^)(void))invalidationHandler { _invalidationBlock = [invalidationHandler copy]; __weak __typeof__(self) weakSelf = self; [(id)_connection.remoteObjectProxy setInvalidationHandler:^{ [weakSelf invokeInvalidation]; }]; } - (void)setServiceName:(NSString *)serviceName { [(id)_connection.remoteObjectProxy setServiceName:serviceName]; } - (void)probeStatusInfoWithReply:(void (^)(NSData * _Nullable installationInfoData))reply { [(id)_connection.remoteObjectProxy probeStatusInfoWithReply:reply]; } - (void)probeStatusConnectivityWithReply:(void (^)(void))reply { [(id)_connection.remoteObjectProxy probeStatusConnectivityWithReply:reply]; } - (void)invalidate { [(id)_connection.remoteObjectProxy invalidate]; [_connection invalidate]; _connection = nil; } - (void)invokeInvalidation { if (_invalidationBlock != nil) { _invalidationBlock(); _invalidationBlock = nil; } } @end #endif ================================================ FILE: InstallerStatus/main.m ================================================ // // main.m // InstallerStatus // // Created by Mayur Pawashe on 7/10/16. // Copyright © 2016 Sparkle Project. All rights reserved. // #import #import "SUInstallerStatus.h" #include "AppKitPrevention.h" @interface ServiceDelegate : NSObject @end @implementation ServiceDelegate - (BOOL)listener:(NSXPCListener *)__unused listener shouldAcceptNewConnection:(NSXPCConnection *)newConnection { // This method is where the NSXPCListener configures, accepts, and resumes a new incoming NSXPCConnection. // Configure the connection. // First, set the interface that the exported object implements. newConnection.exportedInterface = [NSXPCInterface interfaceWithProtocol:@protocol(SUInstallerStatusProtocol)]; // Next, set the object that the connection exports. All messages sent on the connection to this service will be sent to the exported object to handle. The connection retains the exported object. SUInstallerStatus *exportedObject = [[SUInstallerStatus alloc] initWithRemote:YES]; newConnection.exportedObject = exportedObject; // Resuming the connection allows the system to deliver more incoming messages. [newConnection resume]; // Returning YES from this method tells the system that you have accepted this connection. If you want to reject the connection for some reason, call -invalidate on the connection and return NO. return YES; } @end int main(int __unused argc, const char * __unused argv[]) { // Create the delegate for the service. ServiceDelegate *delegate = [ServiceDelegate new]; // Set up the one NSXPCListener for this service. It will handle all incoming connections. NSXPCListener *listener = [NSXPCListener serviceListener]; listener.delegate = delegate; // Resuming the serviceListener starts this service. This method does not return. [listener resume]; return 0; } ================================================ FILE: LICENSE ================================================ Copyright (c) 2006-2013 Andy Matuschak. Copyright (c) 2009-2013 Elgato Systems GmbH. Copyright (c) 2011-2014 Kornel Lesiński. Copyright (c) 2015-2017 Mayur Pawashe. Copyright (c) 2014 C.W. Betts. Copyright (c) 2014 Petroules Corporation. Copyright (c) 2014 Big Nerd Ranch. All rights reserved. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================= EXTERNAL LICENSES ================= bspatch.c and bsdiff.c, from bsdiff 4.3 : Copyright 2003-2005 Colin Percival All rights reserved Redistribution and use in source and binary forms, with or without modification, are permitted providing that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -- sais.c and sais.h, from sais-lite (2010/08/07) : The sais-lite copyright is as follows: Copyright (c) 2008-2010 Yuta Mori All Rights Reserved. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -- Portable C implementation of Ed25519, from https://github.com/orlp/ed25519 Copyright (c) 2015 Orson Peters This software is provided 'as-is', without any express or implied warranty. In no event will the authors be held liable for any damages arising from the use of this software. Permission is granted to anyone to use this software for any purpose, including commercial applications, and to alter it and redistribute it freely, subject to the following restrictions: 1. The origin of this software must not be misrepresented; you must not claim that you wrote the original software. If you use this software in a product, an acknowledgment in the product documentation would be appreciated but is not required. 2. Altered source versions must be plainly marked as such, and must not be misrepresented as being the original software. 3. This notice may not be removed or altered from any source distribution. -- SUSignatureVerifier.m: Copyright (c) 2011 Mark Hamlin. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted providing that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ================================================ FILE: Makefile ================================================ .PHONY: all localizable-strings release build test ci all: build ifndef BUILDDIR BUILDDIR := $(shell mkdir -p "build" && mktemp -d "build/Sparkle.XXXXXX") endif localizable-strings: rm -f Sparkle/Base.lproj/Sparkle.strings genstrings -o Sparkle/Base.lproj -s SULocalizedString Sparkle/*.m iconv -f UTF-16 -t UTF-8 < Sparkle/Base.lproj/Localizable.strings > Sparkle/Base.lproj/Sparkle.strings rm Sparkle/Base.lproj/Localizable.strings release: xcodebuild -scheme Distribution -configuration Release -derivedDataPath "$(BUILDDIR)" build ./Configurations/release-move-tag.sh open "$(BUILDDIR)/Build/Products/Release/" cat Sparkle.podspec @echo "Don't forget to update CocoaPods! pod trunk push" @echo "Don't forget to upload Sparkle-for-Swift-Package-Manager.zip!" build: xcodebuild clean build # Need to first gem install jazzy to run this rule docs: jazzy --author "Sparkle Project" --objc --umbrella-header Sparkle/Sparkle.h --framework-root . --readme Documentation/API_README.markdown --theme jony --output Documentation/html uitest: xcodebuild -scheme UITests -configuration Debug test check-localizations: ./Sparkle/CheckLocalizations.swift -root . -htmlPath "$(TMPDIR)/LocalizationsReport.htm" open "$(TMPDIR)/LocalizationsReport.htm" ================================================ FILE: Package.swift ================================================ // swift-tools-version:5.3 import PackageDescription // Version is technically not required here, SPM doesn't check let version = "2.9.0" // Tag is required to point towards the right asset. SPM requires the tag to follow semantic versioning to be able to resolve it. let tag = "2.9.0" let checksum = "44a0d5a00d5bfb6c2ad1755a92b694d948876d61ad44f35a0119eb0e7ace4b65" let url = "https://github.com/sparkle-project/Sparkle/releases/download/\(tag)/Sparkle-for-Swift-Package-Manager.zip" let package = Package( name: "Sparkle", platforms: [.macOS(.v10_13)], // leaving "10.13" as a breadcrumb for searching products: [ .library( name: "Sparkle", targets: ["Sparkle"]) ], targets: [ .binaryTarget( name: "Sparkle", url: url, checksum: checksum ) ] ) ================================================ FILE: README.markdown ================================================ # Sparkle 2 [![Build Status](https://github.com/sparkle-project/Sparkle/actions/workflows/ci.yml/badge.svg?branch=2.x)](https://github.com/sparkle-project/Sparkle/actions/workflows/ci.yml) ![SwiftPM](https://img.shields.io/badge/SwiftPM-compatible-4BC51D.svg?style=flat) [![Carthage compatible](https://img.shields.io/badge/Carthage-compatible-4BC51D.svg?style=flat)](https://github.com/Carthage/Carthage) [![CocoaPods](https://img.shields.io/cocoapods/v/Sparkle.svg?cacheSeconds=86400)](https://cocoapods.org/pods/Sparkle) Secure and reliable software update framework for macOS. Sparkle shows familiar update window with release notes Sparkle 2 adds support for application sandboxing, custom user interfaces, updating external bundles, and a more modern architecture which includes faster and more reliable installs. Pre-releases when available can be found on the [Sparkle's Releases](https://github.com/sparkle-project/Sparkle/releases) or on your favorite package manager. More nightly builds can be downloaded by selecting a recent [workflow run](https://github.com/sparkle-project/Sparkle/actions?query=event%3Apush+is%3Asuccess+branch%3A2.x) and downloading the corresponding Sparkle-distribution artifact. The current status for future versions of Sparkle is tracked by [its roadmap](https://github.com/sparkle-project/Sparkle/milestones). Please visit [Sparkle's website](http://sparkle-project.org) for up to date documentation on using and migrating over to Sparkle 2. Refer to [Changelog](CHANGELOG) for a more detailed list of changes. More internal design documents to the project can be found in the repository under [Documentation](Documentation/). ## Features * Seamless. There's no mention of Sparkle; your icons and app name are used. * Secure. Updates are verified using EdDSA signatures and Apple Code Signing. Supports Sandboxed applications in Sparkle 2. * Fast. Supports delta updates which only patch files that have changed and atomic-safe installs. * Easy to install. Sparkle requires no code in your app, and only needs static files on a web server. * Customizable. Sparkle 2 supports plugging in a custom UI for updates. * Flexible. Supports applications, package installers, preference panes, and other plug-ins. Sparkle 2 supports updating external bundles. * Handles permissions, quarantine, and automatically asks for authentication if needed. * Uses RSS-based appcasts for release information. Appcasts are a de-facto standard supported by 3rd party update-tracking programs and websites. * Stays hidden until second launch for better first impressions. * Truly self-updating — the user can choose to automatically download and install all updates in the background. * Ability to use channels for beta updates (in Sparkle 2), add phased rollouts to users, and mark updates as critical or major. * Progress and status notifications for the host app. ## Requirements * Runtime: macOS 10.13 or later. * Build: Latest major Xcode (stable or beta, whichever is latest) and one major version less. * HTTPS server for serving updates (see [App Transport Security](http://sparkle-project.org/documentation/app-transport-security/)) ## Usage See [getting started guide](https://sparkle-project.org/documentation/). No code is necessary, but a bit of configuration is required. ### Troubleshooting * Please check **Console.app** for logs under your application. Sparkle prints detailed information there about all problems it encounters. It often also suggests solutions to the problems, so please read Sparkle's log messages carefully. * Use the `generate_appcast` tool which creates appcast files, correct signatures, and delta updates automatically. * Make sure the URL specified in [`SUFeedURL`](https://sparkle-project.org/documentation/customization/) is valid (typos/404s are a common error!), and that it uses modern TLS ([test it](https://www.ssllabs.com/ssltest/)). ### API symbols Sparkle is built with `-fvisibility=hidden -fvisibility-inlines-hidden` which means no symbols are exported by default. If you are adding a symbol to the public API you must decorate the declaration with the `SU_EXPORT` macro (grep the source code for examples). ### Building the distribution package You do not usually need to build a Sparkle distribution unless you're making changes to Sparkle itself. To build a Sparkle distribution, `cd` to the root of the Sparkle source tree and run `make release`. Sparkle-*VERSION*.tar.xz will be created and revealed in Finder after the build has completed. Alternatively, build the Distribution scheme in the Xcode UI. ### Code of Conduct We pledge to have an open and welcoming environment. See our [Code of Conduct](CODE_OF_CONDUCT.md). ================================================ FILE: Resources/AppIcon.icon/icon.json ================================================ { "fill" : { "automatic-gradient" : "srgb:1.00000,1.00000,1.00000,1.00000" }, "groups" : [ { "layers" : [ { "fill-specializations" : [ { "value" : { "solid" : "extended-srgb:0.00000,0.47843,1.00000,1.00000" } }, { "appearance" : "dark", "value" : { "solid" : "extended-srgb:0.00000,0.47843,1.00000,1.00000" } } ], "glass" : true, "hidden" : false, "image-name" : "sparkles.png", "name" : "sparkles", "position" : { "scale" : 1.15, "translation-in-points" : [ 0, 0 ] } }, { "glass" : true, "hidden" : false, "image-name" : "circular_arrow.png", "name" : "circular_arrow", "position" : { "scale" : 1.25, "translation-in-points" : [ 0, 0 ] } } ], "shadow" : { "kind" : "neutral", "opacity" : 0.5 }, "translucency" : { "enabled" : true, "value" : 0.5 } } ], "supported-platforms" : { "circles" : [ "watchOS" ], "squares" : "shared" } } ================================================ FILE: Resources/Images.xcassets/AppIcon.appiconset/Contents.json ================================================ { "images" : [ { "size" : "16x16", "idiom" : "mac", "filename" : "icon_16x16.png", "scale" : "1x" }, { "size" : "16x16", "idiom" : "mac", "filename" : "icon_16x16@2x.png", "scale" : "2x" }, { "size" : "32x32", "idiom" : "mac", "filename" : "icon_32x32.png", "scale" : "1x" }, { "size" : "32x32", "idiom" : "mac", "filename" : "icon_32x32@2x.png", "scale" : "2x" }, { "size" : "128x128", "idiom" : "mac", "filename" : "icon_128x128.png", "scale" : "1x" }, { "size" : "128x128", "idiom" : "mac", "filename" : "icon_128x128@2x.png", "scale" : "2x" }, { "size" : "256x256", "idiom" : "mac", "filename" : "icon_256x256.png", "scale" : "1x" }, { "size" : "256x256", "idiom" : "mac", "filename" : "icon_256x256@2x.png", "scale" : "2x" }, { "size" : "512x512", "idiom" : "mac", "filename" : "icon_512x512.png", "scale" : "1x" }, { "size" : "512x512", "idiom" : "mac", "filename" : "icon_512x512@2x.png", "scale" : "2x" } ], "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: Resources/ReleaseNotesColorStyle.css ================================================ @media (prefers-color-scheme: dark) { html { color-scheme: dark; color: white; background: transparent; } :link { color: #419CFF; } :link:active { color: #FF1919; } } ================================================ FILE: Resources/SampleAppcast.xml ================================================ Your Great App's Changelog Most recent changes with links to updates. en Version 2.0 (2 bugs fixed; 3 new features) https://sparkle-project.org 2.0 https://you.com/app/2.0.html https://you.com/app/changelog.php Wed, 09 Jan 2006 19:20:11 +0000 10.13 Version 1.5 (8 bugs fixed; 2 new features) https://sparkle-project.org 1.5 https://you.com/app/1.5.html https://you.com/app/changelog.php?up-to=1.5 Wed, 01 Jan 2006 12:20:11 +0000 10.13 Version 1.4 (5 bugs fixed; 2 new features) https://sparkle-project.org https://you.com/app/1.4.html https://you.com/app/changelog.php?up-to=1.4 241 1.4 Wed, 25 Dec 2005 12:20:11 +0000 10.13 ================================================ FILE: Sparkle/AppKitPrevention.h ================================================ // // AppKitPrevention.h // Sparkle // // Created by Mayur Pawashe on 1/17/17. // Copyright © 2017 Sparkle Project. All rights reserved. // // #include (not #import) this header to prevent AppKit from being imported // Note this should be your LAST #include in your implementation file // If this error is triggered, you can have Xcode indicate to you which source file including this header caused the issue #ifdef _APPKITDEFINES_H #error This is a core or daemon-safe module and should NOT import AppKit #endif ================================================ FILE: Sparkle/Autoupdate/TerminationListener.h ================================================ // // TerminationListener.h // Sparkle // // Created by Mayur Pawashe on 3/7/16. // Copyright © 2016 Sparkle Project. All rights reserved. // #import NS_ASSUME_NONNULL_BEGIN @interface TerminationListener : NSObject - (instancetype)initWithProcessIdentifier:(NSNumber * _Nullable)processIdentifier; @property (nonatomic, readonly) BOOL terminated; // If the process identifier provided was nil, then the completion block will invoke immediately with a YES success - (void)startListeningWithCompletion:(void (^)(BOOL success))completionBlock; @end NS_ASSUME_NONNULL_END ================================================ FILE: Sparkle/Autoupdate/TerminationListener.m ================================================ // // TerminationListener.m // Sparkle // // Created by Mayur Pawashe on 3/7/16. // Copyright © 2016 Sparkle Project. All rights reserved. // #import "TerminationListener.h" #import "SULog.h" #include #include #include #include "AppKitPrevention.h" @interface TerminationListener () @property (nonatomic, readonly, nullable) NSNumber *processIdentifier; @property (nonatomic) BOOL watchedTermination; @property (nonatomic, copy) void (^completionBlock)(BOOL); @end @implementation TerminationListener @synthesize completionBlock = _completionBlock; @synthesize processIdentifier = _processIdentifier; @synthesize watchedTermination = _watchedTermination; - (instancetype)initWithProcessIdentifier:(NSNumber * _Nullable)processIdentifier { self = [super init]; if (self != nil) { _processIdentifier = processIdentifier; } return self; } - (BOOL)terminated { return (self.watchedTermination || self.processIdentifier == nil) ? YES : (kill(self.processIdentifier.intValue, 0) != 0); } - (void)invokeCompletionWithSuccess:(BOOL)success { if (self.completionBlock != nil) { self.completionBlock(success); self.completionBlock = nil; } } - (void)startListeningWithCompletion:(void (^)(BOOL))completionBlock { self.completionBlock = completionBlock; if (self.processIdentifier == nil) { [self invokeCompletionWithSuccess:YES]; return; } // Use kqueues to determine when the process will terminate // As described in https://developer.apple.com/library/mac/technotes/tn2050/_index.html#//apple_ref/doc/uid/DTS10003081-CH1-SUBSECTION10 // By using kqueues, we can stay away from using AppKit in case we ever decide to abandon it pid_t processIdentifier = self.processIdentifier.intValue; int queue = kqueue(); if (queue == -1) { SULog(SULogLevelError, @"Failed to create kqueue() due to error %d: %@", errno, @(strerror(errno))); [self invokeCompletionWithSuccess:NO]; return; } struct kevent changes; EV_SET(&changes, processIdentifier, EVFILT_PROC, EV_ADD | EV_RECEIPT, NOTE_EXIT, 0, NULL); if (kevent(queue, &changes, 1, &changes, 1, NULL) == -1) { SULog(SULogLevelError, @"Failed to invoke kevent() due to error %d: %@", errno, @(strerror(errno))); [self invokeCompletionWithSuccess:NO]; return; } // We will assume this terminationListener will never be deallocated #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wcast-qual" CFFileDescriptorContext context = { 0, (void *)CFBridgingRetain(self), NULL, NULL, NULL }; #pragma clang diagnostic pop CFFileDescriptorRef noteExitKQueueRef = CFFileDescriptorCreate(NULL, queue, true, noteExitKQueueCallback, &context); if (noteExitKQueueRef == NULL) { SULog(SULogLevelError, @"Failed to create file descriptor via CFFileDescriptorCreate()"); CFRelease((__bridge CFTypeRef)(self)); [self invokeCompletionWithSuccess:NO]; return; } CFRunLoopSourceRef runLoopSource = CFFileDescriptorCreateRunLoopSource(NULL, noteExitKQueueRef, 0); if (runLoopSource == NULL) { SULog(SULogLevelError, @"Failed to create runLoopSource via CFFileDescriptorCreateRunLoopSource()"); CFRelease((__bridge CFTypeRef)(self)); [self invokeCompletionWithSuccess:NO]; return; } CFRunLoopAddSource(CFRunLoopGetCurrent(), runLoopSource, kCFRunLoopDefaultMode); CFRelease(runLoopSource); CFFileDescriptorEnableCallBacks(noteExitKQueueRef, kCFFileDescriptorReadCallBack); // Make sure we didn't set the listener callback to a dead PID // If we did, we could hang forever. To avoid this, we check if the process has terminated *after* we set up the callback // If we tried to do this check before setting the callback, we could run into an issue where the process can terminate after our check // but before setting the callback if ([self terminated]) { [self invokeCompletionWithSuccess:YES]; } dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ if ([self terminated]) { [self invokeCompletionWithSuccess:YES]; } }); } static void noteExitKQueueCallback(CFFileDescriptorRef file, CFOptionFlags __unused callBackTypes, void *info) { struct kevent event; kevent(CFFileDescriptorGetNativeDescriptor(file), NULL, 0, &event, 1, NULL); TerminationListener *self = CFBridgingRelease(info); self.watchedTermination = YES; [self invokeCompletionWithSuccess:YES]; } @end ================================================ FILE: Sparkle/Base.lproj/Sparkle.strings ================================================ /* Description text for SUUpdateAlert when the critical update has already been downloaded and ready to install. */ "%1$@ %2$@ has been downloaded and is ready to use! This is an important update; would you like to install it and relaunch %1$@ now?" = "%1$@ %2$@ has been downloaded and is ready to use! This is an important update; would you like to install it and relaunch %1$@ now?"; /* Description text for SUUpdateAlert when the update has already been downloaded and ready to install. */ "%1$@ %2$@ has been downloaded and is ready to use! Would you like to install it and relaunch %1$@ now?" = "%1$@ %2$@ has been downloaded and is ready to use! Would you like to install it and relaunch %1$@ now?"; /* No comment provided by engineer. */ "%1$@ %2$@ is available but your macOS version is too new for this update. This update only supports up to macOS %3$@." = "%1$@ %2$@ is available but your macOS version is too new for this update. This update only supports up to macOS %3$@."; /* No comment provided by engineer. */ "%1$@ %2$@ is available but your macOS version is too old to install it. At least macOS %3$@ is required." = "%1$@ %2$@ is available but your macOS version is too old to install it. At least macOS %3$@ is required."; /* Mac is too old and update requires Apple silicon Mac */ "%1$@ %2$@ is available but this update requires a new Apple silicon Mac." = "%1$@ %2$@ is available but this update requires a new Apple silicon Mac."; /* No comment provided by engineer. */ "%1$@ can’t be updated if it’s running from the location it was downloaded to." = "%1$@ can’t be updated if it’s running from the location it was downloaded to."; /* No comment provided by engineer. */ "%1$@ can’t be updated because it was opened from a read-only or a temporary location." = "%1$@ can’t be updated because it was opened from a read-only or a temporary location."; /* No comment provided by engineer. */ "%@ %@ is currently the newest version available." = "%1$@ %2$@ is currently the newest version available."; /* No comment provided by engineer. */ "%@ %@ is currently the newest version available.\n(You are currently running version %@.)" = "%1$@ %2$@ is currently the newest version available.\n(You are currently running version %3$@.)"; /* Description text for SUUpdateAlert when the critical update is downloadable. */ "%@ %@ is now available—you have %@. This is an important update; would you like to download it now?" = "%1$@ %2$@ is now available—you have %3$@. This is an important update; would you like to download it now?"; /* Description text for SUUpdateAlert when the update is downloadable. */ "%@ %@ is now available—you have %@. Would you like to download it now?" = "%1$@ %2$@ is now available—you have %3$@. Would you like to download it now?"; /* Description text for SUUpdateAlert when the update informational with no download. */ "%@ %@ is now available—you have %@. Would you like to learn more about this update on the web?" = "%1$@ %2$@ is now available—you have %3$@. Would you like to learn more about this update on the web?"; /* The download progress in a unit of bytes, e.g. 100 KB */ "%@ downloaded" = "%@ downloaded"; /* No comment provided by engineer. */ "%@ is now updated to version %@!" = "%1$@ is now updated to version %2$@!"; /* No comment provided by engineer. */ "%@ is now updated!" = "%@ is now updated!"; /* The download progress in units of bytes, e.g. 100 KB of 1,0 MB */ "%@ of %@" = "%1$@ of %2$@"; /* No comment provided by engineer. */ "A new version of %@ is available!" = "A new version of %@ is available!"; /* No comment provided by engineer. */ "A new version of %@ is ready to install!" = "A new version of %@ is ready to install!"; /* No comment provided by engineer. */ "An error occurred in retrieving update information. Please try again later." = "An error occurred in retrieving update information. Please try again later."; /* No comment provided by engineer. */ "An error occurred while connecting to the installer. Please try again later." = "An error occurred while connecting to the installer. Please try again later."; /* No comment provided by engineer. */ "An error occurred while downloading the update. Please try again later." = "An error occurred while downloading the update. Please try again later."; /* No comment provided by engineer. */ "An error occurred while downloading the release notes." = "An error occurred while downloading the release notes."; /* No comment provided by engineer. */ "An error occurred while extracting the archive. Please try again later." = "An error occurred while extracting the archive. Please try again later."; /* No comment provided by engineer. */ "An error occurred while launching the installer. Please try again later." = "An error occurred while launching the installer. Please try again later."; /* No comment provided by engineer. */ "An error occurred while parsing the update feed." = "An error occurred while parsing the update feed."; /* No comment provided by engineer. */ "An error occurred while running the updater. Please try again later." = "An error occurred while running the updater. Please try again later."; /* No comment provided by engineer. */ "An error occurred while starting the installer. Please try again later." = "An error occurred while starting the installer. Please try again later."; /* No comment provided by engineer. */ "An important update to %@ is ready to install" = "An important update to %@ is ready to install"; /* No comment provided by engineer. */ "Application Name" = "Application Name"; /* No comment provided by engineer. */ "Application Version" = "Application Version"; /* No comment provided by engineer. */ "Cancel" = "Cancel"; /* No comment provided by engineer. */ "Cancel Update" = "Cancel Update"; /* No comment provided by engineer. */ "Checking for updates…" = "Checking for updates…"; /* No comment provided by engineer. */ "CPU is 64-Bit?" = "CPU is 64-Bit?"; /* No comment provided by engineer. */ "CPU Speed (MHz)" = "CPU Speed (MHz)"; /* No comment provided by engineer. */ "CPU Subtype" = "CPU Subtype"; /* No comment provided by engineer. */ "CPU Type" = "CPU Type"; /* Take care not to overflow the status window. */ "Downloading update…" = "Downloading update…"; /* Take care not to overflow the status window. */ "Extracting update…" = "Extracting update…"; /* No comment provided by engineer. */ "Failed to resume installing update." = "Failed to resume installing update."; /* No comment provided by engineer. */ "Install and Relaunch" = "Install and Relaunch"; /* Alternate title for 'Remind Me Later' button when downloaded updates can be resumed */ "Install on Quit" = "Install on Quit"; /* Take care not to overflow the status window. */ "Installing update…" = "Installing update…"; /* Alternate title for 'Install Update' button when there's no download in RSS feed. */ "Learn More…" = "Learn More…"; /* No comment provided by engineer. */ "Mac Model" = "Mac Model"; /* No comment provided by engineer. */ "Memory (MB)" = "Memory (MB)"; /* No comment provided by engineer. */ "No" = "No"; /* No comment provided by engineer. */ "Number of CPUs" = "Number of CPUs"; /* No comment provided by engineer. */ "OK" = "OK"; /* No comment provided by engineer. */ "OS Version" = "OS Version"; /* No comment provided by engineer. */ "Preferred Language" = "Preferred Language"; /* No comment provided by engineer. */ "Quit %1$@, move it into your Applications folder, relaunch it from there and try again." = "Quit %1$@, move it into your Applications folder, relaunch it from there and try again."; /* No comment provided by engineer. */ "Ready to Install" = "Ready to Install"; /* No comment provided by engineer. */ "Should %1$@ automatically check for updates? You can always check for updates manually from the %1$@ menu." = "Should %1$@ automatically check for updates? You can always check for updates manually from the %1$@ menu."; /* No comment provided by engineer. */ "The installation failed due to not having permission to write the new update." = "The installation failed due to not having permission to write the new update."; /* No comment provided by engineer. */ "The updater failed to start. Please verify you have the latest version of %@ and contact the app developer if the issue still persists. Check the Console logs for more information." = "The updater failed to start. Please verify you have the latest version of %@ and contact the app developer if the issue still persists. Check the Console logs for more information."; /* No comment provided by engineer. */ "The update is improperly signed and could not be validated. Please try again later or contact the app developer." = "The update is improperly signed and could not be validated. Please try again later or contact the app developer."; /* No comment provided by engineer. */ "The update feed is improperly signed and could not be validated. Please try again later or contact the app developer." = "The update feed is improperly signed and could not be validated. Please try again later or contact the app developer."; /* No comment provided by engineer. */ "The release notes is improperly signed and could not be validated. Please contact the app developer for more information." = "The release notes is improperly signed and could not be validated. Please contact the app developer for more information."; /* No comment provided by engineer. */ "Unable to Check For Updates" = "Unable to Check For Updates"; /* No comment provided by engineer. */ "Update Error!" = "Update Error!"; /* No comment provided by engineer. */ "Update Installed" = "Update Installed"; /* No comment provided by engineer. */ "Updating %@" = "Updating %@"; /* No comment provided by engineer. */ "Use Finder to copy %1$@ to the Applications folder, relaunch it from there, and try again." = "Use Finder to copy %1$@ to the Applications folder, relaunch it from there, and try again."; /* No comment provided by engineer. */ "Version History" = "Version History"; /* No comment provided by engineer. */ "Yes" = "Yes"; /* No comment provided by engineer. */ "You may need to allow modifications from %1$@ in System Settings under Privacy & Security and App Management to install future updates." = "You may need to allow modifications from %1$@ in System Settings under Privacy & Security and App Management to install future updates."; /* No comment provided by engineer. */ "Your macOS version is too new" = "Your macOS version is too new"; /* No comment provided by engineer. */ "Your macOS version is too old" = "Your macOS version is too old"; /* Mac is too old and update requires newer hardware */ "Your Mac is too old" = "Your Mac is too old"; /* Status message shown when the user checks for updates but is already current or the feed doesn't contain any updates. */ "You’re up to date!" = "You’re up to date!"; /* Software Update title/label */ "Software Update" = "Software Update"; /* Remind Me Later choice for update alert */ "Remind Me Later" = "Remind Me Later"; /* Skip This Version choice for update alert */ "Skip This Version" = "Skip This Version"; /* Install Update choice for update alert */ "Install Update" = "Install Update"; /* Automatically download and install updates in the future option for update alert */ "Automatically download and install updates in the future" = "Automatically download and install updates in the future"; /* Check for updates automatically response for update permission dialog */ "Check Automatically" = "Check Automatically"; /* Don't check for updates automatically response for update permission dialog */ "Don’t Check" = "Don’t Check"; /* Title question for update permission dialog */ "Check for updates automatically?" = "Check for updates automatically?"; /* Include anonymous system profile choice for update permission dialog */ "Include anonymous system profile" = "Include anonymous system profile"; /* Automatically download and install updates choice for update permission dialog */ "Automatically download and install updates" = "Automatically download and install updates"; /* Anonymous system profile information disclosure for update permission dialog */ "Anonymous system profile information is used to help us plan future development work. Please contact us if you have any questions about this.\n\nThis is the information that would be sent:" = "Anonymous system profile information is used to help us plan future development work. Please contact us if you have any questions about this.\n\nThis is the information that would be sent:"; ================================================ FILE: Sparkle/CheckLocalizations.swift ================================================ #!/usr/bin/xcrun swift import Foundation func die(_ msg: String) { print("ERROR: \(msg)") exit(1) } extension XMLElement { convenience init(name: String, attributes: [String: String], stringValue string: String? = nil) { self.init(name: name, stringValue: string) setAttributesWith(attributes) } } let ud = UserDefaults.standard let sparkleRoot = ud.object(forKey: "root") as? String let htmlPath = ud.object(forKey: "htmlPath") as? String if sparkleRoot == nil || htmlPath == nil { die("Missing arguments") } let enStringsPath = sparkleRoot! + "/Sparkle/Base.lproj/Sparkle.strings" let enStringsDict = NSDictionary(contentsOfFile: enStringsPath) if enStringsDict == nil { die("Invalid English strings") } let enStringsDictKeys = enStringsDict!.allKeys let dirPath = NSString(string: sparkleRoot! + "/Sparkle") let dirContents = try! FileManager.default.contentsOfDirectory(atPath: dirPath as String) let css = "body { font-family: sans-serif; font-size: 10pt; }" + "h1 { font-size: 12pt; }" + ".missing { background-color: #FFBABA; color: #D6010E; white-space: pre; }" + ".unused { background-color: #BDE5F8; color: #00529B; white-space: pre; }" + ".unlocalized { background-color: #FEEFB3; color: #9F6000; white-space: pre; }" var html = XMLDocument(rootElement: XMLElement(name: "html")) html.dtd = XMLDTD() html.dtd!.name = html.rootElement()!.name html.characterEncoding = "UTF-8" html.documentContentKind = XMLDocument.ContentKind.xhtml var body = XMLElement(name: "body") var head = XMLElement(name: "head") html.rootElement()!.addChild(head) html.rootElement()!.addChild(body) head.addChild(XMLElement(name: "meta", attributes: ["charset": html.characterEncoding!])) head.addChild(XMLElement(name: "title", stringValue: "Sparkle Localizations Report")) head.addChild(XMLElement(name: "style", stringValue: css)) let locale = Locale.current for dirEntry in dirContents { if NSString(string: dirEntry).pathExtension != "lproj" || dirEntry == "Base.lproj" || dirEntry == "en.lproj" { continue } let lang = (locale as NSLocale).displayName(forKey: NSLocale.Key.languageCode, value: NSString(string: dirEntry).deletingPathExtension) body.addChild(XMLElement(name: "h1", stringValue: "\(dirEntry) (\(lang!))")) let stringsPath = NSString(string: dirPath.appendingPathComponent(dirEntry)).appendingPathComponent("Sparkle.strings") let stringsDict = NSDictionary(contentsOfFile: stringsPath) if stringsDict == nil { die("Invalid strings file \(dirEntry)") continue } var missing: [String] = [] var unlocalized: [String] = [] var unused: [String] = [] for key in enStringsDictKeys { let str = stringsDict?.object(forKey: key) as? String if str == nil { missing.append(key as! String) } else if let enStr = enStringsDict?.object(forKey: key) as? String { if enStr == str { unlocalized.append(key as! String) } } } let stringsDictKeys = stringsDict!.allKeys for key in stringsDictKeys { if enStringsDict?.object(forKey: key) == nil { unused.append(key as! String) } } let sorter = { (s1: String, s2: String) -> Bool in return s1 < s2 } missing.sort(by: sorter) unlocalized.sort(by: sorter) unused.sort(by: sorter) let addRow = { (prefix: String, cssClass: String, key: String) -> Void in body.addChild(XMLElement(name: "span", attributes: ["class": cssClass], stringValue: [prefix, key].joined(separator: " ") + "\n")) } for key in missing { addRow("Missing", "missing", key) } for key in unlocalized { addRow("Unlocalized", "unlocalized", key) } for key in unused { addRow("Unused", "unused", key) } } var err: NSError? if !((try? html.xmlData.write(to: URL(fileURLWithPath: htmlPath!), options: [.atomic])) != nil) { die("Can't write report: \(err!)") } ================================================ FILE: Sparkle/InstallerProgress/InstallerProgress-Info.plist ================================================ CFBundleDevelopmentRegion en CFBundleExecutable $(EXECUTABLE_NAME) CFBundleIconFile CFBundleIdentifier $(PRODUCT_BUNDLE_IDENTIFIER) CFBundleInfoDictionaryVersion 6.0 CFBundleName $(PRODUCT_NAME) CFBundlePackageType APPL CFBundleShortVersionString $(MARKETING_VERSION) CFBundleSignature ???? CFBundleVersion ${CURRENT_PROJECT_VERSION} LSApplicationCategoryType public.app-category.utilities LSMinimumSystemVersion $(MACOSX_DEPLOYMENT_TARGET) LSUIElement 1 CFBundleLocalizations en ca ar cs da de el es fa fi fr he hr hu is it ja ko nb nl nn pl pt-BR pt-PT ro ru sk sl sv th tr uk vi zh_CN zh_HK zh_TW NSPrincipalClass NSApplication ================================================ FILE: Sparkle/InstallerProgress/InstallerProgressAppController.h ================================================ // // InstallerProgressAppController.h // Sparkle // // Created by Mayur Pawashe on 4/10/16. // Copyright © 2016 Sparkle Project. All rights reserved. // #import NS_ASSUME_NONNULL_BEGIN @protocol InstallerProgressDelegate; SPU_OBJC_DIRECT_MEMBERS @interface InstallerProgressAppController : NSObject - (instancetype)initWithApplication:(NSApplication *)application arguments:(NSArray *)arguments delegate:(id)delegate; - (void)run; - (void)cleanupAndExitWithStatus:(int)status error:(NSError * _Nullable)error __attribute__((noreturn)); @end NS_ASSUME_NONNULL_END ================================================ FILE: Sparkle/InstallerProgress/InstallerProgressAppController.m ================================================ // // InstallerProgressAppController.m // Sparkle // // Created by Mayur Pawashe on 4/10/16. // Copyright © 2016 Sparkle Project. All rights reserved. // #import "InstallerProgressAppController.h" #import "InstallerProgressDelegate.h" #import "SPUMessageTypes.h" #import "SULog.h" #import "SULog+NSError.h" #import "SUApplicationInfo.h" #import "SPUInstallerAgentProtocol.h" #import "SUInstallerAgentInitiationProtocol.h" #import "StatusInfo.h" #import "SUHost.h" #import "SUErrors.h" #import "SUNormalization.h" #import "SUConstants.h" #import "SPUSecureCoding.h" #import "SPUInstallationInfo.h" /** * Terminate the application after a delay from launching the new update to avoid OS activation issues * This delay should be be high enough to increase the likelihood that our updated app will be launched up front, * but should be low enough so that the user doesn't ponder why the updater hasn't finished terminating yet */ static const NSTimeInterval SUTerminationTimeDelay = 0.3; #if __MAC_OS_X_VERSION_MAX_ALLOWED < 140000 @interface NSApplication (ActivationAPIs) - (void)activate; @end #endif @interface InstallerProgressAppController () @end #define CONNECTION_ACKNOWLEDGEMENT_TIMEOUT 7ull @implementation InstallerProgressAppController { NSApplication *_application; NSRunningApplication *_targetRunningApplication; NSXPCConnection *_connection; SUHost *_oldHost; NSString *_oldHostBundlePath; StatusInfo *_statusInfo; NSBundle *_applicationBundle; NSString *_normalizedPath; __weak id _delegate; void (^_terminationCompletionHandler)(void); BOOL _connected; BOOL _repliedToRegistration; BOOL _shouldRelaunchHostBundle; BOOL _systemDomain; BOOL _submittedLauncherJob; BOOL _willTerminate; BOOL _applicationInitiallyAlive; } - (instancetype)initWithApplication:(NSApplication *)application arguments:(NSArray *)arguments delegate:(id)delegate { self = [super init]; if (self != nil) { if (arguments.count != 3) { SULog(SULogLevelError, @"Error: Invalid number of arguments supplied: %@", arguments); [self cleanupAndExitWithStatus:EXIT_FAILURE error:nil]; } NSString *hostBundlePath = arguments[1]; if (hostBundlePath.length == 0) { SULog(SULogLevelError, @"Error: Host bundle path length is 0"); [self cleanupAndExitWithStatus:EXIT_FAILURE error:nil]; } NSBundle *hostBundle = [NSBundle bundleWithPath:hostBundlePath]; if (hostBundle == nil) { SULog(SULogLevelError, @"Error: Host bundle for target is nil"); [self cleanupAndExitWithStatus:EXIT_FAILURE error:nil]; } NSString *hostBundleIdentifier = hostBundle.bundleIdentifier; if (hostBundleIdentifier == nil) { SULog(SULogLevelError, @"Error: Host bundle identifier for target is nil"); [self cleanupAndExitWithStatus:EXIT_FAILURE error:nil]; return nil; // just to silence analyzer warnings later about hostBundleIdentifier being nil } SUHost *host = [[SUHost alloc] initWithBundle:hostBundle]; _shouldRelaunchHostBundle = [host boolForInfoDictionaryKey:SURelaunchHostBundleKey]; _oldHostBundlePath = host.bundlePath; _oldHost = host; // Note that we are connecting to the installer rather than the installer connecting to us // This difference is significant. We shouldn't have a model where the 'server' tries to connect to a 'client', // nor have a model where a process that runs at the highest level (the installer can run as root) tries to connect to a user level agent or process BOOL systemDomain = arguments[2].boolValue; NSXPCConnectionOptions connectionOptions = systemDomain ? NSXPCConnectionPrivileged : (NSXPCConnectionOptions)0; _systemDomain = systemDomain; _connection = [[NSXPCConnection alloc] initWithMachServiceName:SPUProgressAgentServiceNameForBundleIdentifier(hostBundleIdentifier) options:connectionOptions]; _statusInfo = [[StatusInfo alloc] initWithHostBundleIdentifier:hostBundleIdentifier]; application.delegate = self; _application = application; _delegate = delegate; [delegate loadLocalizationStringsFromHost:host]; _connection.exportedInterface = [NSXPCInterface interfaceWithProtocol:@protocol(SPUInstallerAgentProtocol)]; _connection.exportedObject = self; _connection.remoteObjectInterface = [NSXPCInterface interfaceWithProtocol:@protocol(SUInstallerAgentInitiationProtocol)]; __weak __typeof__(self) weakSelf = self; _connection.interruptionHandler = ^{ dispatch_async(dispatch_get_main_queue(), ^{ __typeof__(self) strongSelf = weakSelf; if (strongSelf != nil) { [strongSelf->_connection invalidate]; } }); }; _connection.invalidationHandler = ^{ dispatch_async(dispatch_get_main_queue(), ^{ __typeof__(self) strongSelf = weakSelf; if (strongSelf != nil) { int exitStatus = (strongSelf->_repliedToRegistration ? EXIT_SUCCESS : EXIT_FAILURE); NSError *registrationError; if (!strongSelf->_repliedToRegistration) { registrationError = [NSError errorWithDomain:SUSparkleErrorDomain code:SUAgentInvalidationError userInfo:@{ NSLocalizedDescriptionKey: @"Error: Agent Invalidating without having the chance to reply to installer" }]; } else { registrationError = nil; } if (!strongSelf->_willTerminate) { [strongSelf cleanupAndExitWithStatus:exitStatus error:registrationError]; } } }); }; } return self; } - (void)run { [_application run]; } - (void)startConnection SPU_OBJC_DIRECT { [_statusInfo startListener]; [_connection resume]; [(id)_connection.remoteObjectProxy connectionDidInitiateWithReply:^{ dispatch_async(dispatch_get_main_queue(), ^{ self->_connected = YES; }); }]; dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(CONNECTION_ACKNOWLEDGEMENT_TIMEOUT * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ if (!self->_connected) { SULog(SULogLevelError, @"Timeout error: failed to receive acknowledgement from installer"); [self cleanupAndExitWithStatus:EXIT_FAILURE error:nil]; } }); } - (void)applicationDidFinishLaunching:(NSNotification *)__unused notification { [self startConnection]; } - (void)cleanupAndExitWithStatus:(int)status error:(NSError * _Nullable)error __attribute__((noreturn)) { if (error != nil) { SULog(SULogLevelError, @"Agent failed.."); SULogError(error); [(id)_connection.remoteObjectProxy connectionWillInvalidateWithError:error]; } [_statusInfo invalidate]; [_connection invalidate]; // Remove the agent bundle; it is assumed this bundle is in a temporary/cache/support directory NSError *theError = nil; NSString *bundlePath = [[NSBundle mainBundle] bundlePath]; if (![[NSFileManager defaultManager] removeItemAtPath:bundlePath error:&theError]) { SULog(SULogLevelError, @"Couldn't remove agent bundle: %@.", theError); } else { // There should be nothing else in the parent temporary directory given to us, // so let us try to remove it. Note rmdir() will fail if there are unexpectably other // items present NSString *parentDirectory = bundlePath.stringByDeletingLastPathComponent; const char *fileSystemRepresentation = parentDirectory.fileSystemRepresentation; if (fileSystemRepresentation != NULL) { if (rmdir(fileSystemRepresentation) != 0) { SULog(SULogLevelError, @"Failed to remove parent agent bundle directory: %@: %d", parentDirectory, errno); } } } exit(status); } - (NSArray *)runningApplicationsWithBundle:(NSBundle *)bundle SPU_OBJC_DIRECT { // Resolve symlinks otherwise when we compare file paths, we may not realize two paths that are represented differently are the same NSArray *bundlePathComponents = bundle.bundlePath.stringByResolvingSymlinksInPath.pathComponents; NSString *bundleIdentifier = bundle.bundleIdentifier; NSMutableArray *matchedRunningApplications = [[NSMutableArray alloc] init]; if (bundleIdentifier != nil && bundlePathComponents != nil) { NSArray *runningApplications = [NSRunningApplication runningApplicationsWithBundleIdentifier:bundleIdentifier]; // If we find any running application that is translocated and looks like the bundle, we should record those too // We will want to terminate those apps and observe their pids, but we will only do this if we don't find any regular matches NSMutableArray *potentialMatchingTranslocatedRunningApplications = [[NSMutableArray alloc] init]; BOOL needsToHandleImproperBundles; if (@available(macOS 16, *)) { needsToHandleImproperBundles = NO; } else { needsToHandleImproperBundles = YES; } for (NSRunningApplication *runningApplication in runningApplications) { // Comparing the URLs hasn't worked well for me in practice, so I'm comparing the file paths instead NSString *candidatePath = runningApplication.bundleURL.URLByResolvingSymlinksInPath.path; if (candidatePath != nil) { NSArray *candidatePathComponents = candidatePath.pathComponents; if (needsToHandleImproperBundles) { // Workaround cases where macOS appends Contents/MacOS/ to the bundle path which can happen in corner cases (eg: mishandled bundles or helper executables). // This may happen with app bundles which have wrapper/tramopline executables that spawn the main executable // https://github.com/sparkle-project/Sparkle/issues/2725 NSUInteger candidatePathComponentsCount = candidatePathComponents.count; // > 3 is fine instead of >= 3 because there should be another parent path component for the bundle if (candidatePathComponentsCount > 3 && [candidatePathComponents[candidatePathComponentsCount - 3] isEqualToString:@"Contents"] && [candidatePathComponents[candidatePathComponentsCount - 2] isEqualToString:@"MacOS"]) { // Lastly verify if the bundle is not actually a directory // Note the IO check is the last check for this corner case // This ensures it's not an ordinary helper app that happens to be inside another app bundle. BOOL candidatePathIsDir = YES; if ([[NSFileManager defaultManager] fileExistsAtPath:candidatePath isDirectory:&candidatePathIsDir] && !candidatePathIsDir) { NSMutableArray *trimmedBundlePath = [candidatePathComponents mutableCopy]; [trimmedBundlePath removeObjectsInRange:NSMakeRange(candidatePathComponentsCount - 3, 3)]; candidatePathComponents = trimmedBundlePath; } } } if ([candidatePathComponents isEqualToArray:bundlePathComponents]) { [matchedRunningApplications addObject:runningApplication]; } else if (matchedRunningApplications.count == 0 && candidatePathComponents.count > 0 && bundlePathComponents.count > 0) { NSString *lastBundlePathComponent = bundlePathComponents.lastObject; NSString *lastCandidatePathComponent = candidatePathComponents.lastObject; if (lastBundlePathComponent != nil && lastCandidatePathComponent != nil && [lastBundlePathComponent isEqualToString:lastCandidatePathComponent] && [candidatePathComponents containsObject:@"AppTranslocation"]) { [potentialMatchingTranslocatedRunningApplications addObject:runningApplication]; } } } } // Non-translocated apps take priority first // And we only use translocated version of apps if there are no regular apps matched if (matchedRunningApplications.count == 0) { [matchedRunningApplications addObjectsFromArray:potentialMatchingTranslocatedRunningApplications]; } } return [matchedRunningApplications copy]; } - (void)registerApplicationBundlePath:(NSString *)applicationBundlePath reply:(void (^)(BOOL))reply { dispatch_async(dispatch_get_main_queue(), ^{ #pragma clang diagnostic push #if __has_warning("-Wcompletion-handler") #pragma clang diagnostic ignored "-Wcompletion-handler" #endif if (applicationBundlePath != nil && !self->_willTerminate && self->_targetRunningApplication == nil) { NSBundle *applicationBundle = [NSBundle bundleWithPath:applicationBundlePath]; if (applicationBundle == nil) { [self cleanupAndExitWithStatus:EXIT_FAILURE error:[NSError errorWithDomain:SUSparkleErrorDomain code:SUAgentInvalidationError userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Error: Encountered invalid path for waiting termination: %@", applicationBundlePath] }]]; } // Compute normalized path that we may use later for relaunching the application // We compute normalized path from progress agent instead of trusting or having the installer // pass it to us if (SPARKLE_NORMALIZE_INSTALLED_APPLICATION_NAME && [applicationBundle.bundlePath isEqualToString:self->_oldHostBundlePath]) { NSString *normalizedPath = SUNormalizedInstallationPath(self->_oldHost); // We only use normalized path if it doesn't already exist // Check the installer which has the same logic if (![[NSFileManager defaultManager] fileExistsAtPath:normalizedPath]) { self->_normalizedPath = SUNormalizedInstallationPath(self->_oldHost); } } NSArray *runningApplications = [self runningApplicationsWithBundle:applicationBundle]; // We're just picking the first running application to send.. // Ideally we'd send them all and have the installer monitor all of them but I don't want to deal with that complexity at the moment // Although that would still have the issue if another instance of the application launched during that duration // Lastly we don't handle monitoring or terminating processes from logged in users NSRunningApplication *firstRunningApplication = runningApplications.firstObject; BOOL targetDead = (firstRunningApplication == nil || firstRunningApplication.terminated); if (reply != NULL) { reply(targetDead); } self->_repliedToRegistration = YES; self->_applicationBundle = applicationBundle; self->_applicationInitiallyAlive = !targetDead; self->_targetRunningApplication = firstRunningApplication; } else { SULog(SULogLevelError, @"Error: -registerApplicationBundlePath:reply: called in unexpected state"); } #pragma clang diagnostic pop }); } - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context { NSString *isTerminatedKeyPath = NSStringFromSelector(@selector(isTerminated)); if ([keyPath isEqualToString:isTerminatedKeyPath]) { if (_targetRunningApplication.terminated && _terminationCompletionHandler != nil) { _terminationCompletionHandler(); [_targetRunningApplication removeObserver:self forKeyPath:isTerminatedKeyPath]; _terminationCompletionHandler = nil; _targetRunningApplication = nil; } } else { [super observeValueForKeyPath:keyPath ofObject:object change:change context:context]; } } - (void)listenForTerminationWithCompletion:(void (^)(void))completionHandler { dispatch_async(dispatch_get_main_queue(), ^{ #pragma clang diagnostic push #if __has_warning("-Wcompletion-handler") #pragma clang diagnostic ignored "-Wcompletion-handler" #endif if (self->_targetRunningApplication != nil && self->_terminationCompletionHandler == nil) { if (self->_targetRunningApplication.terminated) { if (completionHandler != NULL) { completionHandler(); } } else { self->_terminationCompletionHandler = [completionHandler copy]; [self->_targetRunningApplication addObserver:self forKeyPath:NSStringFromSelector(@selector(isTerminated)) options:NSKeyValueObservingOptionNew context:NULL]; } } else { SULog(SULogLevelError, @"Error: -listenForTerminationWithCompletion: called in unexpected state"); } #pragma clang diagnostic pop }); } - (void)registerInstallationInfoData:(NSData *)installationInfoData { dispatch_async(dispatch_get_main_queue(), ^{ if (self->_statusInfo.installationInfoData == nil && installationInfoData != nil) { SPUInstallationInfo *installationInfo = (SPUInstallationInfo *)SPUUnarchiveRootObjectSecurely(installationInfoData, [SPUInstallationInfo class]); if (installationInfo != nil) { installationInfo.systemDomain = self->_systemDomain; self->_statusInfo.installationInfoData = SPUArchiveRootObjectSecurely(installationInfo); } else { SULog(SULogLevelError, @"Error: Failed to decode initial installation info from installer: %@", installationInfoData); } } }); } - (void)sendTerminationSignal { dispatch_async(dispatch_get_main_queue(), ^{ if (!self->_willTerminate && self->_applicationBundle != nil) { // Note we are sending an Apple quit event, which gives the application or user a chance to delay or cancel the request, which is what we desire for (NSRunningApplication *runningApplication in [self runningApplicationsWithBundle:self->_applicationBundle]) { [runningApplication terminate]; } } }); } - (void)relaunchApplication { dispatch_async(dispatch_get_main_queue(), ^{ if (!self->_willTerminate && self->_applicationBundle != nil && self->_applicationInitiallyAlive) { NSString *pathToRelaunch; if (self->_normalizedPath != nil) { pathToRelaunch = self->_normalizedPath; } else if (self->_shouldRelaunchHostBundle) { // Use self->_oldHostBundlePath because it was computed before self->_oldHost could have been removed pathToRelaunch = self->_oldHostBundlePath; } else { pathToRelaunch = self->_applicationBundle.bundlePath; } // We should at least make sure we're opening a bundle NSBundle *relaunchBundle = [NSBundle bundleWithPath:pathToRelaunch]; if (relaunchBundle == nil) { [self cleanupAndExitWithStatus:EXIT_FAILURE error:[NSError errorWithDomain:SUSparkleErrorDomain code:SUAgentInvalidationError userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Error: Encountered invalid path to relaunch: %@", pathToRelaunch] }]]; } // Note: we can launch application bundles or open plug-in bundles if (![[NSWorkspace sharedWorkspace] openURL:[NSURL fileURLWithPath:pathToRelaunch isDirectory:YES]]) { SULog(SULogLevelError, @"Error: Failed to relaunch bundle at %@", pathToRelaunch); } // Delay termination for a little bit to better increase the chance the updated application when relaunched will be the frontmost application // This is related to macOS activation issues when terminating a frontmost application happens right before launching another app self->_willTerminate = YES; dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(SUTerminationTimeDelay * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ [self cleanupAndExitWithStatus:EXIT_SUCCESS error:nil]; }); } }); } - (void)showProgress { dispatch_async(dispatch_get_main_queue(), ^{ if (!self->_willTerminate) { // Show app icon in the dock ProcessSerialNumber psn = { 0, kCurrentProcess }; TransformProcessType(&psn, kProcessTransformToForegroundApplication); // Note: the application icon needs to be set after showing the icon in the dock self->_application.applicationIconImage = [SUApplicationInfo bestIconForHost:self->_oldHost]; // Activate ourselves otherwise we will probably be in the background if (@available(macOS 14, *)) { [self->_application activate]; } else { [self->_application activateIgnoringOtherApps:YES]; } [self->_delegate installerProgressShouldDisplayWithHost:self->_oldHost]; } }); } - (void)stopProgress { dispatch_async(dispatch_get_main_queue(), ^{ // Dismiss any UI immediately [self->_delegate installerProgressShouldStop]; self->_delegate = nil; // No need to broadcast status service anymore // In fact we shouldn't when we decide to relaunch the update [self->_statusInfo invalidate]; self->_statusInfo = nil; }); } @end ================================================ FILE: Sparkle/InstallerProgress/InstallerProgressDelegate.h ================================================ // // InstallerProgressDelegate.h // Sparkle // // Created by Mayur Pawashe on 4/10/16. // Copyright © 2016 Sparkle Project. All rights reserved. // #import NS_ASSUME_NONNULL_BEGIN @class SUHost; @protocol InstallerProgressDelegate - (void)loadLocalizationStringsFromHost:(SUHost *)host; - (void)installerProgressShouldDisplayWithHost:(SUHost *)host; - (void)installerProgressShouldStop; @end NS_ASSUME_NONNULL_END ================================================ FILE: Sparkle/InstallerProgress/SPUInstallerAgentProtocol.h ================================================ // // SPUInstallerAgentProtocol.h // Sparkle // // Created by Mayur Pawashe on 7/17/16. // Copyright © 2016 Sparkle Project. All rights reserved. // #import NS_ASSUME_NONNULL_BEGIN @protocol SPUInstallerAgentProtocol - (void)registerApplicationBundlePath:(NSString *)applicationBundlePath reply:(void (^)(BOOL))reply; - (void)registerInstallationInfoData:(NSData *)installationInfoData; - (void)listenForTerminationWithCompletion:(void (^)(void))completionHandler; - (void)sendTerminationSignal; - (void)showProgress; - (void)stopProgress; - (void)relaunchApplication; @end NS_ASSUME_NONNULL_END ================================================ FILE: Sparkle/InstallerProgress/SUInstallerAgentInitiationProtocol.h ================================================ // // SUInstallerAgentInitiationProtocol.h // Sparkle // // Created by Mayur Pawashe on 7/17/16. // Copyright © 2016 Sparkle Project. All rights reserved. // #import @protocol SUInstallerAgentInitiationProtocol - (void)connectionDidInitiateWithReply:(void (^)(void))acknowledgement; - (void)connectionWillInvalidateWithError:(NSError *)error; @end ================================================ FILE: Sparkle/InstallerProgress/ShowInstallerProgress.h ================================================ // // ShowInstallerProgress.h // Sparkle // // Created by Mayur Pawashe on 4/10/16. // Copyright © 2016 Sparkle Project. All rights reserved. // #import #import "InstallerProgressDelegate.h" NS_ASSUME_NONNULL_BEGIN @interface ShowInstallerProgress : NSObject @end NS_ASSUME_NONNULL_END ================================================ FILE: Sparkle/InstallerProgress/ShowInstallerProgress.m ================================================ // // ShowInstallerProgress.m // Installer Progress // // Created by Mayur Pawashe on 4/7/16. // Copyright © 2016 Sparkle Project. All rights reserved. // #import #import "ShowInstallerProgress.h" #import "SUStatusController.h" #import "SUHost.h" #import "SULocalizations.h" @implementation ShowInstallerProgress { SUStatusController *_statusController; NSString *_updatingString; NSString *_cancelUpdateTitle; NSString *_installingUpdateTitle; } - (void)loadLocalizationStringsFromHost:(SUHost *)host { // Try to retrieve localization strings from the old bundle if possible // We won't display these strings until installerProgressShouldDisplayWithHost: // (which will be after the update is trusted) // If we fail to load localizations in any way, we default to English #if SPARKLE_COPY_LOCALIZATIONS NSBundle *hostSparkleBundle; { NSURL *hostSparkleURL = [host.bundle.privateFrameworksURL URLByAppendingPathComponent:@"Sparkle.framework" isDirectory:YES]; if (hostSparkleURL == nil) { hostSparkleBundle = nil; } else { hostSparkleBundle = [NSBundle bundleWithURL:hostSparkleURL]; } } #endif NSString *updatingString; { NSString *hostNameFromBundle = host.name; NSString *hostName = (hostNameFromBundle != nil) ? hostNameFromBundle : @""; #if SPARKLE_COPY_LOCALIZATIONS { NSString *updatingFormatStringFromBundle = (hostSparkleBundle != nil) ? SULocalizedStringFromTableInBundle(@"Updating %@", @"Sparkle", hostSparkleBundle, nil) : nil; if (updatingFormatStringFromBundle != nil) { // Replacing the %@ will be a bit safer than using +[NSString stringWithFormat:] updatingString = [updatingFormatStringFromBundle stringByReplacingOccurrencesOfString:@"%@" withString:hostName]; } else { updatingString = [@"Updating " stringByAppendingString:hostName]; } } #else { updatingString = [@"Updating " stringByAppendingString:hostName]; } #endif } _updatingString = updatingString; NSString *cancelUpdateTitle; #if SPARKLE_COPY_LOCALIZATIONS { NSString *cancelUpdateTitleFromBundle = (hostSparkleBundle != nil) ? SULocalizedStringFromTableInBundle(@"Cancel Update", @"Sparkle", hostSparkleBundle, @"") : nil; cancelUpdateTitle = (cancelUpdateTitleFromBundle != nil) ? cancelUpdateTitleFromBundle : @"Cancel Update"; } #else { cancelUpdateTitle = @"Cancel Update"; } #endif _cancelUpdateTitle = cancelUpdateTitle; NSString *installingUpdateTitle; #if SPARKLE_COPY_LOCALIZATIONS { NSString *installingUpdateTitleFromBundle = (hostSparkleBundle != nil) ? SULocalizedStringFromTableInBundle(@"Installing update…", @"Sparkle", hostSparkleBundle, @"") : nil; installingUpdateTitle = (installingUpdateTitleFromBundle != nil) ? installingUpdateTitleFromBundle : @"Installing update…"; } #else { installingUpdateTitle = @"Installing update…"; } #endif _installingUpdateTitle = installingUpdateTitle; } - (void)installerProgressShouldDisplayWithHost:(SUHost *)host { _statusController = [[SUStatusController alloc] initWithHost:host windowTitle:_updatingString centerPointValue:nil minimizable:NO closable:NO]; [_statusController setButtonTitle:_cancelUpdateTitle target:nil action:nil isDefault:NO accessibilityIdentifier:@"SUStatusCancel"]; [_statusController beginActionWithTitle:_installingUpdateTitle maxProgressValue:0 statusText:@""]; [_statusController showWindow:self]; } - (void)installerProgressShouldStop { [_statusController close]; _statusController = nil; } @end ================================================ FILE: Sparkle/InstallerProgress/main.m ================================================ // // main.m // Sparkle // // Created by Mayur Pawashe on 4/10/16. // Copyright © 2016 Sparkle Project. All rights reserved. // #import #import "InstallerProgressAppController.h" #import "ShowInstallerProgress.h" int main(int __unused argc, const char __unused *argv[]) { @autoreleasepool { id showInstallerProgress = [[ShowInstallerProgress alloc] init]; InstallerProgressAppController *appController = [[InstallerProgressAppController alloc] initWithApplication:[NSApplication sharedApplication] arguments:[[NSProcessInfo processInfo] arguments] delegate:showInstallerProgress]; // Ignore SIGTERM because we are going to catch it ourselves signal(SIGTERM, SIG_IGN); dispatch_source_t sigtermSource = dispatch_source_create(DISPATCH_SOURCE_TYPE_SIGNAL, SIGTERM, 0, dispatch_get_main_queue()); dispatch_source_set_event_handler(sigtermSource, ^{ [appController cleanupAndExitWithStatus:SIGTERM error:nil]; }); dispatch_resume(sigtermSource); [appController run]; } return EXIT_SUCCESS; } ================================================ FILE: Sparkle/SPUAppcastItemState.h ================================================ // // SPUAppcastItemState.h // Sparkle // // Created by Mayur Pawashe on 5/31/21. // Copyright © 2021 Sparkle Project. All rights reserved. // #import NS_ASSUME_NONNULL_BEGIN // Appcast Item state that contains properties that depends on a host SPU_OBJC_DIRECT_MEMBERS @interface SPUAppcastItemState : NSObject - (instancetype)init NS_UNAVAILABLE; - (instancetype)initWithMajorUpgrade:(BOOL)majorUpgrade criticalUpdate:(BOOL)criticalUpdate informationalUpdate:(BOOL)informationalUpdate minimumUpdateVersionIsOK:(BOOL)minimumUpdateVersionIsOK minimumOperatingSystemVersionIsOK:(BOOL)minimumOperatingSystemVersionIsOK maximumOperatingSystemVersionIsOK:(BOOL)maximumOperatingSystemVersionIsOK arm64HardwareRequirementIsOK:(BOOL)arm64HardwareRequirementIsOK; @property (nonatomic, readonly) BOOL majorUpgrade; @property (nonatomic, readonly) BOOL criticalUpdate; @property (nonatomic, readonly) BOOL informationalUpdate; @property (nonatomic, readonly) BOOL minimumUpdateVersionIsOK; @property (nonatomic, readonly) BOOL minimumOperatingSystemVersionIsOK; @property (nonatomic, readonly) BOOL maximumOperatingSystemVersionIsOK; @property (nonatomic, readonly) BOOL arm64HardwareRequirementIsOK; @end NS_ASSUME_NONNULL_END ================================================ FILE: Sparkle/SPUAppcastItemState.m ================================================ // // SPUAppcastItemState.m // Sparkle // // Created by Mayur Pawashe on 5/31/21. // Copyright © 2021 Sparkle Project. All rights reserved. // #import "SPUAppcastItemState.h" #include "AppKitPrevention.h" #define SPUAppcastItemStateMajorUpgradeKey @"SPUAppcastItemStateMajorUpgrade" #define SPUAppcastItemStateCriticalUpdateKey @"SPUAppcastItemStateCriticalUpdate" #define SPUAppcastItemStateInformationalUpdateKey @"SPUAppcastItemStateInformationalUpdate" #define SPUAppcastItemStateUpdateMinimumVersionIsOKKey @"SPUAppcastItemStateMinimumUpdateVersionIsOK" #define SPUAppcastItemStateMinimumOperatingSystemVersionIsOKKey @"SPUAppcastItemStateMinimumOperatingSystemVersionIsOK" #define SPUAppcastItemStateMaximumOperatingSystemVersionIsOKKey @"SPUAppcastItemStateMaximumOperatingSystemVersionIsOK" #define SPUAppcastItemStateArm64HardwareRequirementIsOKKey @"SPUAppcastItemStateArm64HardwareRequirementIsOK" @interface SPUAppcastItemState () @end @implementation SPUAppcastItemState @synthesize majorUpgrade = _majorUpgrade; @synthesize criticalUpdate = _criticalUpdate; @synthesize informationalUpdate = _informationalUpdate; @synthesize minimumUpdateVersionIsOK = _minimumUpdateVersionIsOK; @synthesize minimumOperatingSystemVersionIsOK = _minimumOperatingSystemVersionIsOK; @synthesize maximumOperatingSystemVersionIsOK = _maximumOperatingSystemVersionIsOK; @synthesize arm64HardwareRequirementIsOK = _arm64HardwareRequirementIsOK; - (instancetype)initWithMajorUpgrade:(BOOL)majorUpgrade criticalUpdate:(BOOL)criticalUpdate informationalUpdate:(BOOL)informationalUpdate minimumUpdateVersionIsOK:(BOOL)minimumUpdateVersionIsOK minimumOperatingSystemVersionIsOK:(BOOL)minimumOperatingSystemVersionIsOK maximumOperatingSystemVersionIsOK:(BOOL)maximumOperatingSystemVersionIsOK arm64HardwareRequirementIsOK:(BOOL)arm64HardwareRequirementIsOK { self = [super init]; if (self != nil) { _majorUpgrade = majorUpgrade; _criticalUpdate = criticalUpdate; _informationalUpdate = informationalUpdate; _minimumUpdateVersionIsOK = minimumUpdateVersionIsOK; _minimumOperatingSystemVersionIsOK = minimumOperatingSystemVersionIsOK; _maximumOperatingSystemVersionIsOK = maximumOperatingSystemVersionIsOK; _arm64HardwareRequirementIsOK = arm64HardwareRequirementIsOK; } return self; } + (BOOL)supportsSecureCoding { return YES; } - (void)encodeWithCoder:(NSCoder *)encoder { [encoder encodeBool:_majorUpgrade forKey:SPUAppcastItemStateMajorUpgradeKey]; [encoder encodeBool:_criticalUpdate forKey:SPUAppcastItemStateCriticalUpdateKey]; [encoder encodeBool:_informationalUpdate forKey:SPUAppcastItemStateInformationalUpdateKey]; [encoder encodeBool:_minimumUpdateVersionIsOK forKey:SPUAppcastItemStateUpdateMinimumVersionIsOKKey]; [encoder encodeBool:_minimumOperatingSystemVersionIsOK forKey:SPUAppcastItemStateMinimumOperatingSystemVersionIsOKKey]; [encoder encodeBool:_maximumOperatingSystemVersionIsOK forKey:SPUAppcastItemStateMaximumOperatingSystemVersionIsOKKey]; [encoder encodeBool:_arm64HardwareRequirementIsOK forKey:SPUAppcastItemStateArm64HardwareRequirementIsOKKey]; } - (instancetype)initWithCoder:(NSCoder *)decoder { BOOL majorUpgrade = [decoder decodeBoolForKey:SPUAppcastItemStateMajorUpgradeKey]; BOOL criticalUpdate = [decoder decodeBoolForKey:SPUAppcastItemStateCriticalUpdateKey]; BOOL informationalUpdate = [decoder decodeBoolForKey:SPUAppcastItemStateInformationalUpdateKey]; BOOL minimumUpdateVersionIsOK = [decoder decodeBoolForKey:SPUAppcastItemStateUpdateMinimumVersionIsOKKey]; BOOL minimumOperatingSystemVersionIsOK = [decoder decodeBoolForKey:SPUAppcastItemStateMinimumOperatingSystemVersionIsOKKey]; BOOL maximumOperatingSystemVersionIsOK = [decoder decodeBoolForKey:SPUAppcastItemStateMaximumOperatingSystemVersionIsOKKey]; BOOL arm64HardwareRequirementIsOK = [decoder decodeBoolForKey:SPUAppcastItemStateArm64HardwareRequirementIsOKKey]; return [self initWithMajorUpgrade:majorUpgrade criticalUpdate:criticalUpdate informationalUpdate:informationalUpdate minimumUpdateVersionIsOK:minimumUpdateVersionIsOK minimumOperatingSystemVersionIsOK:minimumOperatingSystemVersionIsOK maximumOperatingSystemVersionIsOK:maximumOperatingSystemVersionIsOK arm64HardwareRequirementIsOK:arm64HardwareRequirementIsOK]; } @end ================================================ FILE: Sparkle/SPUAppcastItemStateResolver+Private.h ================================================ // // SPUAppcastItemStateResolver+Private.h // Sparkle // // Created by Mayur Pawashe on 6/20/21. // Copyright © 2021 Sparkle Project. All rights reserved. // #ifndef SPUAppcastItemStateResolver_Private_h #define SPUAppcastItemStateResolver_Private_h NS_ASSUME_NONNULL_BEGIN @interface SPUAppcastItemStateResolver () - (SPUAppcastItemState *)resolveStateWithInformationalUpdateVersions:(NSSet * _Nullable)informationalUpdateVersions minimumUpdateVersion:(NSString * _Nullable)minimumUpdateVersion minimumOperatingSystemVersion:(NSString * _Nullable)minimumOperatingSystemVersion maximumOperatingSystemVersion:(NSString * _Nullable)maximumOperatingSystemVersion minimumAutoupdateVersion:(NSString * _Nullable)minimumAutoupdateVersion criticalUpdateDictionary:(NSDictionary * _Nullable)criticalUpdateDictionary hardwareRequirements:(NSSet *)hardwareRequirements; + (BOOL)isMinimumAutoupdateVersionOK:(NSString * _Nullable)minimumAutoupdateVersion hostVersion:(NSString *)hostVersion versionComparator:(id)versionComparator; @end NS_ASSUME_NONNULL_END #endif /* SPUAppcastItemStateResolver_Private_h */ ================================================ FILE: Sparkle/SPUAppcastItemStateResolver.h ================================================ // // SPUAppcastItemStateResolver.h // Sparkle // // Created by Mayur Pawashe on 5/31/21. // Copyright © 2021 Sparkle Project. All rights reserved. // #import #if defined(BUILDING_SPARKLE_SOURCES_EXTERNALLY) // Ignore incorrect warning #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wquoted-include-in-framework-header" #import "SUExport.h" #pragma clang diagnostic pop #else #import #endif NS_ASSUME_NONNULL_BEGIN @class SUStandardVersionComparator, SPUAppcastItemState; @protocol SUVersionComparison; /** Private exposed class used to resolve Appcast Item properties that rely on external factors such as a host. This resolver is used for constructing appcast items. */ SU_EXPORT @interface SPUAppcastItemStateResolver : NSObject - (instancetype)init NS_UNAVAILABLE; - (instancetype)initWithHostVersion:(NSString *)hostVersion applicationVersionComparator:(id)applicationVersionComparator standardVersionComparator:(SUStandardVersionComparator *)standardVersionComparator; @end NS_ASSUME_NONNULL_END ================================================ FILE: Sparkle/SPUAppcastItemStateResolver.m ================================================ // // SPUAppcastItemStateResolver.m // Sparkle // // Created by Mayur Pawashe on 5/31/21. // Copyright © 2021 Sparkle Project. All rights reserved. // #import "SPUAppcastItemStateResolver.h" #import "SPUAppcastItemStateResolver+Private.h" #import "SPUAppcastItemState.h" #import "SUVersionComparisonProtocol.h" #import "SUStandardVersionComparator.h" #import "SUConstants.h" #import "SUOperatingSystem.h" #import "SULog.h" #import #import #import #include "AppKitPrevention.h" @implementation SPUAppcastItemStateResolver { NSString *_hostVersion; id _applicationVersionComparator; SUStandardVersionComparator *_standardVersionComparator; } - (instancetype)initWithHostVersion:(NSString *)hostVersion applicationVersionComparator:(id)applicationVersionComparator standardVersionComparator:(SUStandardVersionComparator *)standardVersionComparator { self = [super init]; if (self != nil) { _hostVersion = [hostVersion copy]; _applicationVersionComparator = applicationVersionComparator; _standardVersionComparator = standardVersionComparator; } return self; } - (BOOL)isMinimumUpdateVersionOK:(NSString * _Nullable)minimumUpdateVersion SPU_OBJC_DIRECT { NSString *hostVersion = _hostVersion; BOOL minimumVersionOK = YES; if (minimumUpdateVersion != nil && ![minimumUpdateVersion isEqualToString:@""]) { minimumVersionOK = [_applicationVersionComparator compareVersion:(NSString * _Nonnull)minimumUpdateVersion toVersion:hostVersion] != NSOrderedDescending; } return minimumVersionOK; } - (BOOL)isMinimumOperatingSystemVersionOK:(NSString * _Nullable)minimumSystemVersion SPU_OBJC_DIRECT { BOOL minimumVersionOK = YES; if (minimumSystemVersion != nil && ![minimumSystemVersion isEqualToString:@""]) { minimumVersionOK = [_standardVersionComparator compareVersion:(NSString * _Nonnull)minimumSystemVersion toVersion:[SUOperatingSystem systemVersionString]] != NSOrderedDescending; } return minimumVersionOK; } - (BOOL)isMaximumOperatingSystemVersionOK:(NSString * _Nullable)maximumSystemVersion SPU_OBJC_DIRECT { BOOL maximumVersionOK = YES; if (maximumSystemVersion != nil && ![maximumSystemVersion isEqualToString:@""]) { maximumVersionOK = [_standardVersionComparator compareVersion:(NSString * _Nonnull)maximumSystemVersion toVersion:[SUOperatingSystem systemVersionString]] != NSOrderedAscending; } return maximumVersionOK; } + (BOOL)isMinimumAutoupdateVersionOK:(NSString * _Nullable)minimumAutoupdateVersion hostVersion:(NSString *)hostVersion versionComparator:(id)versionComparator { return (minimumAutoupdateVersion.length == 0 || ([versionComparator compareVersion:hostVersion toVersion:(NSString * _Nonnull)minimumAutoupdateVersion] != NSOrderedAscending)); } - (BOOL)isMinimumAutoupdateVersionOK:(NSString * _Nullable)minimumAutoupdateVersion SPU_OBJC_DIRECT { return [[self class] isMinimumAutoupdateVersionOK:minimumAutoupdateVersion hostVersion:_hostVersion versionComparator:_applicationVersionComparator]; } - (BOOL)isCriticalUpdateWithCriticalUpdateDictionary:(NSDictionary * _Nullable)criticalUpdateDictionary SPU_OBJC_DIRECT { // Check if any critical update info is provided if (criticalUpdateDictionary == nil) { return NO; } // If no critical version is supplied, then it is critical NSString *criticalVersion = criticalUpdateDictionary[SUAppcastAttributeVersion]; if (criticalVersion == nil || ![criticalVersion isKindOfClass:[NSString class]]) { return YES; } // Update is only critical when coming from previous versions return ([_applicationVersionComparator compareVersion:_hostVersion toVersion:criticalVersion] == NSOrderedAscending); } - (BOOL)isInformationalUpdateWithInformationalUpdateVersions:(NSSet * _Nullable)informationalUpdateVersions SPU_OBJC_DIRECT { if (informationalUpdateVersions == nil) { return NO; } // Informational only update regardless of version the app is updating from if (informationalUpdateVersions.count == 0) { return YES; } NSString *hostVersion = _hostVersion; // Informational update only for a set of host versions we're updating from if ([informationalUpdateVersions containsObject:hostVersion]) { return YES; } // If an informational update version has a '<' prefix, this is an informational update if // hostVersion < this info update version for (NSString *informationalUpdateVersion in informationalUpdateVersions) { if ([informationalUpdateVersion hasPrefix:@"<"] && [_applicationVersionComparator compareVersion:hostVersion toVersion:[informationalUpdateVersion substringFromIndex:1]] == NSOrderedAscending) { return YES; } } return NO; } - (BOOL)isArm64HardwareRequirementOK:(NSSet *)hardwareRequirements minimumSystemVersion:(NSString *_Nullable )minimumSystemVersion SPU_OBJC_DIRECT { #if TARGET_CPU_X86_64 // macOS 27+ will no longer support Intel Macs BOOL hasARM64Requirement; if (minimumSystemVersion.length > 0 && [_standardVersionComparator compareVersion:(NSString * _Nonnull)minimumSystemVersion toVersion:@"27.0"] != NSOrderedAscending) { hasARM64Requirement = YES; } else { hasARM64Requirement = [hardwareRequirements containsObject:SUAppcastElementHardwareRequirementARM64]; } if (!hasARM64Requirement) { return YES; } // If the process is run under Rosetta, then the hardware is compatible // https://developer.apple.com/documentation/apple-silicon/about-the-rosetta-translation-environment int translatedResult = 0; size_t translatedResultSize = sizeof(translatedResult); if (sysctlbyname("sysctl.proc_translated", &translatedResult, &translatedResultSize, NULL, 0) == -1) { if (errno == ENOENT) { // Native x86_64 process return NO; } // An error occured SULog(SULogLevelError, @"Error: failed to detect if process is running under rosetta with error: %d", errno); return YES; } return (translatedResult == 1); #else return YES; #endif } - (SPUAppcastItemState *)resolveStateWithInformationalUpdateVersions:(NSSet * _Nullable)informationalUpdateVersions minimumUpdateVersion:(NSString * _Nullable)minimumUpdateVersion minimumOperatingSystemVersion:(NSString * _Nullable)minimumOperatingSystemVersion maximumOperatingSystemVersion:(NSString * _Nullable)maximumOperatingSystemVersion minimumAutoupdateVersion:(NSString * _Nullable)minimumAutoupdateVersion criticalUpdateDictionary:(NSDictionary * _Nullable)criticalUpdateDictionary hardwareRequirements:(NSSet *)hardwareRequirements { BOOL informationalUpdate = [self isInformationalUpdateWithInformationalUpdateVersions:informationalUpdateVersions]; BOOL minimumUpdateVersionIsOK = [self isMinimumUpdateVersionOK:minimumUpdateVersion]; BOOL minimumOperatingSystemVersionIsOK = [self isMinimumOperatingSystemVersionOK:minimumOperatingSystemVersion]; BOOL maximumOperatingSystemVersionIsOK = [self isMaximumOperatingSystemVersionOK:maximumOperatingSystemVersion]; BOOL majorUpgrade = ![self isMinimumAutoupdateVersionOK:minimumAutoupdateVersion]; BOOL criticalUpdate = [self isCriticalUpdateWithCriticalUpdateDictionary:criticalUpdateDictionary]; BOOL arm64HardwareRequirementIsOK = [self isArm64HardwareRequirementOK:hardwareRequirements minimumSystemVersion:minimumOperatingSystemVersion]; return [[SPUAppcastItemState alloc] initWithMajorUpgrade:majorUpgrade criticalUpdate:criticalUpdate informationalUpdate:informationalUpdate minimumUpdateVersionIsOK:minimumUpdateVersionIsOK minimumOperatingSystemVersionIsOK:minimumOperatingSystemVersionIsOK maximumOperatingSystemVersionIsOK:maximumOperatingSystemVersionIsOK arm64HardwareRequirementIsOK:arm64HardwareRequirementIsOK]; } @end ================================================ FILE: Sparkle/SPUAppcastSigningValidationStatus.h ================================================ // // SPUAppcastSigningValidationStatus.h // Sparkle // // Created on 12/30/25. // Copyright © 2025 Sparkle Project. All rights reserved. // #ifndef SPUAppcastSigningValidationStatus_h #define SPUAppcastSigningValidationStatus_h typedef NS_ENUM(NSInteger, SPUAppcastSigningValidationStatus) { /** The bundle does not opt into requiring appcast signing and no validation of the appcast feed is done. */ SPUAppcastSigningValidationStatusSkipped = 0, /** The appcast is signed and validation has passed succesfully. */ SPUAppcastSigningValidationStatusSucceeded, /** The appcast is signed and validation has failed. In this case, appcast items operate in a 'safe' fallback mode meaning that they cannot be marked as a critical update, cannot be marked as informational update, and will not have any release note or link references. */ SPUAppcastSigningValidationStatusFailed, }; #endif /* SPUAppcastSigningValidationStatus_h */ ================================================ FILE: Sparkle/SPUAutomaticUpdateDriver.h ================================================ // // SPUAutomaticUpdateDriver.h // Sparkle // // Created by Mayur Pawashe on 3/18/16. // Copyright © 2016 Sparkle Project. All rights reserved. // #import #import "SPUUpdateDriver.h" NS_ASSUME_NONNULL_BEGIN @class SUHost; @protocol SPUUpdaterDelegate, SPUUserDriver; SPU_OBJC_DIRECT_MEMBERS @interface SPUAutomaticUpdateDriver : NSObject - (instancetype)initWithHost:(SUHost *)host applicationBundle:(NSBundle *)applicationBundle updater:(id)updater userDriver:(id )userDriver updaterDelegate:(nullable id )updaterDelegate; @end NS_ASSUME_NONNULL_END ================================================ FILE: Sparkle/SPUAutomaticUpdateDriver.m ================================================ // // SPUAutomaticUpdateDriver.m // Sparkle // // Created by Mayur Pawashe on 3/18/16. // Copyright © 2016 Sparkle Project. All rights reserved. // #import "SPUAutomaticUpdateDriver.h" #import "SPUUpdateDriver.h" #import "SUHost.h" #import "SPUUpdaterDelegate.h" #import "SPUCoreBasedUpdateDriver.h" #import "SULog.h" #import "SUAppcastItem.h" #import "SPUUserDriver.h" #import "SUErrors.h" #include "AppKitPrevention.h" @interface SPUAutomaticUpdateDriver () @end @implementation SPUAutomaticUpdateDriver { SPUCoreBasedUpdateDriver *_coreDriver; SUAppcastItem* _updateItem; __weak id _updater; __weak id _userDriver; __weak id _updaterDelegate; BOOL _installerDidFinishPreparation; } - (instancetype)initWithHost:(SUHost *)host applicationBundle:(NSBundle *)applicationBundle updater:(id)updater userDriver:(id )userDriver updaterDelegate:(nullable id )updaterDelegate { self = [super init]; if (self != nil) { _updater = updater; // The user driver is only used for a termination callback _userDriver = userDriver; _updaterDelegate = updaterDelegate; _coreDriver = [[SPUCoreBasedUpdateDriver alloc] initWithHost:host applicationBundle:applicationBundle updateCheck:SPUUpdateCheckUpdatesInBackground updater:updater updaterDelegate:updaterDelegate delegate:self]; } return self; } - (void)setCompletionHandler:(SPUUpdateDriverCompletion)completionBlock { [_coreDriver setCompletionHandler:completionBlock]; } - (void)setUpdateShownHandler:(void (^)(void))updateShownHandler { } - (void)setUpdateWillInstallHandler:(void (^)(void))updateWillInstallHandler { [_coreDriver setUpdateWillInstallHandler:updateWillInstallHandler]; } - (void)checkForUpdatesAtAppcastURL:(NSURL *)appcastURL withUserAgent:(NSString *)userAgent httpHeaders:(NSDictionary * _Nullable)httpHeaders { [_coreDriver checkForUpdatesAtAppcastURL:appcastURL withUserAgent:userAgent httpHeaders:httpHeaders inBackground:YES requiresSilentInstall:YES]; } - (void)resumeInstallingUpdate { // Nothing really to do here.. this shouldn't be called. SULog(SULogLevelError, @"Error: resumeInstallingUpdate: called on SPUAutomaticUpdateDriver"); } - (void)resumeUpdate:(id)__unused resumableUpdate { // Nothing really to do here.. this shouldn't be called. SULog(SULogLevelError, @"Error: resumeDownloadedUpdate: called on SPUAutomaticUpdateDriver"); } // Note: critical updates can be downloaded automatically first before needing user attention static BOOL SPUUpdateRequiresUserAttentionBeforeDownloading(SUAppcastItem *updateItem) { return (updateItem.isInformationOnlyUpdate || updateItem.majorUpgrade || updateItem.signingValidationStatus == SPUAppcastSigningValidationStatusFailed); } - (void)basicDriverDidFindUpdateWithAppcastItem:(SUAppcastItem *)updateItem secondaryAppcastItem:(SUAppcastItem * _Nullable)secondaryUpdateItem { _updateItem = updateItem; if (SPUUpdateRequiresUserAttentionBeforeDownloading(updateItem)) { [_coreDriver deferInformationalUpdate:updateItem secondaryUpdate:secondaryUpdateItem]; [self abortUpdate]; } else { [_coreDriver downloadUpdateFromAppcastItem:updateItem secondaryAppcastItem:secondaryUpdateItem inBackground:YES]; } } - (BOOL)showingUpdate { return NO; } - (void)installerDidFinishPreparationAndWillInstallImmediately:(BOOL)willInstallImmediately { _installerDidFinishPreparation = YES; if (!willInstallImmediately) { BOOL installationHandledByDelegate = NO; id updaterDelegate = _updaterDelegate; if ([updaterDelegate respondsToSelector:@selector(updater:willInstallUpdateOnQuit:immediateInstallationBlock:)]) { __weak __typeof__(self) weakSelf = self; installationHandledByDelegate = [updaterDelegate updater:_updater willInstallUpdateOnQuit:_updateItem immediateInstallationBlock:^{ dispatch_async(dispatch_get_main_queue(), ^{ __typeof__(self) strongSelf = weakSelf; if (strongSelf != nil) { [strongSelf->_coreDriver finishInstallationWithResponse:SPUUserUpdateChoiceInstall displayingUserInterface:NO]; } }); }]; } if (!installationHandledByDelegate) { // We are done and can safely abort now // The installer tool will keep the installation alive [self abortUpdate]; } } } - (void)basicDriverIsRequestingAbortUpdateWithError:(NSError *)error { [self abortUpdateWithError:error]; } - (void)coreDriverIsRequestingAbortUpdateWithError:(NSError *)error { [self abortUpdateWithError:error]; } - (void)abortUpdate { [self abortUpdateWithError:nil]; } - (void)abortUpdateWithError:(NSError *)error { // It should not be necessary to include properties from SPUUpdateRequiresUserAttentionBeforeDownloading() because _installerDidFinishPreparation should be NO in those cases, // but we'll include the check anyway BOOL showNextUpdateImmediately = (error == nil || error.code == SUInstallationAuthorizeLaterError) && (!_installerDidFinishPreparation || _updateItem.criticalUpdate || SPUUpdateRequiresUserAttentionBeforeDownloading(_updateItem)); [_coreDriver abortUpdateAndShowNextUpdateImmediately:showNextUpdateImmediately error:error]; } @end ================================================ FILE: Sparkle/SPUBasicUpdateDriver.h ================================================ // // SPUBasicUpdateDriver.h // Sparkle // // Created by Mayur Pawashe on 3/18/16. // Copyright © 2016 Sparkle Project. All rights reserved. // #import #import "SPUUpdateDriver.h" #import "SPUUpdateCheck.h" NS_ASSUME_NONNULL_BEGIN @class SUHost, SUAppcastItem; @protocol SPUUpdaterDelegate; @protocol SPUBasicUpdateDriverDelegate - (void)basicDriverDidFindUpdateWithAppcastItem:(SUAppcastItem *)appcastItem secondaryAppcastItem:(SUAppcastItem * _Nullable)secondaryAppcastItem systemDomain:(NSNumber * _Nullable)systemDomain; - (void)basicDriverIsRequestingAbortUpdateWithError:(nullable NSError *)error; @optional - (void)basicDriverDidFinishLoadingAppcast; @end SPU_OBJC_DIRECT_MEMBERS @interface SPUBasicUpdateDriver : NSObject - (instancetype)initWithHost:(SUHost *)host updateCheck:(SPUUpdateCheck)updateCheck updater:(id)updater updaterDelegate:(nullable id )updaterDelegate delegate:(id )delegate; - (void)setCompletionHandler:(SPUUpdateDriverCompletion)completionBlock; - (void)checkForUpdatesAtAppcastURL:(NSURL *)appcastURL withUserAgent:(NSString *)userAgent httpHeaders:(NSDictionary * _Nullable)httpHeaders inBackground:(BOOL)background; - (void)resumeInstallingUpdate; - (void)resumeUpdate:(id)resumableUpdate; - (void)abortUpdateAndShowNextUpdateImmediately:(BOOL)shouldSignalShowingUpdate resumableUpdate:(id _Nullable)resumableUpdate error:(nullable NSError *)error; @end NS_ASSUME_NONNULL_END ================================================ FILE: Sparkle/SPUBasicUpdateDriver.m ================================================ // // SPUBasicUpdateDriver.m // Sparkle // // Created by Mayur Pawashe on 3/18/16. // Copyright © 2016 Sparkle Project. All rights reserved. // #import "SPUBasicUpdateDriver.h" #import "SUAppcastDriver.h" #import "SPUUpdaterDelegate.h" #import "SUErrors.h" #import "SULocalizations.h" #import "SUHost.h" #import "SUAppcastItem.h" #import "SPUProbeInstallStatus.h" #import "SPUInstallationInfo.h" #import "SPUResumableUpdate.h" #import "SPUAppcastItemState.h" #import "SUAppcastItem+Private.h" #import "SPUInstallationType.h" #import "SUVersionDisplayProtocol.h" #import "SPUStandardVersionDisplay.h" #import "SPUNoUpdateFoundInfo.h" #include "AppKitPrevention.h" @interface SPUBasicUpdateDriver () @end @implementation SPUBasicUpdateDriver { SUAppcastDriver *_appcastDriver; SUHost *_host; SPUUpdateDriverCompletion _completionBlock; SPUUpdateCheck _updateCheck; __weak id _updater; __weak id _updaterDelegate; __weak id _delegate; BOOL _aborted; } - (instancetype)initWithHost:(SUHost *)host updateCheck:(SPUUpdateCheck)updateCheck updater:(id)updater updaterDelegate:(id )updaterDelegate delegate:(id )delegate { self = [super init]; if (self != nil) { _host = host; _updateCheck = updateCheck; _updater = updater; _updaterDelegate = updaterDelegate; _delegate = delegate; _appcastDriver = [[SUAppcastDriver alloc] initWithHost:host updater:updater updaterDelegate:updaterDelegate delegate:self]; } return self; } - (void)setCompletionHandler:(SPUUpdateDriverCompletion)completionBlock { _completionBlock = [completionBlock copy]; } - (void)checkForUpdatesAtAppcastURL:(NSURL *)appcastURL withUserAgent:(NSString *)userAgent httpHeaders:(NSDictionary * _Nullable)httpHeaders inBackground:(BOOL)background { if ([_host isRunningOnReadOnlyVolume]) { NSString *hostName = _host.name; id delegate = _delegate; #if SPARKLE_COPY_LOCALIZATIONS NSBundle *sparkleBundle = SUSparkleBundle(); #endif if ([_host isRunningTranslocated]) { [delegate basicDriverIsRequestingAbortUpdateWithError:[NSError errorWithDomain:SUSparkleErrorDomain code:SURunningTranslocated userInfo:@{ NSLocalizedRecoverySuggestionErrorKey: [NSString stringWithFormat:SULocalizedStringFromTableInBundle(@"Quit %1$@, move it into your Applications folder, relaunch it from there and try again.", SPARKLE_TABLE, sparkleBundle, nil), hostName], NSLocalizedDescriptionKey: [NSString stringWithFormat:SULocalizedStringFromTableInBundle(@"%1$@ can’t be updated if it’s running from the location it was downloaded to.", SPARKLE_TABLE, sparkleBundle, nil), hostName], }]]; } else { [delegate basicDriverIsRequestingAbortUpdateWithError:[NSError errorWithDomain:SUSparkleErrorDomain code:SURunningFromDiskImageError userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:SULocalizedStringFromTableInBundle(@"%1$@ can’t be updated because it was opened from a read-only or a temporary location.", SPARKLE_TABLE, sparkleBundle, nil), hostName], NSLocalizedRecoverySuggestionErrorKey: [NSString stringWithFormat:SULocalizedStringFromTableInBundle(@"Use Finder to copy %1$@ to the Applications folder, relaunch it from there, and try again.", SPARKLE_TABLE, sparkleBundle, nil), hostName] }]]; } } else { [_appcastDriver loadAppcastFromURL:appcastURL userAgent:userAgent httpHeaders:httpHeaders inBackground:background]; } } - (void)notifyResumableUpdateItem:(SUAppcastItem *)updateItem secondaryUpdateItem:(SUAppcastItem * _Nullable)secondaryUpdateItem systemDomain:(NSNumber * _Nullable)systemDomain SPU_OBJC_DIRECT { if (updateItem == nil) { [_delegate basicDriverIsRequestingAbortUpdateWithError:[NSError errorWithDomain:SUSparkleErrorDomain code:SUResumeAppcastError userInfo:@{ NSLocalizedDescriptionKey: SULocalizedStringFromTableInBundle(@"Failed to resume installing update.", SPARKLE_TABLE, SUSparkleBundle(), nil) }]]; } else { // Kind of lying, but triggering the notification so drivers can know when to stop showing initial fetching progress [self notifyFinishLoadingAppcast]; SUAppcastItem *nonNullUpdateItem = updateItem; [self notifyFoundValidUpdateWithAppcastItem:nonNullUpdateItem secondaryAppcastItem:secondaryUpdateItem systemDomain:systemDomain resuming:YES]; } } - (void)resumeInstallingUpdate { NSString *hostBundleIdentifier = _host.bundle.bundleIdentifier; assert(hostBundleIdentifier != nil); [SPUProbeInstallStatus probeInstallerUpdateItemForHostBundleIdentifier:hostBundleIdentifier completion:^(SPUInstallationInfo * _Nullable installationInfo) { dispatch_async(dispatch_get_main_queue(), ^{ [self notifyResumableUpdateItem:installationInfo.appcastItem secondaryUpdateItem:nil systemDomain:@(installationInfo.systemDomain)]; }); }]; } - (void)resumeUpdate:(id)resumableUpdate { [self notifyResumableUpdateItem:resumableUpdate.updateItem secondaryUpdateItem:resumableUpdate.secondaryUpdateItem systemDomain:nil]; } - (void)didFailToFetchAppcastWithError:(NSError *)error { if (!_aborted) { [_delegate basicDriverIsRequestingAbortUpdateWithError:error]; } } - (void)notifyFinishLoadingAppcast SPU_OBJC_DIRECT { id delegate = _delegate; if ([delegate respondsToSelector:@selector(basicDriverDidFinishLoadingAppcast)]) { [delegate basicDriverDidFinishLoadingAppcast]; } } - (void)didFinishLoadingAppcast:(SUAppcast *)appcast { if (!_aborted) { id updaterDelegate = _updaterDelegate; if ([updaterDelegate respondsToSelector:@selector((updater:didFinishLoadingAppcast:))]) { [updaterDelegate updater:_updater didFinishLoadingAppcast:appcast]; } [self notifyFinishLoadingAppcast]; } } - (void)notifyFoundValidUpdateWithAppcastItem:(SUAppcastItem *)updateItem secondaryAppcastItem:(SUAppcastItem * _Nullable)secondaryUpdateItem systemDomain:(NSNumber * _Nullable)systemDomain resuming:(BOOL)resuming SPU_OBJC_DIRECT { if (!_aborted) { id delegate = _delegate; id updaterDelegate = _updaterDelegate; id updater = _updater; if (!resuming) { // Give the delegate a chance to bail NSError *shouldNotProceedError = nil; if ([updaterDelegate respondsToSelector:@selector(updater:shouldProceedWithUpdate:updateCheck:error:)] && ![updaterDelegate updater:updater shouldProceedWithUpdate:updateItem updateCheck:_updateCheck error:&shouldNotProceedError]) { [delegate basicDriverIsRequestingAbortUpdateWithError:shouldNotProceedError]; return; } } [[NSNotificationCenter defaultCenter] postNotificationName:SUUpdaterDidFindValidUpdateNotification object:updater userInfo:@{ SUUpdaterAppcastItemNotificationKey: updateItem }]; if ([updaterDelegate respondsToSelector:@selector((updater:didFindValidUpdate:))]) { [updaterDelegate updater:updater didFindValidUpdate:updateItem]; } [delegate basicDriverDidFindUpdateWithAppcastItem:updateItem secondaryAppcastItem:secondaryUpdateItem systemDomain:systemDomain]; } } - (void)didFindValidUpdateWithAppcastItem:(SUAppcastItem *)updateItem secondaryAppcastItem:(SUAppcastItem * _Nullable)secondaryAppcastItem { [self notifyFoundValidUpdateWithAppcastItem:updateItem secondaryAppcastItem:secondaryAppcastItem systemDomain:nil resuming:NO]; } - (void)didNotFindUpdateWithLatestAppcastItem:(nullable SUAppcastItem *)latestAppcastItem hostToLatestAppcastItemComparisonResult:(NSComparisonResult)hostToLatestAppcastItemComparisonResult background:(BOOL)background { if (!_aborted) { NSString *localizedDescription; #if SPARKLE_COPY_LOCALIZATIONS NSBundle *sparkleBundle = SUSparkleBundle(); #else NSBundle *sparkleBundle = nil; #endif SPUNoUpdateFoundReason reason; if (latestAppcastItem != nil) { switch (hostToLatestAppcastItemComparisonResult) { case NSOrderedDescending: // This means the user is a 'newer than latest' version. give a slight hint to the user instead of wrongly claiming this version is identical to the latest feed version. localizedDescription = SULocalizedStringFromTableInBundle(@"You’re up to date!", SPARKLE_TABLE, sparkleBundle, "Status message shown when the user checks for updates but is already current or the feed doesn't contain any updates."); reason = SPUNoUpdateFoundReasonOnNewerThanLatestVersion; break; case NSOrderedSame: // No new update is available and we're on the latest localizedDescription = SULocalizedStringFromTableInBundle(@"You’re up to date!", SPARKLE_TABLE, sparkleBundle, "Status message shown when the user checks for updates but is already current or the feed doesn't contain any updates."); reason = SPUNoUpdateFoundReasonOnLatestVersion; break; case NSOrderedAscending: // A new update is available but cannot be installed // More detailed recovery suggestions are in SPUNoUpdateFoundRecoverySuggestion() if (!latestAppcastItem.arm64HardwareRequirementIsOK) { localizedDescription = SULocalizedStringFromTableInBundle(@"Your Mac is too old", SPARKLE_TABLE, sparkleBundle, nil); reason = SPUNoUpdateFoundReasonHardwareDoesNotSupportARM64; } else if (!latestAppcastItem.minimumOperatingSystemVersionIsOK) { localizedDescription = SULocalizedStringFromTableInBundle(@"Your macOS version is too old", SPARKLE_TABLE, sparkleBundle, nil); reason = SPUNoUpdateFoundReasonSystemIsTooOld; } else if (!latestAppcastItem.maximumOperatingSystemVersionIsOK) { localizedDescription = SULocalizedStringFromTableInBundle(@"Your macOS version is too new", SPARKLE_TABLE, sparkleBundle, nil); reason = SPUNoUpdateFoundReasonSystemIsTooNew; } else { // We shouldn't realistically get here localizedDescription = SULocalizedStringFromTableInBundle(@"You’re up to date!", SPARKLE_TABLE, sparkleBundle, "Status message shown when the user checks for updates but is already current or the feed doesn't contain any updates."); reason = SPUNoUpdateFoundReasonUnknown; } break; } } else { // When no updates are found in the appcast // We will need to assume the user is up to date if the feed doesn't have any applicable update items // There could be update items on channels the updater is not subscribed to for example. But we can't tell the user about them. // There could also only be update items available for other platforms or none at all. localizedDescription = SULocalizedStringFromTableInBundle(@"You’re up to date!", SPARKLE_TABLE, sparkleBundle, "Status message shown when the user checks for updates but is already current or the feed doesn't contain any updates."); reason = SPUNoUpdateFoundReasonOnLatestVersion; } // We use the standard version displayer here to construct a reason string, // but it's possible for the user driver to override this before displaying if they wish id versionDisplayer = [SPUStandardVersionDisplay standardVersionDisplay]; NSString *recoverySuggestion = SPUNoUpdateFoundRecoverySuggestion(reason, latestAppcastItem, _host, versionDisplayer, sparkleBundle); NSString *recoveryOption = SULocalizedStringFromTableInBundle(@"OK", SPARKLE_TABLE, sparkleBundle, nil); NSMutableDictionary *userInfo = [NSMutableDictionary dictionaryWithDictionary:@{ NSLocalizedDescriptionKey: localizedDescription, NSLocalizedRecoverySuggestionErrorKey: recoverySuggestion, NSLocalizedRecoveryOptionsErrorKey: @[recoveryOption], SPUNoUpdateFoundReasonKey: @(reason), SPUNoUpdateFoundUserInitiatedKey: @(!background), }]; if (latestAppcastItem != nil) { userInfo[SPULatestAppcastItemFoundKey] = latestAppcastItem; } NSError *notFoundError = [NSError errorWithDomain:SUSparkleErrorDomain code:SUNoUpdateError userInfo:[userInfo copy]]; id updaterDelegate = _updaterDelegate; id updater = _updater; if (updater != nil) { if ([updaterDelegate respondsToSelector:@selector((updaterDidNotFindUpdate:error:))]) { [updaterDelegate updaterDidNotFindUpdate:updater error:notFoundError]; } else if ([updaterDelegate respondsToSelector:@selector((updaterDidNotFindUpdate:))]) { [updaterDelegate updaterDidNotFindUpdate:updater]; } [[NSNotificationCenter defaultCenter] postNotificationName:SUUpdaterDidNotFindUpdateNotification object:updater userInfo:userInfo]; } [_delegate basicDriverIsRequestingAbortUpdateWithError:notFoundError]; } } - (void)abortUpdateAndShowNextUpdateImmediately:(BOOL)shouldShowUpdateImmediately resumableUpdate:(id _Nullable)resumableUpdate error:(nullable NSError *)error { _aborted = YES; [_appcastDriver cleanup:^{ if (self->_completionBlock != nil) { self->_completionBlock(shouldShowUpdateImmediately, resumableUpdate, error); self->_completionBlock = nil; } }]; } @end ================================================ FILE: Sparkle/SPUCoreBasedUpdateDriver.h ================================================ // // SPUCoreBasedUpdateDriver.h // Sparkle // // Created by Mayur Pawashe on 3/18/16. // Copyright © 2016 Sparkle Project. All rights reserved. // #import #import "SPUUpdateDriver.h" #import "SPUUserUpdateState.h" #import "SPUUpdateCheck.h" NS_ASSUME_NONNULL_BEGIN @class SUHost, SUAppcastItem; @protocol SPUUpdaterDelegate; @protocol SPUCoreBasedUpdateDriverDelegate - (void)basicDriverDidFindUpdateWithAppcastItem:(SUAppcastItem *)updateItem secondaryAppcastItem:(SUAppcastItem * _Nullable)secondaryAppcastItem; - (void)installerDidFinishPreparationAndWillInstallImmediately:(BOOL)willInstallImmediately; - (void)coreDriverIsRequestingAbortUpdateWithError:(nullable NSError *)error; - (void)basicDriverIsRequestingAbortUpdateWithError:(nullable NSError *)error; @optional - (void)basicDriverDidFinishLoadingAppcast; - (void)downloadDriverWillBeginDownload; - (void)downloadDriverDidReceiveExpectedContentLength:(uint64_t)expectedContentLength; - (void)downloadDriverDidReceiveDataOfLength:(uint64_t)length; - (void)coreDriverDidStartExtractingUpdate; - (void)installerDidStartInstallingWithApplicationTerminated:(BOOL)applicationTerminated; - (void)installerDidExtractUpdateWithProgress:(double)progress; - (void)installerDidFinishInstallationAndRelaunched:(BOOL)relaunched acknowledgement:(void(^)(void))acknowledgement; @end SPU_OBJC_DIRECT_MEMBERS @interface SPUCoreBasedUpdateDriver : NSObject - (instancetype)initWithHost:(SUHost *)host applicationBundle:(NSBundle *)applicationBundle updateCheck:(SPUUpdateCheck)updateCheck updater:(id)updater updaterDelegate:(nullable id )updaterDelegate delegate:(id)delegate; - (void)setCompletionHandler:(SPUUpdateDriverCompletion)completionBlock; - (void)setUpdateWillInstallHandler:(void (^)(void))updateWillInstallHandler; - (void)checkForUpdatesAtAppcastURL:(NSURL *)appcastURL withUserAgent:(NSString *)userAgent httpHeaders:(NSDictionary * _Nullable)httpHeaders inBackground:(BOOL)background requiresSilentInstall:(BOOL)silentInstall; - (void)resumeInstallingUpdate; - (void)resumeUpdate:(id)resumableUpdate; - (void)downloadUpdateFromAppcastItem:(SUAppcastItem *)updateItem secondaryAppcastItem:(SUAppcastItem * _Nullable)secondaryUpdateItem inBackground:(BOOL)background; - (void)deferInformationalUpdate:(SUAppcastItem *)updateItem secondaryUpdate:(SUAppcastItem * _Nullable)secondaryUpdateItem; - (void)extractDownloadedUpdate; - (void)clearDownloadedUpdate; - (void)finishInstallationWithResponse:(SPUUserUpdateChoice)installUpdateStatus displayingUserInterface:(BOOL)displayingUserInterface; - (void)abortUpdateAndShowNextUpdateImmediately:(BOOL)shouldShowUpdateImmediately error:(nullable NSError *)error; @end NS_ASSUME_NONNULL_END ================================================ FILE: Sparkle/SPUCoreBasedUpdateDriver.m ================================================ // // SPUCoreBasedUpdateDriver.m // Sparkle // // Created by Mayur Pawashe on 3/18/16. // Copyright © 2016 Sparkle Project. All rights reserved. // #import "SPUCoreBasedUpdateDriver.h" #import "SUHost.h" #import "SPUUpdaterDelegate.h" #import "SPUBasicUpdateDriver.h" #import "SPUInstallerDriver.h" #import "SPUDownloadDriver.h" #import "SULog.h" #import "SULog+NSError.h" #import "SUErrors.h" #import "SPUResumableUpdate.h" #import "SPUDownloadedUpdate.h" #import "SPUInformationalUpdate.h" #import "SUAppcastItem.h" #import "SULocalizations.h" #import "SPUInstallationType.h" #import "SUPhasedUpdateGroupInfo.h" #include "AppKitPrevention.h" @interface SPUCoreBasedUpdateDriver () @end @implementation SPUCoreBasedUpdateDriver { SPUBasicUpdateDriver *_basicDriver; SPUDownloadDriver *_downloadDriver; SPUInstallerDriver *_installerDriver; SUAppcastItem *_updateItem; SUAppcastItem * _Nullable _secondaryUpdateItem; id _resumableUpdate; SPUDownloadedUpdate *_downloadedUpdateForRemoval; SUHost *_host; NSString *_userAgent; NSDictionary * _Nullable _httpHeaders; __weak id _updater; // if we didn't have legacy support, I'd remove this.. __weak id _updaterDelegate; __weak id _delegate; BOOL _resumingInstallingUpdate; BOOL _silentInstall; } - (instancetype)initWithHost:(SUHost *)host applicationBundle:(NSBundle *)applicationBundle updateCheck:(SPUUpdateCheck)updateCheck updater:(id)updater updaterDelegate:(nullable id )updaterDelegate delegate:(id)delegate { self = [super init]; if (self != nil) { _delegate = delegate; NSString *bundleIdentifier = host.bundle.bundleIdentifier; assert(bundleIdentifier != nil); _basicDriver = [[SPUBasicUpdateDriver alloc] initWithHost:host updateCheck:updateCheck updater:updater updaterDelegate:updaterDelegate delegate:self]; _installerDriver = [[SPUInstallerDriver alloc] initWithHost:host applicationBundle:applicationBundle updater:updater updaterDelegate:updaterDelegate delegate:self]; _host = host; _updater = updater; _updaterDelegate = updaterDelegate; } return self; } - (void)setCompletionHandler:(SPUUpdateDriverCompletion)completionBlock { [_basicDriver setCompletionHandler:completionBlock]; } - (void)setUpdateWillInstallHandler:(void (^)(void))updateWillInstallHandler { [_installerDriver setUpdateWillInstallHandler:updateWillInstallHandler]; } - (void)checkForUpdatesAtAppcastURL:(NSURL *)appcastURL withUserAgent:(NSString *)userAgent httpHeaders:(NSDictionary * _Nullable)httpHeaders inBackground:(BOOL)background requiresSilentInstall:(BOOL)silentInstall { _userAgent = [userAgent copy]; _httpHeaders = httpHeaders; _silentInstall = silentInstall; [_basicDriver checkForUpdatesAtAppcastURL:appcastURL withUserAgent:userAgent httpHeaders:httpHeaders inBackground:background]; } - (void)resumeInstallingUpdate { _resumingInstallingUpdate = YES; _silentInstall = NO; [_basicDriver resumeInstallingUpdate]; } - (void)resumeUpdate:(id)resumableUpdate { _resumableUpdate = resumableUpdate; _silentInstall = NO; [_basicDriver resumeUpdate:resumableUpdate]; } - (void)basicDriverDidFinishLoadingAppcast { id delegate = _delegate; if ([delegate respondsToSelector:@selector(basicDriverDidFinishLoadingAppcast)]) { [delegate basicDriverDidFinishLoadingAppcast]; } } - (void)basicDriverDidFindUpdateWithAppcastItem:(SUAppcastItem *)updateItem secondaryAppcastItem:(SUAppcastItem * _Nullable)secondaryUpdateItem systemDomain:(NSNumber * _Nullable)systemDomain { _updateItem = updateItem; _secondaryUpdateItem = secondaryUpdateItem; if (_resumingInstallingUpdate) { assert(systemDomain != nil); [_installerDriver resumeInstallingUpdateWithUpdateItem:updateItem systemDomain:systemDomain.boolValue]; } [_delegate basicDriverDidFindUpdateWithAppcastItem:updateItem secondaryAppcastItem:secondaryUpdateItem]; } - (void)downloadUpdateFromAppcastItem:(SUAppcastItem *)updateItem secondaryAppcastItem:(SUAppcastItem * _Nullable)secondaryUpdateItem inBackground:(BOOL)background SPU_OBJC_DIRECT { _downloadDriver = [[SPUDownloadDriver alloc] initWithUpdateItem:updateItem secondaryUpdateItem:secondaryUpdateItem host:_host userAgent:_userAgent httpHeaders:_httpHeaders inBackground:background delegate:self]; id updater = _updater; id updaterDelegate = _updaterDelegate; if (updater != nil && [updaterDelegate respondsToSelector:@selector((updater:willDownloadUpdate:withRequest:))]) { [updaterDelegate updater:updater willDownloadUpdate:updateItem withRequest:_downloadDriver.request]; } [_downloadDriver downloadFile]; } - (void)downloadDriverWillBeginDownload { id delegate = _delegate; if ([delegate respondsToSelector:@selector(downloadDriverWillBeginDownload)]) { [delegate downloadDriverWillBeginDownload]; } } - (void)downloadDriverDidReceiveExpectedContentLength:(uint64_t)expectedContentLength { id delegate = _delegate; if ([delegate respondsToSelector:@selector(downloadDriverDidReceiveExpectedContentLength:)]) { [delegate downloadDriverDidReceiveExpectedContentLength:expectedContentLength]; } } - (void)downloadDriverDidReceiveDataOfLength:(uint64_t)length { id delegate = _delegate; if ([delegate respondsToSelector:@selector(downloadDriverDidReceiveDataOfLength:)]) { [delegate downloadDriverDidReceiveDataOfLength:length]; } } - (void)downloadDriverDidDownloadUpdate:(SPUDownloadedUpdate *)downloadedUpdate { // Use a new update group for our next downloaded update // We could restrict this to when the appcast was downloaded in the background, // but it shouldn't matter. if (downloadedUpdate.updateItem.phasedRolloutInterval != nil) { [SUPhasedUpdateGroupInfo setNewUpdateGroupIdentifierForHost:_host]; } id updater = _updater; id updaterDelegate = _updaterDelegate; if (updater != nil && [updaterDelegate respondsToSelector:@selector(updater:didDownloadUpdate:)]) { [updaterDelegate updater:updater didDownloadUpdate:_updateItem]; } _resumableUpdate = downloadedUpdate; [self extractUpdate:downloadedUpdate]; } - (void)deferInformationalUpdate:(SUAppcastItem *)updateItem secondaryUpdate:(SUAppcastItem * _Nullable)secondaryUpdateItem { _resumableUpdate = [[SPUInformationalUpdate alloc] initWithAppcastItem:updateItem secondaryAppcastItem:secondaryUpdateItem]; } - (void)extractDownloadedUpdate { id resumableUpdate = _resumableUpdate; assert(resumableUpdate != nil && [resumableUpdate isKindOfClass:[SPUDownloadedUpdate class]]); [self extractUpdate:(SPUDownloadedUpdate *)resumableUpdate]; } - (void)clearDownloadedUpdate { id downloadedUpdateObject = (_resumableUpdate != nil) ? _resumableUpdate : _downloadedUpdateForRemoval; assert(downloadedUpdateObject != nil); if (downloadedUpdateObject != nil && [downloadedUpdateObject isKindOfClass:[SPUDownloadedUpdate class]]) { if (_downloadDriver == nil) { _downloadDriver = [[SPUDownloadDriver alloc] initWithHost:_host]; } SPUDownloadedUpdate *downloadedUpdate = (SPUDownloadedUpdate *)downloadedUpdateObject; [_downloadDriver removeDownloadedUpdate:downloadedUpdate]; } // Clear any type of resumable update _resumableUpdate = nil; } - (void)extractUpdate:(SPUDownloadedUpdate *)downloadedUpdate SPU_OBJC_DIRECT { id updater = _updater; id updaterDelegate = _updaterDelegate; if (updater != nil && [updaterDelegate respondsToSelector:@selector(updater:willExtractUpdate:)]) { [updaterDelegate updater:updater willExtractUpdate:_updateItem]; } // Now we have to extract the downloaded archive. id delegate = _delegate; if ([delegate respondsToSelector:@selector(coreDriverDidStartExtractingUpdate)]) { [delegate coreDriverDidStartExtractingUpdate]; } [_installerDriver extractDownloadedUpdate:downloadedUpdate silently:_silentInstall completion:^(NSError * _Nullable error) { if (error != nil) { if (error.code != SUInstallationAuthorizeLaterError) { [self clearDownloadedUpdate]; } [self->_delegate coreDriverIsRequestingAbortUpdateWithError:error]; } else { // If the installer started properly, we can't use the downloaded update archive anymore // Especially if the installer fails later and we try resuming the update with a missing archive file // We must clear the download after the installer begins using it however (in -installerDidStartInstalling) self->_downloadedUpdateForRemoval = downloadedUpdate; self->_resumableUpdate = nil; if (updater != nil && [updaterDelegate respondsToSelector:@selector(updater:didExtractUpdate:)]) { [updaterDelegate updater:updater didExtractUpdate:self->_updateItem]; } } }]; } - (void)downloadDriverDidFailToDownloadFileWithError:(NSError *)error { if ([_updateItem isDeltaUpdate]) { SULog(SULogLevelError, @"Failed to download delta update. Falling back to regular update..."); SULogError(error); [self fallBackAndDownloadRegularUpdate]; } else { id updater = _updater; id updaterDelegate = _updaterDelegate; if (updater != nil && [updaterDelegate respondsToSelector:@selector((updater:failedToDownloadUpdate:error:))]) { NSError *errorToReport = [error.userInfo objectForKey:NSUnderlyingErrorKey]; if (errorToReport == nil) { errorToReport = error; } [updaterDelegate updater:updater failedToDownloadUpdate:_updateItem error:errorToReport]; } [_delegate coreDriverIsRequestingAbortUpdateWithError:error]; } } - (void)installerDidStartInstallingWithApplicationTerminated:(BOOL)applicationTerminated { id delegate = _delegate; if ([delegate respondsToSelector:@selector(installerDidStartInstallingWithApplicationTerminated:)]) { [delegate installerDidStartInstallingWithApplicationTerminated:applicationTerminated]; } } - (void)installerDidStartExtracting { // The installer has moved the archive and no longer needs the download directory [self clearDownloadedUpdate]; } - (void)installerDidExtractUpdateWithProgress:(double)progress { id delegate = _delegate; if ([delegate respondsToSelector:@selector(installerDidExtractUpdateWithProgress:)]) { [delegate installerDidExtractUpdateWithProgress:progress]; } } - (void)installerDidFinishPreparationAndWillInstallImmediately:(BOOL)willInstallImmediately { [_delegate installerDidFinishPreparationAndWillInstallImmediately:willInstallImmediately]; } - (void)finishInstallationWithResponse:(SPUUserUpdateChoice)response displayingUserInterface:(BOOL)displayingUserInterface { switch (response) { case SPUUserUpdateChoiceDismiss: [_delegate coreDriverIsRequestingAbortUpdateWithError:nil]; break; case SPUUserUpdateChoiceSkip: [_installerDriver cancelUpdate]; break; case SPUUserUpdateChoiceInstall: [_installerDriver installWithToolAndRelaunch:YES displayingUserInterface:displayingUserInterface]; break; } } - (void)installerWillFinishInstallationAndRelaunch:(BOOL)relaunch { id updater = _updater; id updaterDelegate = _updaterDelegate; if (updater != nil) { if ([updaterDelegate respondsToSelector:@selector((updater:willInstallUpdate:))]) { [updaterDelegate updater:updater willInstallUpdate:_updateItem]; } if (relaunch) { [[NSNotificationCenter defaultCenter] postNotificationName:SUUpdaterWillRestartNotification object:updater]; if ([updaterDelegate respondsToSelector:@selector((updaterWillRelaunchApplication:))]) { [updaterDelegate updaterWillRelaunchApplication:updater]; } } } } - (void)installerDidFinishInstallationAndRelaunched:(BOOL)relaunched acknowledgement:(void(^)(void))acknowledgement { id delegate = _delegate; if ([delegate respondsToSelector:@selector(installerDidFinishInstallationAndRelaunched:acknowledgement:)]) { [delegate installerDidFinishInstallationAndRelaunched:relaunched acknowledgement:acknowledgement]; } else { acknowledgement(); } } - (void)installerIsRequestingAbortInstallWithError:(nullable NSError *)error { [_delegate coreDriverIsRequestingAbortUpdateWithError:error]; } - (void)basicDriverIsRequestingAbortUpdateWithError:(nullable NSError *)error { // A delegate may want to handle this type of error specially [_delegate basicDriverIsRequestingAbortUpdateWithError:error]; } - (void)fallBackAndDownloadRegularUpdate SPU_OBJC_DIRECT { SUAppcastItem *secondaryUpdateItem = _secondaryUpdateItem; assert(secondaryUpdateItem != nil); BOOL backgroundDownload = _downloadDriver.inBackground; // Fall back to the non-delta update. Note that we don't want to trigger another update was found event. _updateItem = secondaryUpdateItem; _secondaryUpdateItem = nil; [self downloadUpdateFromAppcastItem:secondaryUpdateItem secondaryAppcastItem:nil inBackground:backgroundDownload]; } - (void)installerDidFailToApplyDeltaUpdate { [self clearDownloadedUpdate]; [self fallBackAndDownloadRegularUpdate]; } - (void)abortUpdateAndShowNextUpdateImmediately:(BOOL)shouldShowUpdateImmediately error:(nullable NSError *)error { [_installerDriver abortInstall]; void (^basicDriverAbort)(void) = ^{ id resumableUpdate = (error == nil || error.code == SUInstallationAuthorizeLaterError) ? self->_resumableUpdate : nil; [self->_basicDriver abortUpdateAndShowNextUpdateImmediately:shouldShowUpdateImmediately resumableUpdate:resumableUpdate error:error]; }; if (_downloadDriver != nil) { [_downloadDriver cleanup:^{ basicDriverAbort(); }]; } else { basicDriverAbort(); } } @end ================================================ FILE: Sparkle/SPUDownloadData.h ================================================ // // SPUDownloadData.h // Sparkle // // Created by Mayur Pawashe on 8/10/16. // Copyright © 2016 Sparkle Project. All rights reserved. // #import #if defined(BUILDING_SPARKLE_SOURCES_EXTERNALLY) // Ignore incorrect warning #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wquoted-include-in-framework-header" #import "SUExport.h" #pragma clang diagnostic pop #else #import #endif NS_ASSUME_NONNULL_BEGIN /** * A class for containing downloaded data along with some information about it. */ SU_EXPORT NS_SWIFT_SENDABLE @interface SPUDownloadData : NSObject /** * The raw data that was downloaded. */ @property (nonatomic, readonly) NSData *data; /** * The URL that was fetched from. * * This may be different from the URL in the request if there were redirects involved. */ @property (nonatomic, readonly, copy) NSURL *URL; /** * The IANA charset encoding name if available. Eg: "utf-8" */ @property (nonatomic, readonly, nullable, copy) NSString *textEncodingName; /** * The MIME type if available. Eg: "text/plain" */ @property (nonatomic, readonly, nullable, copy) NSString *MIMEType; @end NS_ASSUME_NONNULL_END ================================================ FILE: Sparkle/SPUDownloadData.m ================================================ // // SPUDownloadData.m // Sparkle // // Created by Mayur Pawashe on 8/10/16. // Copyright © 2016 Sparkle Project. All rights reserved. // #import "SPUDownloadData.h" #include "AppKitPrevention.h" static NSString *SPUDownloadDataKey = @"SPUDownloadData"; static NSString *SPUDownloadURLKey = @"SPUDownloadURL"; static NSString *SPUDownloadTextEncodingKey = @"SPUDownloadTextEncoding"; static NSString *SPUDownloadMIMETypeKey = @"SPUDownloadMIMEType"; @implementation SPUDownloadData @synthesize data = _data; @synthesize URL = _URL; @synthesize textEncodingName = _textEncodingName; @synthesize MIMEType = _MIMEType; + (BOOL)supportsSecureCoding { return YES; } - (instancetype)initWithData:(NSData *)data URL:(NSURL *)URL textEncodingName:(NSString * _Nullable)textEncodingName MIMEType:(NSString *)MIMEType { self = [super init]; if (self != nil) { _data = data; _URL = URL; _textEncodingName = textEncodingName; _MIMEType = MIMEType; } return self; } - (void)encodeWithCoder:(NSCoder *)coder { [coder encodeObject:_data forKey:SPUDownloadDataKey]; [coder encodeObject:_URL forKey:SPUDownloadURLKey]; if (_textEncodingName != nil) { [coder encodeObject:_textEncodingName forKey:SPUDownloadTextEncodingKey]; } if (_MIMEType != nil) { [coder encodeObject:_MIMEType forKey:SPUDownloadMIMETypeKey]; } } - (nullable instancetype)initWithCoder:(NSCoder *)decoder { NSData *data = [decoder decodeObjectOfClass:[NSData class] forKey:SPUDownloadDataKey]; if (data == nil) { return nil; } NSURL *URL = [decoder decodeObjectOfClass:[NSURL class] forKey:SPUDownloadURLKey]; if (URL == nil) { return nil; } NSString *textEncodingName = [decoder decodeObjectOfClass:[NSString class] forKey:SPUDownloadTextEncodingKey]; NSString *MIMEType = [decoder decodeObjectOfClass:[NSString class] forKey:SPUDownloadMIMETypeKey]; return [self initWithData:data URL:URL textEncodingName:textEncodingName MIMEType:MIMEType]; } @end ================================================ FILE: Sparkle/SPUDownloadDataPrivate.h ================================================ // // SPUDownloadDataPrivate.h // SPUDownloadDataPrivate // // Created by Mayur Pawashe on 8/13/21. // Copyright © 2021 Sparkle Project. All rights reserved. // #import NS_ASSUME_NONNULL_BEGIN @interface SPUDownloadData (Private) - (instancetype)initWithData:(NSData *)data URL:(NSURL *)URL textEncodingName:(NSString * _Nullable)textEncodingName MIMEType:(NSString * _Nullable)MIMEType; @end NS_ASSUME_NONNULL_END ================================================ FILE: Sparkle/SPUDownloadDriver.h ================================================ // // SPUDownloadDriver.h // Sparkle // // Created by Mayur Pawashe on 3/15/16. // Copyright © 2016 Sparkle Project. All rights reserved. // #import NS_ASSUME_NONNULL_BEGIN @class SUAppcastItem, SUHost, SPUDownloadedUpdate, SPUDownloadData; @protocol SPUDownloadDriverDelegate - (void)downloadDriverDidFailToDownloadFileWithError:(NSError *)error; @optional - (void)downloadDriverWillBeginDownload; // For persistent update downloads - (void)downloadDriverDidDownloadUpdate:(SPUDownloadedUpdate *)downloadedUpdate; // For temporary downloads - (void)downloadDriverDidDownloadData:(SPUDownloadData *)downloadData; // Only for persistent downloads - (void)downloadDriverDidReceiveExpectedContentLength:(uint64_t)expectedContentLength; // Only for persistent downloads - (void)downloadDriverDidReceiveDataOfLength:(uint64_t)length; @end #ifndef BUILDING_SPARKLE_TESTS SPU_OBJC_DIRECT_MEMBERS #endif @interface SPUDownloadDriver : NSObject - (instancetype)initWithRequestURL:(NSURL *)requestURL host:(SUHost *)host userAgent:(NSString * _Nullable)userAgent httpHeaders:(NSDictionary * _Nullable)httpHeaders inBackground:(BOOL)background delegate:(id)delegate; - (instancetype)initWithUpdateItem:(SUAppcastItem *)updateItem secondaryUpdateItem:(SUAppcastItem * _Nullable)secondaryUpdateItem host:(SUHost *)host userAgent:(NSString * _Nullable)userAgent httpHeaders:(NSDictionary * _Nullable)httpHeaders inBackground:(BOOL)background delegate:(id)delegate; - (instancetype)initWithHost:(SUHost *)host; - (void)downloadFile; - (void)removeDownloadedUpdate:(SPUDownloadedUpdate *)downloadedUpdate; @property (nonatomic, readonly) NSMutableURLRequest *request; @property (nonatomic, readonly) BOOL inBackground; - (void)cleanup:(void (^)(void))completionHandler; @end NS_ASSUME_NONNULL_END ================================================ FILE: Sparkle/SPUDownloadDriver.m ================================================ // // SPUDownloadDriver.m // Sparkle // // Created by Mayur Pawashe on 3/15/16. // Copyright © 2016 Sparkle Project. All rights reserved. // #import "SPUDownloadDriver.h" #import "SPUDownloaderDelegate.h" #import "SPUDownloader.h" #import "SPUXPCServiceInfo.h" #import "SUAppcastItem.h" #import "SUFileManager.h" #import "SULocalizations.h" #import "SUHost.h" #import "SULog.h" #import "SUErrors.h" #import "SPUDownloadedUpdate.h" #import "SPUDownloadData.h" #import "SUConstants.h" #include "AppKitPrevention.h" @interface SPUDownloadDriver () @end @implementation SPUDownloadDriver { id _downloader; #if DOWNLOADER_XPC_SERVICE_EMBEDDED NSXPCConnection *_connection; #endif SUAppcastItem *_updateItem; SUAppcastItem * _Nullable _secondaryUpdateItem; SUHost *_host; NSData *_downloadBookmarkData; NSString *_downloadToken; __weak id _delegate; uint64_t _expectedContentLength; BOOL _retrievedDownloadResult; BOOL _cleaningUp; } @synthesize request = _request; @synthesize inBackground = _inBackground; - (instancetype)initWithHost:(SUHost *)host { self = [super init]; if (self != nil) { _host = host; #if DOWNLOADER_XPC_SERVICE_EMBEDDED if (SPUXPCServiceIsEnabled(SUEnableDownloaderServiceKey)) { _connection = [[NSXPCConnection alloc] initWithServiceName:@DOWNLOADER_BUNDLE_ID]; _connection.remoteObjectInterface = [NSXPCInterface interfaceWithProtocol:@protocol(SPUDownloaderProtocol)]; _connection.exportedInterface = [NSXPCInterface interfaceWithProtocol:@protocol(SPUDownloaderDelegate)]; _connection.exportedObject = self; _downloader = _connection.remoteObjectProxy; __weak __typeof__(self) weakSelf = self; _connection.interruptionHandler = ^{ dispatch_async(dispatch_get_main_queue(), ^{ __typeof__(self) strongSelf = weakSelf; if (strongSelf != nil && !strongSelf->_retrievedDownloadResult) { [strongSelf->_connection invalidate]; } }); }; _connection.invalidationHandler = ^{ dispatch_async(dispatch_get_main_queue(), ^{ __typeof__(self) strongSelf = weakSelf; if (strongSelf != nil && !strongSelf->_retrievedDownloadResult && !strongSelf->_cleaningUp) { strongSelf->_downloader = nil; NSString *additionalFailureReason; { NSString *executableFailureReason; if (!SPUXPCServiceHasExecutablePermission(@DOWNLOADER_NAME, &executableFailureReason)) { additionalFailureReason = [NSString stringWithFormat:@" %@", executableFailureReason]; } else { additionalFailureReason = @""; } } NSDictionary *userInfo = @{ NSLocalizedDescriptionKey: SULocalizedStringFromTableInBundle(@"An error occurred while downloading the update. Please try again later.", SPARKLE_TABLE, SUSparkleBundle(), nil), NSLocalizedFailureReasonErrorKey: [NSString stringWithFormat:@"If your app is not sandboxed or has com.apple.security.network.client set to YES, please remove %@ from your Info.plist. Please also check Console logs for "@DOWNLOADER_NAME" if there are any additional details.%@", SUEnableDownloaderServiceKey, additionalFailureReason] }; NSError *downloadError = [NSError errorWithDomain:SUSparkleErrorDomain code:SUDownloadError userInfo:userInfo]; [strongSelf->_delegate downloadDriverDidFailToDownloadFileWithError:downloadError]; } }); }; [_connection resume]; } else #endif { _downloader = [[SPUDownloader alloc] initWithDelegate:self]; } } return self; } - (instancetype)initWithRequestURL:(NSURL *)requestURL host:(SUHost *)host userAgent:(NSString * _Nullable)userAgent httpHeaders:(NSDictionary * _Nullable)httpHeaders inBackground:(BOOL)background delegate:(id)delegate { self = [self initWithHost:host]; if (self != nil) { _delegate = delegate; _inBackground = background; NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:requestURL]; // Note the cachePolicy has no effect on persistent downloads on disk (i.e downloading update archives) // It impacts temporary in-memory downloads such as appcast feeds and release notes. // For now we don't use caching, but with more testing/experimenting that could change // (e.g. not downloading same feed unmodified from previous request). request.cachePolicy = NSURLRequestReloadIgnoringLocalCacheData; if (userAgent != nil) { [request setValue:(NSString * _Nonnull)userAgent forHTTPHeaderField:@"User-Agent"]; } request.networkServiceType = background ? NSURLNetworkServiceTypeBackground : NSURLNetworkServiceTypeDefault; if (httpHeaders != nil) { for (NSString *key in httpHeaders) { NSString *value = [httpHeaders objectForKey:key]; [request setValue:value forHTTPHeaderField:key]; } } _request = request; } return self; } - (instancetype)initWithUpdateItem:(SUAppcastItem *)updateItem secondaryUpdateItem:(SUAppcastItem * _Nullable)secondaryUpdateItem host:(SUHost *)host userAgent:(NSString * _Nullable)userAgent httpHeaders:(NSDictionary * _Nullable)httpHeaders inBackground:(BOOL)background delegate:(id)delegate { NSURL *updateFileURL = updateItem.fileURL; assert(updateFileURL != nil); self = [self initWithRequestURL:updateFileURL host:host userAgent:userAgent httpHeaders:httpHeaders inBackground:background delegate:delegate]; if (self != nil) { _updateItem = updateItem; _secondaryUpdateItem = secondaryUpdateItem; } return self; } - (void)downloadFile { assert(NSThread.isMainThread); id delegate = _delegate; if ([delegate respondsToSelector:@selector(downloadDriverWillBeginDownload)]) { [delegate downloadDriverWillBeginDownload]; } if (_updateItem != nil) { NSString *desiredFilename = [NSString stringWithFormat:@"%@ %@", [_host name], [_updateItem versionString]]; NSString *bundleIdentifier = _host.bundle.bundleIdentifier; assert(bundleIdentifier != nil); [_downloader startPersistentDownloadWithRequest:_request bundleIdentifier:bundleIdentifier desiredFilename:desiredFilename]; } else { [_downloader startTemporaryDownloadWithRequest:_request]; } } - (void)removeDownloadedUpdate:(SPUDownloadedUpdate *)downloadedUpdate { NSString *bundleIdentifier = _host.bundle.bundleIdentifier; assert(bundleIdentifier != nil); [_downloader removeDownloadDirectoryWithDownloadToken:downloadedUpdate.downloadToken bundleIdentifier:bundleIdentifier]; } - (void)cleanup:(void (^)(void))completionHandler { void (^cleanupBlock)(void) = ^{ self->_cleaningUp = YES; #if DOWNLOADER_XPC_SERVICE_EMBEDDED if (self->_connection != nil) { [self->_connection invalidate]; self->_connection = nil; } #endif self->_downloadBookmarkData = nil; self->_downloadToken = nil; self->_downloader = nil; completionHandler(); }; if (_downloader == nil) { cleanupBlock(); } else { [_downloader cleanup:^{ dispatch_async(dispatch_get_main_queue(), ^{ cleanupBlock(); }); }]; } } - (void)downloaderDidFinishWithTemporaryDownloadData:(SPUDownloadData * _Nullable)downloadData { dispatch_async(dispatch_get_main_queue(), ^{ self->_retrievedDownloadResult = YES; id delegate = self->_delegate; if (self->_updateItem != nil) { if (self->_expectedContentLength > 0 && self->_updateItem.contentLength > 0 && self->_expectedContentLength != self->_updateItem.contentLength) { SULog(SULogLevelError, @"Warning: Downloader's expected content length (%llu) != Appcast item's length (%llu)", self->_expectedContentLength, self->_updateItem.contentLength); } SPUDownloadedUpdate *downloadedUpdate = [[SPUDownloadedUpdate alloc] initWithAppcastItem:self->_updateItem secondaryAppcastItem:self->_secondaryUpdateItem downloadBookmarkData:self->_downloadBookmarkData downloadToken:self->_downloadToken]; if ([delegate respondsToSelector:@selector(downloadDriverDidDownloadUpdate:)]) { [delegate downloadDriverDidDownloadUpdate:downloadedUpdate]; } } else { assert(downloadData != nil); SPUDownloadData *nonNullDownloadData = downloadData; if ([delegate respondsToSelector:@selector(downloadDriverDidDownloadData:)]) { [delegate downloadDriverDidDownloadData:nonNullDownloadData]; } } }); } - (void)downloaderDidFailWithError:(NSError *)error { dispatch_async(dispatch_get_main_queue(), ^{ self->_retrievedDownloadResult = YES; NSURL *failingUrl = error.userInfo[NSURLErrorFailingURLErrorKey]; if (!failingUrl) { failingUrl = [self->_updateItem fileURL]; } NSMutableDictionary *userInfo = [NSMutableDictionary dictionaryWithDictionary:@{ NSLocalizedDescriptionKey: SULocalizedStringFromTableInBundle(@"An error occurred while downloading the update. Please try again later.", SPARKLE_TABLE, SUSparkleBundle(), nil), NSUnderlyingErrorKey: error, }]; if (failingUrl) { userInfo[NSURLErrorFailingURLErrorKey] = failingUrl; } NSError *downloadError = [NSError errorWithDomain:SUSparkleErrorDomain code:SUDownloadError userInfo:userInfo]; [self->_delegate downloadDriverDidFailToDownloadFileWithError:downloadError]; }); } - (void)downloaderDidSetDownloadBookmarkData:(NSData *)downloadBookmarkData downloadToken:(NSString *)downloadToken { dispatch_async(dispatch_get_main_queue(), ^{ self->_downloadBookmarkData = downloadBookmarkData; self->_downloadToken = [downloadToken copy]; }); } - (void)downloaderDidReceiveExpectedContentLength:(int64_t)expectedContentLength { dispatch_async(dispatch_get_main_queue(), ^{ // Fallback to appcast item's content length if we don't get the length from HTTP header id delegate = self->_delegate; if ([delegate respondsToSelector:@selector(downloadDriverDidReceiveExpectedContentLength:)]) { [delegate downloadDriverDidReceiveExpectedContentLength:expectedContentLength > 0 ? (uint64_t)expectedContentLength : self->_updateItem.contentLength]; } // Reset expected content length from downloader // Later we verify if the total length matches with the content length from the appcast if (expectedContentLength > 0) { self->_expectedContentLength = (uint64_t)expectedContentLength; } }); } - (void)downloaderDidReceiveDataOfLength:(uint64_t)length { dispatch_async(dispatch_get_main_queue(), ^{ id delegate = self->_delegate; if ([delegate respondsToSelector:@selector(downloadDriverDidReceiveDataOfLength:)]) { [delegate downloadDriverDidReceiveDataOfLength:length]; } }); } @end ================================================ FILE: Sparkle/SPUDownloadedUpdate.h ================================================ // // SPUDownloadedUpdate.h // Sparkle // // Created by Mayur Pawashe on 1/8/17. // Copyright © 2017 Sparkle Project. All rights reserved. // #import #import "SPUResumableUpdate.h" NS_ASSUME_NONNULL_BEGIN SPU_OBJC_DIRECT_MEMBERS @interface SPUDownloadedUpdate : NSObject - (instancetype)initWithAppcastItem:(SUAppcastItem *)updateItem secondaryAppcastItem:(SUAppcastItem * _Nullable)secondaryItem downloadBookmarkData:(NSData *)downloadBookmarkData downloadToken:(NSString *)downloadToken; @property (nonatomic, readonly) NSData *downloadBookmarkData; @property (nonatomic, readonly) NSString *downloadToken; @end NS_ASSUME_NONNULL_END ================================================ FILE: Sparkle/SPUDownloadedUpdate.m ================================================ // // SPUDownloadedUpdate.m // Sparkle // // Created by Mayur Pawashe on 1/8/17. // Copyright © 2017 Sparkle Project. All rights reserved. // #import "SPUDownloadedUpdate.h" #include "AppKitPrevention.h" @implementation SPUDownloadedUpdate // If we ever enable auto-synthesize in the future, we'll still need this synthesize // because the property is declared in a protocol @synthesize updateItem = _updateItem; @synthesize secondaryUpdateItem = _secondaryUpdateItem; @synthesize downloadBookmarkData = _downloadBookmarkData; @synthesize downloadToken = _downloadToken; - (instancetype)initWithAppcastItem:(SUAppcastItem *)updateItem secondaryAppcastItem:(SUAppcastItem * _Nullable)secondaryUpdateItem downloadBookmarkData:(NSData *)downloadBookmarkData downloadToken:(NSString *)downloadToken { self = [super init]; if (self != nil) { _updateItem = updateItem; _secondaryUpdateItem = secondaryUpdateItem; _downloadBookmarkData = downloadBookmarkData; _downloadToken = [downloadToken copy]; } return self; } @end ================================================ FILE: Sparkle/SPUExtractSignedFeed.h ================================================ // // SPUExtractSignedFeed.h // Sparkle // // Created on 12/25/25. // Copyright © 2025 Sparkle Project. All rights reserved. // #import NS_ASSUME_NONNULL_BEGIN // Extracts content from an appcast without the signing block, optionally returning back the signed signature & expected content length NSData *SPUExtractAppcastContent(NSData *appcastData, NSString * _Nullable __autoreleasing * _Nullable outEdSignatureBase64, uint64_t * _Nullable outContentLength); // Extracts HTML or markdown release notes data without the beginning sign warning comment NSData *SPUExtractReleaseNotesContent(NSData *data); NS_ASSUME_NONNULL_END ================================================ FILE: Sparkle/SPUExtractSignedFeed.m ================================================ // // SPUExtractSignedFeed.m // Sparkle // // Created on 12/25/25. // Copyright © 2025 Sparkle Project. All rights reserved. // #import "SPUExtractSignedFeed.h" NSData *SPUExtractAppcastContent(NSData *appcastData, NSString * _Nullable __autoreleasing * _Nullable outEdSignatureBase64, uint64_t * _Nullable outContentLength) { static char feedSigningPrefix[] = ""; NSUInteger appcastDataLength = appcastData.length; NSRange prefixRange = [appcastData rangeOfData:[NSData dataWithBytesNoCopy:feedSigningPrefix length:sizeof(feedSigningPrefix) - 1 freeWhenDone:NO] options:NSDataSearchBackwards range:NSMakeRange(0, appcastDataLength)]; if (prefixRange.location == NSNotFound) { return appcastData; } NSData *contentFeedData = [appcastData subdataWithRange:NSMakeRange(0, prefixRange.location)]; NSRange suffixRange = [appcastData rangeOfData:[NSData dataWithBytesNoCopy:feedSigningSuffix length:sizeof(feedSigningSuffix) - 1 freeWhenDone:NO] options:(NSDataSearchOptions)0 range:NSMakeRange(NSMaxRange(prefixRange), appcastDataLength - NSMaxRange(prefixRange))]; if (suffixRange.location == NSNotFound) { return appcastData; } NSData *signingBlockData = [appcastData subdataWithRange:NSMakeRange(NSMaxRange(prefixRange), suffixRange.location - NSMaxRange(prefixRange))]; NSString *signingBlockString = [[NSString alloc] initWithData:signingBlockData encoding:NSUTF8StringEncoding]; if (signingBlockString == nil) { return appcastData; } __block NSString *edSignatureBase64 = nil; __block uint64_t contentLength = 0; static NSString *edSignatureKey = @"edSignature:"; static NSString *lengthKey = @"length:"; [signingBlockString enumerateLinesUsingBlock:^(NSString * _Nonnull line, BOOL * _Nonnull __unused stop) { if ([line hasPrefix:edSignatureKey]) { edSignatureBase64 = [[line substringFromIndex:edSignatureKey.length] stringByTrimmingCharactersInSet:NSCharacterSet.whitespaceAndNewlineCharacterSet]; } else if ([line hasPrefix:lengthKey]) { contentLength = (uint64_t)[[[line substringFromIndex:lengthKey.length] stringByTrimmingCharactersInSet:NSCharacterSet.whitespaceAndNewlineCharacterSet] longLongValue]; } }]; if (outEdSignatureBase64 != nil) { *outEdSignatureBase64 = [edSignatureBase64 copy]; } if (outContentLength != NULL) { *outContentLength = contentLength; } return contentFeedData; } NSData *SPUExtractReleaseNotesContent(NSData *data) { NSData *signWarningCommentPrefix = [@"" dataUsingEncoding:NSUTF8StringEncoding]; if (signWarningCommentPrefix.length == 0 || signWarningComment.length == 0) { return data; } if (data.length < signWarningCommentPrefix.length + signWarningComment.length) { return data; } if (![[data subdataWithRange:NSMakeRange(0, signWarningCommentPrefix.length)] isEqualToData:signWarningCommentPrefix]) { return data; } NSRange commentSuffixRange = [data rangeOfData:signWarningComment options:(NSDataSearchOptions)0 range:NSMakeRange(signWarningCommentPrefix.length, data.length - signWarningCommentPrefix.length)]; if (commentSuffixRange.location == NSNotFound) { return data; } // A newline is usually inserted after the signing warning comment // Ignore that character too if present NSUInteger endOfCommentSuffix = NSMaxRange(commentSuffixRange); NSUInteger endOfCommentSuffixAccountingForNewline = (data.length > endOfCommentSuffix && *((const uint8_t *)data.bytes + endOfCommentSuffix) == '\n') ? (endOfCommentSuffix + 1) : endOfCommentSuffix; NSData *contentData = [data subdataWithRange:NSMakeRange(endOfCommentSuffixAccountingForNewline, data.length - endOfCommentSuffixAccountingForNewline)]; return contentData; } ================================================ FILE: Sparkle/SPUGentleUserDriverReminders.h ================================================ // // SPUGentleUserDriverReminders.h // Sparkle // // Copyright © 2022 Sparkle Project. All rights reserved. // #ifndef SPUGentleUserDriverReminders_h #define SPUGentleUserDriverReminders_h /** A private protocol for user drivers implementing gentle scheduled reminders */ @protocol SPUGentleUserDriverReminders - (void)logGentleScheduledUpdateReminderWarningIfNeeded; - (void)resetTimeSinceOpportuneUpdateNotice; @end #endif /* SPUGentleUserDriverReminders_h */ ================================================ FILE: Sparkle/SPUInformationalUpdate.h ================================================ // // SPUInformationalUpdate.h // Sparkle // // Created by Mayur Pawashe on 1/8/17. // Copyright © 2017 Sparkle Project. All rights reserved. // #import #import "SPUResumableUpdate.h" NS_ASSUME_NONNULL_BEGIN SPU_OBJC_DIRECT_MEMBERS @interface SPUInformationalUpdate : NSObject - (instancetype)initWithAppcastItem:(SUAppcastItem *)updateItem secondaryAppcastItem:(SUAppcastItem * _Nullable)secondaryUpdateItem; @end NS_ASSUME_NONNULL_END ================================================ FILE: Sparkle/SPUInformationalUpdate.m ================================================ // // SPUInformationalUpdate.m // Sparkle // // Created by Mayur Pawashe on 1/8/17. // Copyright © 2017 Sparkle Project. All rights reserved. // #import "SPUInformationalUpdate.h" #include "AppKitPrevention.h" @implementation SPUInformationalUpdate // If we ever enable auto-synthesize in the future, we'll still need this synthesize // because the property is declared in a protocol @synthesize updateItem = _updateItem; @synthesize secondaryUpdateItem = _secondaryUpdateItem; - (instancetype)initWithAppcastItem:(SUAppcastItem *)updateItem secondaryAppcastItem:(SUAppcastItem * _Nullable)secondaryUpdateItem { self = [super init]; if (self != nil) { _updateItem = updateItem; _secondaryUpdateItem = secondaryUpdateItem; } return self; } @end ================================================ FILE: Sparkle/SPUInstallationType.h ================================================ // // SPUInstallationType.h // Sparkle // // Created by Mayur Pawashe on 7/24/16. // Copyright © 2016 Sparkle Project. All rights reserved. // #ifndef SPUInstallationType_h #define SPUInstallationType_h #define SPUInstallationTypeApplication @"application" // the default installation type for ordinary application updates #define SPUInstallationTypeGuidedPackage @"package" // the preferred installation type for package installations #define SPUInstallationTypeInteractivePackage @"interactive-package" // removed installation type; use guided package instead #define SPUInstallationTypesArray (@[SPUInstallationTypeApplication, SPUInstallationTypeGuidedPackage]) #define SPUValidInstallationType(x) ((x != nil) && [SPUInstallationTypesArray containsObject:(NSString * _Nonnull)x]) #endif /* SPUInstallationType_h */ ================================================ FILE: Sparkle/SPUInstallerDriver.h ================================================ // // SPUInstallerDriver.h // Sparkle // // Created by Mayur Pawashe on 3/17/16. // Copyright © 2016 Sparkle Project. All rights reserved. // #import NS_ASSUME_NONNULL_BEGIN @protocol SPUUpdaterDelegate; @class SUHost, SUAppcastItem, SPUDownloadedUpdate; @protocol SPUInstallerDriverDelegate - (void)installerDidStartInstallingWithApplicationTerminated:(BOOL)applicationTerminated; - (void)installerDidStartExtracting; - (void)installerDidExtractUpdateWithProgress:(double)progress; - (void)installerDidFinishPreparationAndWillInstallImmediately:(BOOL)willInstallImmediately; - (void)installerWillFinishInstallationAndRelaunch:(BOOL)relaunch; - (void)installerDidFinishInstallationAndRelaunched:(BOOL)relaunch acknowledgement:(void(^)(void))acknowledgement; - (void)installerIsRequestingAbortInstallWithError:(nullable NSError *)error; - (void)installerDidFailToApplyDeltaUpdate; @end SPU_OBJC_DIRECT_MEMBERS @interface SPUInstallerDriver : NSObject - (instancetype)initWithHost:(SUHost *)host applicationBundle:(NSBundle *)applicationBundle updater:(id)updater updaterDelegate:(nullable id)updaterDelegate delegate:(nullable id)delegate; - (void)resumeInstallingUpdateWithUpdateItem:(SUAppcastItem *)updateItem systemDomain:(BOOL)systemDomain; - (void)setUpdateWillInstallHandler:(void (^)(void))updateWillInstallHandler; - (void)extractDownloadedUpdate:(SPUDownloadedUpdate *)downloadedUpdate silently:(BOOL)silently completion:(void (^)(NSError * _Nullable))completionHandler; - (void)installWithToolAndRelaunch:(BOOL)relaunch displayingUserInterface:(BOOL)showUI; - (void)cancelUpdate; - (void)abortInstall; @end NS_ASSUME_NONNULL_END ================================================ FILE: Sparkle/SPUInstallerDriver.m ================================================ // // SPUInstallerDriver.m // Sparkle // // Created by Mayur Pawashe on 3/17/16. // Copyright © 2016 Sparkle Project. All rights reserved. // #import "SPUInstallerDriver.h" #import "SULog.h" #import "SPUMessageTypes.h" #import "SPUXPCServiceInfo.h" #import "SPUUpdaterDelegate.h" #import "SUAppcastItem.h" #import "SUAppcastItem+Private.h" #import "SULocalizations.h" #import "SUErrors.h" #import "SUHost.h" #import "SUFileManager.h" #import "SPUSecureCoding.h" #import "SPUInstallationInputData.h" #import "SUInstallerLauncher.h" #import "SUInstallerConnection.h" #import "SUInstallerConnectionProtocol.h" #import "SUXPCInstallerConnection.h" #import "SPUDownloadedUpdate.h" #import "SPUInstallationType.h" #import "SUConstants.h" #import "SPUProbeInstallStatus.h" #include "AppKitPrevention.h" #define FIRST_INSTALLER_MESSAGE_TIMEOUT 7ull #if SPARKLE_BUILD_LEGACY_SUUPDATER @interface NSObject (PrivateDelegateMethods) - (nullable NSString *)_pathToRelaunchForUpdater:(SPUUpdater *)updater; @end #endif // Note: we don't want to directly pull in AppKit here especially if the main application does not need it @interface NSObject (ActivationAPIs) // NSApplication + (id)sharedApplication; - (void)yieldActivationToApplication:(id)application; // NSRunningApplication + (NSArray *)runningApplicationsWithBundleIdentifier:(NSString *)bundleIdentifier; - (NSURL *)bundleURL; @end @interface SPUInstallerDriver () @end @implementation SPUInstallerDriver { SUHost *_host; NSBundle *_applicationBundle; id _installerConnection; SUAppcastItem *_updateItem; NSData *_updateURLBookmarkData; NSError *_installerError; __weak id _updater; __weak id _updaterDelegate; __weak id _delegate; void (^_updateWillInstallHandler)(void); SPUInstallerMessageType _currentStage; NSUInteger _extractionAttempts; BOOL _postponedOnce; BOOL _relaunch; BOOL _systemDomain; BOOL _aborted; BOOL _notifiedDelegateInstallationWillFinish; } - (instancetype)initWithHost:(SUHost *)host applicationBundle:(NSBundle *)applicationBundle updater:(id)updater updaterDelegate:(id)updaterDelegate delegate:(nullable id)delegate { self = [super init]; if (self != nil) { _host = host; _applicationBundle = applicationBundle; _updater = updater; _updaterDelegate = updaterDelegate; _delegate = delegate; } return self; } - (void)setUpdateWillInstallHandler:(void (^)(void))updateWillInstallHandler { _updateWillInstallHandler = [updateWillInstallHandler copy]; } - (void)_reportInstallerError:(nullable NSError *)currentInstallerError genericErrorCode:(NSInteger)genericErrorCode genericUserInfo:(NSDictionary *)genericUserInfo SPU_OBJC_DIRECT { // First see if there is a good custom error we can show // We only check for signing validation errors and installation errors due to not having write permission currently NSError *customError = nil; if (currentInstallerError != nil) { NSError *underlyingError = currentInstallerError.userInfo[NSUnderlyingErrorKey]; if (underlyingError != nil) { if (underlyingError.code == SUValidationError) { NSDictionary *userInfo = @{ NSLocalizedDescriptionKey: SULocalizedStringFromTableInBundle(@"The update is improperly signed and could not be validated. Please try again later or contact the app developer.", SPARKLE_TABLE, SUSparkleBundle(), nil), NSUnderlyingErrorKey: (NSError * _Nonnull)currentInstallerError }; customError = [NSError errorWithDomain:SUSparkleErrorDomain code:SUInstallationError userInfo:userInfo]; } else if (underlyingError.code == SUInstallationError) { NSError *secondUnderlyingError = underlyingError.userInfo[NSUnderlyingErrorKey]; if (secondUnderlyingError != nil && [secondUnderlyingError.domain isEqualToString:NSCocoaErrorDomain] && secondUnderlyingError.code == NSFileWriteNoPermissionError) { // Note: these error strings will only surface for external app updaters like sparkle-cli (i.e, updaters that update other app bundles) #if SPARKLE_COPY_LOCALIZATIONS NSBundle *sparkleBundle = SUSparkleBundle(); #endif NSMutableDictionary *userInfo = [NSMutableDictionary dictionaryWithDictionary:@{ NSLocalizedDescriptionKey: SULocalizedStringFromTableInBundle(@"The installation failed due to not having permission to write the new update.", SPARKLE_TABLE, sparkleBundle, nil), NSUnderlyingErrorKey: (NSError * _Nonnull)currentInstallerError }]; // macOS 13 and later introduce a policy where Gatekeeper can block app modifications if the apps have different Team IDs if (@available(macOS 13, *)) { NSBundle *mainBundle = [NSBundle mainBundle]; if (![mainBundle isEqual:_host.bundle]) { SUHost *mainBundleHost = [[SUHost alloc] initWithBundle:mainBundle]; userInfo[NSLocalizedRecoverySuggestionErrorKey] = [NSString stringWithFormat:SULocalizedStringFromTableInBundle(@"You may need to allow modifications from %1$@ in System Settings under Privacy & Security and App Management to install future updates.", SPARKLE_TABLE, sparkleBundle, nil), mainBundleHost.name]; } } customError = [NSError errorWithDomain:SUSparkleErrorDomain code:SUInstallationWriteNoPermissionError userInfo:userInfo]; } } } } // Otherwise if there's no custom error, then use a generic installer error to show // and keep the underlying error around for logging NSError *installerError; if (customError != nil) { installerError = customError; } else { NSMutableDictionary *userInfo = [NSMutableDictionary dictionaryWithDictionary:genericUserInfo]; if (currentInstallerError != nil) { userInfo[NSUnderlyingErrorKey] = currentInstallerError; } installerError = [NSError errorWithDomain:SUSparkleErrorDomain code:genericErrorCode userInfo:userInfo]; } [_delegate installerIsRequestingAbortInstallWithError:installerError]; } - (void)setUpConnection SPU_OBJC_DIRECT { if (_installerConnection != nil) { return; } NSString *hostBundleIdentifier = _host.bundle.bundleIdentifier; assert(hostBundleIdentifier != nil); BOOL usingInstallerService; #if INSTALLER_CONNECTION_XPC_SERVICE_EMBEDDED if (SPUXPCServiceIsEnabled(SUEnableInstallerConnectionServiceKey)) { _installerConnection = [[SUXPCInstallerConnection alloc] initWithDelegate:self]; usingInstallerService = YES; } else #endif { _installerConnection = [[SUInstallerConnection alloc] initWithDelegate:self remote:NO]; usingInstallerService = NO; } __weak __typeof__(self) weakSelf = self; [_installerConnection setInvalidationHandler:^{ dispatch_async(dispatch_get_main_queue(), ^{ __typeof__(self) strongSelf = weakSelf; if (strongSelf != nil && strongSelf->_installerConnection != nil && !strongSelf->_aborted) { NSString *impactedTools = usingInstallerService ? (@SPARKLE_RELAUNCH_TOOL_NAME" and "@INSTALLER_LAUNCHER_NAME) : @SPARKLE_RELAUNCH_TOOL_NAME; NSString *additionalFailureReason; { NSString *executableFailureReason; if (!SPUHelperHasExecutablePermission(@SPARKLE_RELAUNCH_TOOL_NAME, &executableFailureReason) || !SPUHelperHasExecutablePermission(@SPARKLE_INSTALLER_PROGRESS_TOOL_NAME@".app/Contents/MacOS/"@SPARKLE_INSTALLER_PROGRESS_TOOL_NAME, &executableFailureReason)) { additionalFailureReason = executableFailureReason; } else { additionalFailureReason = [NSString stringWithFormat:@"If your application is sandboxed, please ensure Installer Connection & Status entitlements are correctly set up: https://sparkle-project.org/documentation/sandboxing/ . Otherwise if %@ %@ not adhoc signed, your app must be signed with a matching team ID", impactedTools, (usingInstallerService ? @"are" : @"is")]; } } NSDictionary *genericUserInfo = @{ NSLocalizedDescriptionKey: SULocalizedStringFromTableInBundle(@"An error occurred while running the updater. Please try again later.", SPARKLE_TABLE, SUSparkleBundle(), nil), NSLocalizedFailureReasonErrorKey:[NSString stringWithFormat:@"The remote port connection was invalidated from the updater. %@. For additional details, check Console logs for %@", additionalFailureReason, impactedTools] }; [strongSelf _reportInstallerError:strongSelf->_installerError genericErrorCode:SUInstallationError genericUserInfo:genericUserInfo]; } }); }]; NSString *serviceName = SPUInstallerServiceNameForBundleIdentifier(hostBundleIdentifier); NSString *installationType = _updateItem.installationType; assert(installationType != nil); [_installerConnection setServiceName:serviceName systemDomain:_systemDomain]; } // This can be called multiple times (eg: if a delta update fails, this may be called again with a regular update item) - (void)extractDownloadedUpdate:(SPUDownloadedUpdate *)downloadedUpdate silently:(BOOL)silently completion:(void (^)(NSError * _Nullable))completionHandler { _updateItem = downloadedUpdate.updateItem; _updateURLBookmarkData = downloadedUpdate.downloadBookmarkData; _currentStage = SPUInstallerNotStarted; if (_installerConnection == nil) { [self launchAutoUpdateSilently:silently completion:completionHandler]; } else { // The Install tool is already alive; just send out installation input data again [self sendInstallationData]; completionHandler(nil); } } - (void)resumeInstallingUpdateWithUpdateItem:(SUAppcastItem *)updateItem systemDomain:(BOOL)systemDomain { _updateItem = updateItem; _systemDomain = systemDomain; } - (void)sendInstallationData SPU_OBJC_DIRECT { NSString *pathToRelaunch = _applicationBundle.bundlePath; id updaterDelegate = _updaterDelegate; id updater = _updater; #if SPARKLE_BUILD_LEGACY_SUUPDATER // Give the delegate one more chance for determining the path to relaunch via a private API used by SUUpdater if (updater != nil && [updaterDelegate respondsToSelector:@selector(_pathToRelaunchForUpdater:)]) { NSString *relaunchPath = [(NSObject *)updaterDelegate _pathToRelaunchForUpdater:updater]; if (relaunchPath != nil) { pathToRelaunch = relaunchPath; } } #endif NSString *decryptionPassword = nil; if (updater != nil && [updaterDelegate respondsToSelector:@selector(decryptionPasswordForUpdater:)]) { decryptionPassword = [updaterDelegate decryptionPasswordForUpdater:updater]; } id delegate = _delegate; SPUInstallationInputData *installationData = [[SPUInstallationInputData alloc] initWithRelaunchPath:pathToRelaunch hostBundlePath:_host.bundlePath updateURLBookmarkData:_updateURLBookmarkData installationType:_updateItem.installationType signatures:_updateItem.signatures decryptionPassword:decryptionPassword expectedVersion:_updateItem.versionString expectedContentLength:_updateItem.contentLength]; NSData *archivedData = SPUArchiveRootObjectSecurely(installationData); if (archivedData == nil) { [delegate installerIsRequestingAbortInstallWithError:[NSError errorWithDomain:SUSparkleErrorDomain code:SUInstallationError userInfo:@{ NSLocalizedDescriptionKey:@"An error occurred while encoding the installer parameters. Please try again later." }]]; return; } [_installerConnection handleMessageWithIdentifier:SPUInstallationData data:archivedData]; _currentStage = SPUInstallerNotStarted; // If the number of extractions attempts stays the same, then we've waited too long and should abort the installation // The extraction attempts is incremented when we receive an extraction should start message from the installer // This also handles the case when a delta extraction fails and tries to re-try another extraction attempt later // We will also want to make sure current stage is still SUInstallerNotStarted because it may not be due to resumability NSUInteger currentExtractionAttempts = _extractionAttempts; __weak __typeof__(self) weakSelf = self; dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(FIRST_INSTALLER_MESSAGE_TIMEOUT * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ __typeof__(self) strongSelf = weakSelf; if (strongSelf != nil && strongSelf->_currentStage == SPUInstallerNotStarted && currentExtractionAttempts == strongSelf->_extractionAttempts) { SULog(SULogLevelError, @"Timeout: Installer never started archive extraction"); [strongSelf->_delegate installerIsRequestingAbortInstallWithError:[NSError errorWithDomain:SUSparkleErrorDomain code:SUInstallationError userInfo:@{ NSLocalizedDescriptionKey:SULocalizedStringFromTableInBundle(@"An error occurred while starting the installer. Please try again later.", SPARKLE_TABLE, SUSparkleBundle(), nil) }]]; } }); } - (void)handleMessageWithIdentifier:(int32_t)identifier data:(NSData *)data { dispatch_async(dispatch_get_main_queue(), ^{ [self _handleMessageWithIdentifier:identifier data:data]; }); } - (void)_handleMessageWithIdentifier:(int32_t)identifier data:(NSData *)data SPU_OBJC_DIRECT { if (!SPUInstallerMessageTypeIsLegal(_currentStage, (SPUInstallerMessageType)identifier)) { SULog(SULogLevelError, @"Error: received out of order message with current stage: %d, requested stage: %d", _currentStage, identifier); return; } id delegate = _delegate; if (identifier == SPUExtractionStarted) { _extractionAttempts++; _currentStage = (SPUInstallerMessageType)identifier; [delegate installerDidStartExtracting]; } else if (identifier == SPUExtractedArchiveWithProgress) { if (data.length == sizeof(double) && sizeof(double) == sizeof(uint64_t)) { uint64_t progressValue = CFSwapInt64LittleToHost(*(const uint64_t *)data.bytes); double progress = *(double *)&progressValue; [delegate installerDidExtractUpdateWithProgress:progress]; _currentStage = (SPUInstallerMessageType)identifier; } } else if (identifier == SPUArchiveExtractionFailed) { // If this is a delta update, there must be a regular update we can fall back to if ([_updateItem isDeltaUpdate]) { [delegate installerDidFailToApplyDeltaUpdate]; } else { // Don't have to store current stage because we're going to abort NSDictionary *genericUserInfo = @{ NSLocalizedDescriptionKey:SULocalizedStringFromTableInBundle(@"An error occurred while extracting the archive. Please try again later.", SPARKLE_TABLE, SUSparkleBundle(), nil) }; NSError *unarchivedError = (NSError *)SPUUnarchiveRootObjectSecurely(data, [NSError class]); [self _reportInstallerError:unarchivedError genericErrorCode:SUUnarchivingError genericUserInfo:genericUserInfo]; } } else if (identifier == SPUValidationStarted) { _currentStage = (SPUInstallerMessageType)identifier; } else if (identifier == SPUInstallationStartedStage1) { _currentStage = (SPUInstallerMessageType)identifier; } else if (identifier == SPUInstallationFinishedStage1) { _currentStage = (SPUInstallerMessageType)identifier; // Let the installer keep a copy of the appcast item data // We may want to ask for it later (note the updater can relaunch without the app necessarily having relaunched) NSData *updateItemData = SPUArchiveRootObjectSecurely(_updateItem); if (updateItemData != nil) { [_installerConnection handleMessageWithIdentifier:SPUSentUpdateAppcastItemData data:updateItemData]; } else { SULog(SULogLevelError, @"Error: Archived data to send for appcast item is nil"); } BOOL hasTargetTerminated = NO; if (data.length >= sizeof(uint8_t)) { hasTargetTerminated = (BOOL)*((const uint8_t *)data.bytes); } [delegate installerDidFinishPreparationAndWillInstallImmediately:hasTargetTerminated]; } else if (identifier == SPUInstallationFinishedStage2) { _currentStage = (SPUInstallerMessageType)identifier; BOOL hasTargetTerminated = NO; if (data.length >= sizeof(uint8_t)) { hasTargetTerminated = (BOOL)*((const uint8_t *)data.bytes); } // If the target was already terminated this may be the first time we notify delegate that installation is about to happen // Otherwise if the target was requested to be terminated/relaunched by the user this may be the second time // Avoid re-notifying the delegate twice if (!_notifiedDelegateInstallationWillFinish) { _notifiedDelegateInstallationWillFinish = YES; [delegate installerWillFinishInstallationAndRelaunch:_relaunch]; } [delegate installerDidStartInstallingWithApplicationTerminated:hasTargetTerminated]; } else if (identifier == SPUInstallationFinishedStage3) { _currentStage = (SPUInstallerMessageType)identifier; [_installerConnection invalidate]; _installerConnection = nil; [delegate installerDidFinishInstallationAndRelaunched:_relaunch acknowledgement:^{ dispatch_async(dispatch_get_main_queue(), ^{ [delegate installerIsRequestingAbortInstallWithError:nil]; }); }]; } else if (identifier == SPUUpdaterAlivePing) { // Don't update the current stage; a ping request has no effect on that. [_installerConnection handleMessageWithIdentifier:SPUUpdaterAlivePong data:[NSData data]]; } else if (identifier == SPUInstallerError) { // Don't update the current stage; an installation error has no effect on that. _installerError = (NSError *)SPUUnarchiveRootObjectSecurely(data, [NSError class]); } } - (void)launchAutoUpdateSilently:(BOOL)silently completion:(void (^)(NSError *_Nullable))completionHandler SPU_OBJC_DIRECT { id installerLauncher; #if INSTALLER_LAUNCHER_XPC_SERVICE_EMBEDDED __block BOOL retrievedLaunchStatus = NO; NSXPCConnection *launcherConnection = nil; if (SPUXPCServiceIsEnabled(SUEnableInstallerLauncherServiceKey)) { launcherConnection = [[NSXPCConnection alloc] initWithServiceName:@INSTALLER_LAUNCHER_BUNDLE_ID]; launcherConnection.remoteObjectInterface = [NSXPCInterface interfaceWithProtocol:@protocol(SUInstallerLauncherProtocol)]; launcherConnection.interruptionHandler = ^{ dispatch_async(dispatch_get_main_queue(), ^{ if (!retrievedLaunchStatus) { // We'll break the retain cycle in the invalidation handler #pragma clang diagnostic push #pragma clang diagnostic ignored "-Warc-retain-cycles" [launcherConnection invalidate]; #pragma clang diagnostic pop } }); }; launcherConnection.invalidationHandler = ^{ dispatch_async(dispatch_get_main_queue(), ^{ #pragma clang diagnostic push #if __has_warning("-Wcompletion-handler") #pragma clang diagnostic ignored "-Wcompletion-handler" #endif if (!retrievedLaunchStatus) { #pragma clang diagnostic pop NSString *additionalFailureReason; { NSString *executableFailureReason; if (!SPUXPCServiceHasExecutablePermission(@INSTALLER_LAUNCHER_NAME, &executableFailureReason)) { additionalFailureReason = [NSString stringWithFormat:@" %@", executableFailureReason]; } else { additionalFailureReason = @""; } } NSError *error = [NSError errorWithDomain:SUSparkleErrorDomain code:SUInstallationError userInfo:@{ NSLocalizedDescriptionKey:SULocalizedStringFromTableInBundle(@"An error occurred while connecting to the installer. Please try again later.", SPARKLE_TABLE, SUSparkleBundle(), nil), NSLocalizedFailureReasonErrorKey: [NSString stringWithFormat:@"If your app is not sandboxed, please remove or disable %@ in your app's Info.plist. Please also check Console logs for "@INSTALLER_LAUNCHER_NAME" and "@SPARKLE_RELAUNCH_TOOL_NAME" processes if there are additional details.%@", SUEnableInstallerLauncherServiceKey, additionalFailureReason]}]; completionHandler(error); // Break the retain cycle #pragma clang diagnostic push #pragma clang diagnostic ignored "-Warc-retain-cycles" launcherConnection.interruptionHandler = nil; launcherConnection.invalidationHandler = nil; #pragma clang diagnostic pop } }); }; [launcherConnection resume]; installerLauncher = launcherConnection.remoteObjectProxy; } else #endif { installerLauncher = [[SUInstallerLauncher alloc] init]; } // Our driver (automatic or UI based) has a say if interaction is allowed as well // An automatic driver may disallow interaction but the updater could try again later for a UI based driver that does allow interaction BOOL driverAllowsInteraction = !silently; NSString *hostBundlePath = _host.bundle.bundlePath; assert(hostBundlePath != nil); NSString *hostBundleIdentifier = _host.bundle.bundleIdentifier; NSString *installationType = _updateItem.installationType; assert(installationType != nil); [installerLauncher launchInstallerWithHostBundlePath:hostBundlePath mainBundlePath:NSBundle.mainBundle.bundlePath installationType:installationType allowingDriverInteraction:driverAllowsInteraction completion:^(SUInstallerLauncherStatus result, BOOL systemDomain) { dispatch_async(dispatch_get_main_queue(), ^{ #if INSTALLER_LAUNCHER_XPC_SERVICE_EMBEDDED retrievedLaunchStatus = YES; [launcherConnection invalidate]; #endif switch (result) { case SUInstallerLauncherFailure: SULog(SULogLevelError, @"Error: Failed to gain authorization required to update target"); completionHandler([NSError errorWithDomain:SUSparkleErrorDomain code:SUInstallationError userInfo:@{ NSLocalizedDescriptionKey:SULocalizedStringFromTableInBundle(@"An error occurred while launching the installer. Please try again later.", SPARKLE_TABLE, SUSparkleBundle(), nil) }]); break; case SUInstallerLauncherCanceled: completionHandler([NSError errorWithDomain:SUSparkleErrorDomain code:SUInstallationCanceledError userInfo:nil]); break; case SUInstallerLauncherAuthorizeLater: completionHandler([NSError errorWithDomain:SUSparkleErrorDomain code:SUInstallationAuthorizeLaterError userInfo:nil]); break; case SUInstallerLauncherSuccess: self->_systemDomain = systemDomain; [self setUpConnection]; [self sendInstallationData]; // Send a probe/ping to the status service, which should boost/prioritize its startup if (hostBundleIdentifier != nil) { [SPUProbeInstallStatus probeInstallerInProgressForHostBundleIdentifier:hostBundleIdentifier completion:^(BOOL stausServiceIsRunning) { if (!stausServiceIsRunning) { SULog(SULogLevelError, @"Error: failed to probe status service for %@ from the framework", hostBundleIdentifier); } completionHandler(nil); }]; } else { completionHandler(nil); } break; } }); }]; } - (BOOL)mayUpdateAndRestart SPU_OBJC_DIRECT { id updaterDelegate = _updaterDelegate; return (!updaterDelegate || ![updaterDelegate respondsToSelector:@selector((updaterShouldRelaunchApplication:))] || [updaterDelegate updaterShouldRelaunchApplication:_updater]); } - (void)installWithToolAndRelaunch:(BOOL)relaunch displayingUserInterface:(BOOL)showUI { assert(_updateItem); id delegate = _delegate; if (![self mayUpdateAndRestart]) { [delegate installerIsRequestingAbortInstallWithError:nil]; return; } // Give the host app an opportunity to postpone the install and relaunch. if (!_postponedOnce) { id updater = _updater; id updaterDelegate = _updaterDelegate; if (updater != nil && [updaterDelegate respondsToSelector:@selector(updater:shouldPostponeRelaunchForUpdate:untilInvokingBlock:)]) { _postponedOnce = YES; __weak __typeof__(self) weakSelf = self; if ([updaterDelegate updater:updater shouldPostponeRelaunchForUpdate:_updateItem untilInvokingBlock:^{ [weakSelf installWithToolAndRelaunch:relaunch displayingUserInterface:showUI]; }]) { return; } } } if (_updateWillInstallHandler != NULL) { _updateWillInstallHandler(); } // Set up connection to the installer if one is not set up already [self setUpConnection]; // For resumability, we'll assume we are far enough for the installation to continue _currentStage = SPUInstallationFinishedStage1; _relaunch = relaunch; // If AppKit is loaded, we will yield to our Sparkle progress app // This will let the system know it should be okay for the progress agent to activate itself (if necessary) // Note we don't want to directly pull in AppKit here especially if the main application does not need it if (showUI) { if (@available(macOS 14, *)) { // Make sure we are not root before using AppKit API if (geteuid() != 0) { Class applicationClass = NSClassFromString(@"NSApplication"); if ([applicationClass respondsToSelector:@selector(sharedApplication)]) { NSObject *application = [applicationClass sharedApplication]; if ([application respondsToSelector:@selector(yieldActivationToApplication:)]) { Class runningApplicationClass = NSClassFromString(@"NSRunningApplication"); if ([runningApplicationClass respondsToSelector:@selector(runningApplicationsWithBundleIdentifier:)]) { NSArray *runningApplications = [runningApplicationClass runningApplicationsWithBundleIdentifier:@SPARKLE_INSTALLER_PROGRESS_TOOL_BUNDLE_ID]; NSString *hostBundleIdentifier = _host.bundle.bundleIdentifier; id targetRunningApplication = nil; for (id runningApplication in runningApplications) { if ([(NSObject *)runningApplication respondsToSelector:@selector(bundleURL)]) { NSURL *bundleURL = [(NSObject *)runningApplication bundleURL]; if (hostBundleIdentifier != nil && [bundleURL.pathComponents containsObject:hostBundleIdentifier]) { targetRunningApplication = runningApplication; break; } } } if (targetRunningApplication != nil) { [application yieldActivationToApplication:targetRunningApplication]; } } } } } } } // The user can request trying to relaunch/quit the app multiple times // Avoid re-notifying the delegate twice if (!_notifiedDelegateInstallationWillFinish) { _notifiedDelegateInstallationWillFinish = YES; [delegate installerWillFinishInstallationAndRelaunch:relaunch]; } uint8_t response[2] = {(uint8_t)relaunch, (uint8_t)showUI}; NSData *responseData = [NSData dataWithBytes:response length:sizeof(response)]; // the installer will send us SPUInstallationFinishedStage2 when stage 2 is done [_installerConnection handleMessageWithIdentifier:SPUResumeInstallationToStage2 data:responseData]; } - (void)cancelUpdate { // Set up connection to the installer if one is not set up already [self setUpConnection]; _aborted = YES; [_installerConnection handleMessageWithIdentifier:SPUCancelInstallation data:[NSData data]]; [_delegate installerIsRequestingAbortInstallWithError:nil]; } - (void)abortInstall { _aborted = YES; if (_installerConnection != nil) { [_installerConnection invalidate]; _installerConnection = nil; } } @end ================================================ FILE: Sparkle/SPULocalCacheDirectory.h ================================================ // // SULocalCacheDirectory.h // Sparkle // // Created by Mayur Pawashe on 6/23/16. // Copyright © 2016 Sparkle Project. All rights reserved. // #import NS_ASSUME_NONNULL_BEGIN SPU_OBJC_DIRECT_MEMBERS @interface SPULocalCacheDirectory : NSObject // Returns a path to a suitable cache directory to create specifically for Sparkle // Intermediate directories to this path may not exist yet // This path may depend on the type of running process, // such that sandboxed vs non-sandboxed processes could yield different paths // The caller should create a subdirectory from the path that is returned here so they don't have files that // conflict with other callers. Once that subdirectory name is decided, the caller can remove old items inside it (using +removeOldItemsInDirectory:) // and then create a unique temporary directory inside it (using +createUniqueDirectoryInDirectory:) + (NSString *)cachePathForBundleIdentifier:(NSString *)bundleIdentifier; // Variant of cachePathForBundleIdentifier: that specifies a userName to create the cache path for // Only use this when running from as root + (NSString *)cachePathForBundleIdentifier:(NSString *)bundleIdentifier userName:(NSString *)userName; // Remove old files inside a directory // A caller may want to invoke this on a directory they own rather than remove and re-create an entire directory // This does nothing if the supplied directory does not exist yet + (void)removeOldItemsInDirectory:(NSString *)directory; // Create a unique directory inside a parent directory // The parent directory doesn't have to exist yet. If it doesn't exist, intermediate directories will be created. + (NSString * _Nullable)createUniqueDirectoryInDirectory:(NSString *)directory; + (NSString * _Nullable)createUniqueDirectoryInDirectory:(NSString *)directory intermediateDirectoryFileAttributes:(nullable NSDictionary *)attributes; @end NS_ASSUME_NONNULL_END ================================================ FILE: Sparkle/SPULocalCacheDirectory.m ================================================ // // SULocalCacheDirectory.m // Sparkle // // Created by Mayur Pawashe on 6/23/16. // Copyright © 2016 Sparkle Project. All rights reserved. // #import "SPULocalCacheDirectory.h" #import "SULog.h" #include "AppKitPrevention.h" static NSTimeInterval OLD_ITEM_DELETION_INTERVAL = 86400 * 10; // 10 days @implementation SPULocalCacheDirectory + (NSString *)_cachePathForCacheDirectory:(NSURL *)cacheURL bundleIdentifier:(NSString *)bundleIdentifier SPU_OBJC_DIRECT { NSString *resultPath = [[[cacheURL URLByAppendingPathComponent:bundleIdentifier isDirectory:YES] URLByAppendingPathComponent:@SPARKLE_BUNDLE_IDENTIFIER isDirectory:YES] path]; assert(resultPath != nil); return resultPath; } // It is important to note this may return a different path whether invoked from a sanboxed vs non-sandboxed process // For this reason, this method should not be a part of SUHost because its behavior depends on what kind of process it's being invoked from + (NSString *)cachePathForBundleIdentifier:(NSString *)bundleIdentifier { NSURL *cacheURL = [[NSFileManager defaultManager] URLForDirectory:NSCachesDirectory inDomain:NSUserDomainMask appropriateForURL:nil create:NO error:NULL]; assert(cacheURL != nil); return [self _cachePathForCacheDirectory:cacheURL bundleIdentifier:bundleIdentifier]; } + (NSString *)cachePathForBundleIdentifier:(NSString *)bundleIdentifier userName:(NSString *)userName { NSString *homeDirectory = NSHomeDirectoryForUser(userName); assert(homeDirectory != nil); NSURL *homeDirectoryURL = [NSURL fileURLWithPath:homeDirectory isDirectory:YES]; NSURL *cacheURL = [[homeDirectoryURL URLByAppendingPathComponent:@"Library" isDirectory:YES] URLByAppendingPathComponent:@"Caches" isDirectory:YES]; assert(cacheURL != nil); return [self _cachePathForCacheDirectory:cacheURL bundleIdentifier:bundleIdentifier]; } + (void)removeOldItemsInDirectory:(NSString *)directory { NSMutableArray *filePathsToRemove = [NSMutableArray array]; NSFileManager *fileManager = [NSFileManager defaultManager]; if ([fileManager fileExistsAtPath:directory]) { NSDirectoryEnumerator *directoryEnumerator = [fileManager enumeratorAtPath:directory]; NSDate *currentDate = [NSDate date]; for (NSString *filename in directoryEnumerator) { NSDictionary *fileAttributes = [fileManager attributesOfItemAtPath:[directory stringByAppendingPathComponent:filename] error:NULL]; if (fileAttributes != nil) { NSDate *lastModificationDate = [fileAttributes objectForKey:NSFileModificationDate]; if ([currentDate timeIntervalSinceDate:lastModificationDate] >= OLD_ITEM_DELETION_INTERVAL) { [filePathsToRemove addObject:[directory stringByAppendingPathComponent:filename]]; } } [directoryEnumerator skipDescendants]; } for (NSString *filename in filePathsToRemove) { [fileManager removeItemAtPath:filename error:NULL]; } } } + (NSString * _Nullable)createUniqueDirectoryInDirectory:(NSString *)directory intermediateDirectoryFileAttributes:(NSDictionary *)intermediateDirectoryFileAttributes { NSFileManager *fileManager = [NSFileManager defaultManager]; NSError *createError = nil; if (![fileManager createDirectoryAtPath:directory withIntermediateDirectories:YES attributes:intermediateDirectoryFileAttributes error:&createError]) { SULog(SULogLevelError, @"Failed to create directory with intermediate components at %@ with error %@", directory, createError); return nil; } NSString *templateString = [directory stringByAppendingPathComponent:@"XXXXXXXXX"]; char buffer[PATH_MAX] = {0}; if ([templateString getFileSystemRepresentation:buffer maxLength:sizeof(buffer)]) { if (mkdtemp(buffer) != NULL) { return [[NSString alloc] initWithUTF8String:buffer]; } } return nil; } + (NSString * _Nullable)createUniqueDirectoryInDirectory:(NSString *)directory { return [self createUniqueDirectoryInDirectory:directory intermediateDirectoryFileAttributes:nil]; } @end ================================================ FILE: Sparkle/SPUNoUpdateFoundInfo.h ================================================ // // SPUNoUpdateFoundInfo.h // Sparkle // // Created on 2/18/23. // Copyright © 2023 Sparkle Project. All rights reserved. // #import #import "SUVersionDisplayProtocol.h" #import "SUErrors.h" @class SUAppcastItem; @class SUHost; NS_ASSUME_NONNULL_BEGIN NSString *SPUNoUpdateFoundRecoverySuggestion(SPUNoUpdateFoundReason reason, SUAppcastItem * _Nullable latestAppcastItem, SUHost *host, id versionDisplayer, NSBundle * _Nullable sparkleBundle); NS_ASSUME_NONNULL_END ================================================ FILE: Sparkle/SPUNoUpdateFoundInfo.m ================================================ // // SPUNoUpdateFoundInfo.m // Sparkle // // Created on 2/18/23. // Copyright © 2023 Sparkle Project. All rights reserved. // #import "SPUNoUpdateFoundInfo.h" #import "SUAppcastItem.h" #import "SUHost.h" #import "SULocalizations.h" #import "SUlog.h" #include "AppKitPrevention.h" NSString *SPUNoUpdateFoundRecoverySuggestion(SPUNoUpdateFoundReason reason, SUAppcastItem *latestAppcastItem, SUHost *host, id versionDisplayer, NSBundle *sparkleBundle) { #if !SPARKLE_COPY_LOCALIZATIONS (void)sparkleBundle; #endif NSString *hostDisplayVersion; NSString *latestAppcastItemDisplayVersion; switch (reason) { case SPUNoUpdateFoundReasonUnknown: case SPUNoUpdateFoundReasonOnLatestVersion: if ([versionDisplayer respondsToSelector:@selector(formatBundleDisplayVersion:withBundleVersion:matchingUpdate:)]) { hostDisplayVersion = [versionDisplayer formatBundleDisplayVersion:host.displayVersion withBundleVersion:host.version matchingUpdate:latestAppcastItem]; } else { hostDisplayVersion = host.displayVersion; } // This is not later used latestAppcastItemDisplayVersion = nil; break; case SPUNoUpdateFoundReasonOnNewerThanLatestVersion: case SPUNoUpdateFoundReasonSystemIsTooOld: case SPUNoUpdateFoundReasonSystemIsTooNew: case SPUNoUpdateFoundReasonHardwareDoesNotSupportARM64: assert(latestAppcastItem != nil); hostDisplayVersion = host.displayVersion; if ([versionDisplayer respondsToSelector:@selector(formatUpdateDisplayVersionFromUpdate:andBundleDisplayVersion:withBundleVersion:)]) { latestAppcastItemDisplayVersion = [versionDisplayer formatUpdateDisplayVersionFromUpdate:latestAppcastItem andBundleDisplayVersion:&hostDisplayVersion withBundleVersion:host.version]; } else { // Legacy -formatVersion:andVersion: was never supported for this path so we don't need to call it latestAppcastItemDisplayVersion = latestAppcastItem.displayVersionString; } break; } NSString *recoverySuggestion; switch (reason) { case SPUNoUpdateFoundReasonUnknown: case SPUNoUpdateFoundReasonOnLatestVersion: recoverySuggestion = [NSString stringWithFormat:SULocalizedStringFromTableInBundle(@"%@ %@ is currently the newest version available.", SPARKLE_TABLE, sparkleBundle, nil), host.name, hostDisplayVersion]; break; case SPUNoUpdateFoundReasonOnNewerThanLatestVersion: recoverySuggestion = [NSString stringWithFormat:SULocalizedStringFromTableInBundle(@"%@ %@ is currently the newest version available.\n(You are currently running version %@.)", SPARKLE_TABLE, sparkleBundle, nil), host.name, latestAppcastItemDisplayVersion, hostDisplayVersion]; break; case SPUNoUpdateFoundReasonSystemIsTooOld: recoverySuggestion = [NSString stringWithFormat:SULocalizedStringFromTableInBundle(@"%1$@ %2$@ is available but your macOS version is too old to install it. At least macOS %3$@ is required.", SPARKLE_TABLE, sparkleBundle, nil), host.name, latestAppcastItemDisplayVersion, latestAppcastItem.minimumSystemVersion]; break; case SPUNoUpdateFoundReasonSystemIsTooNew: recoverySuggestion = [NSString stringWithFormat:SULocalizedStringFromTableInBundle(@"%1$@ %2$@ is available but your macOS version is too new for this update. This update only supports up to macOS %3$@.", SPARKLE_TABLE, sparkleBundle, nil), host.name, latestAppcastItemDisplayVersion, latestAppcastItem.maximumSystemVersion]; break; case SPUNoUpdateFoundReasonHardwareDoesNotSupportARM64: recoverySuggestion = [NSString stringWithFormat:SULocalizedStringFromTableInBundle(@"%1$@ %2$@ is available but this update requires a new Apple silicon Mac.", SPARKLE_TABLE, sparkleBundle, nil), host.name, latestAppcastItemDisplayVersion]; break; } return recoverySuggestion; } ================================================ FILE: Sparkle/SPUProbeInstallStatus.h ================================================ // // SPUProbeInstallStatus.h // Sparkle // // Created by Mayur Pawashe on 3/20/16. // Copyright © 2016 Sparkle Project. All rights reserved. // #import NS_ASSUME_NONNULL_BEGIN @class SPUInstallationInfo; SPU_OBJC_DIRECT_MEMBERS @interface SPUProbeInstallStatus : NSObject + (void)probeInstallerInProgressForHostBundleIdentifier:(NSString *)hostBundleIdentifier completion:(void (^)(BOOL))completionHandler; // completionHandler may not be sent on main queue // additionally, it may be possible that the installer is in progress but we get a nil installation info back + (void)probeInstallerUpdateItemForHostBundleIdentifier:(NSString *)hostBundleIdentifier completion:(void (^)(SPUInstallationInfo * _Nullable))completionHandler; @end NS_ASSUME_NONNULL_END ================================================ FILE: Sparkle/SPUProbeInstallStatus.m ================================================ // // SPUProbeInstallStatus.m // Sparkle // // Created by Mayur Pawashe on 3/20/16. // Copyright © 2016 Sparkle Project. All rights reserved. // #import "SPUProbeInstallStatus.h" #import "SPUXPCServiceInfo.h" #import "SPUMessageTypes.h" #import "SPUInstallationInfo.h" #import "SPUSecureCoding.h" #import "SUInstallerStatus.h" #import "SUXPCInstallerStatus.h" #import "SUConstants.h" #import "SULog.h" #include "AppKitPrevention.h" // This timeout is if probing the installer takes too long // It should be at least more than 1 second since a probe can take around that much time #define PROBE_TIMEOUT 7 @implementation SPUProbeInstallStatus + (void)probeInstallerInProgressForHostBundleIdentifier:(NSString *)hostBundleIdentifier completion:(void (^)(BOOL))completionHandler { id installerStatus; #if INSTALLER_STATUS_XPC_SERVICE_EMBEDDED if (SPUXPCServiceIsEnabled(SUEnableInstallerStatusServiceKey)) { installerStatus = [[SUXPCInstallerStatus alloc] init]; } else #endif { installerStatus = [[SUInstallerStatus alloc] initWithRemote:NO]; } __block BOOL handledCompletion = NO; [installerStatus setInvalidationHandler:^{ dispatch_async(dispatch_get_main_queue(), ^{ #pragma clang diagnostic push #if __has_warning("-Wcompletion-handler") #pragma clang diagnostic ignored "-Wcompletion-handler" #endif if (!handledCompletion) { #pragma clang diagnostic pop completionHandler(NO); handledCompletion = YES; } }); }]; NSString *serviceName = SPUStatusInfoServiceNameForBundleIdentifier(hostBundleIdentifier); [installerStatus setServiceName:serviceName]; [installerStatus probeStatusConnectivityWithReply:^{ dispatch_async(dispatch_get_main_queue(), ^{ #pragma clang diagnostic push #if __has_warning("-Wcompletion-handler") #pragma clang diagnostic ignored "-Wcompletion-handler" #endif if (!handledCompletion) { #pragma clang diagnostic pop completionHandler(YES); handledCompletion = YES; } }); [installerStatus invalidate]; }]; #pragma clang diagnostic push #if __has_warning("-Wcompletion-handler") #pragma clang diagnostic ignored "-Wcompletion-handler" #endif dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(PROBE_TIMEOUT * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ if (!handledCompletion) { #pragma clang diagnostic pop SULog(SULogLevelError, @"Timed out while probing installer progress. If your app is sandboxed, please see https://sparkle-project.org/documentation/sandboxing/#testing for the potential cause."); completionHandler(NO); handledCompletion = YES; } [installerStatus invalidate]; }); } + (void)probeInstallerUpdateItemForHostBundleIdentifier:(NSString *)hostBundleIdentifier completion:(void (^)(SPUInstallationInfo * _Nullable))completionHandler { id installerStatus = nil; #if INSTALLER_STATUS_XPC_SERVICE_EMBEDDED if (SPUXPCServiceIsEnabled(SUEnableInstallerStatusServiceKey)) { installerStatus = [[SUXPCInstallerStatus alloc] init]; } else #endif { installerStatus = [[SUInstallerStatus alloc] initWithRemote:NO]; } __block BOOL handledCompletion = NO; [installerStatus setInvalidationHandler:^{ dispatch_async(dispatch_get_main_queue(), ^{ #pragma clang diagnostic push #if __has_warning("-Wcompletion-handler") #pragma clang diagnostic ignored "-Wcompletion-handler" #endif if (!handledCompletion) { #pragma clang diagnostic pop completionHandler(nil); handledCompletion = YES; } }); }]; NSString *serviceName = SPUStatusInfoServiceNameForBundleIdentifier(hostBundleIdentifier); [installerStatus setServiceName:serviceName]; [installerStatus probeStatusInfoWithReply:^(NSData * _Nullable installationInfoData) { SPUInstallationInfo *installationInfo = nil; if (installationInfoData != nil) { installationInfo = (SPUInstallationInfo *)SPUUnarchiveRootObjectSecurely((NSData * _Nonnull)installationInfoData, [SPUInstallationInfo class]); } dispatch_async(dispatch_get_main_queue(), ^{ #pragma clang diagnostic push #if __has_warning("-Wcompletion-handler") #pragma clang diagnostic ignored "-Wcompletion-handler" #endif if (!handledCompletion) { #pragma clang diagnostic pop completionHandler(installationInfo); handledCompletion = YES; } }); [installerStatus invalidate]; }]; #pragma clang diagnostic push #if __has_warning("-Wcompletion-handler") #pragma clang diagnostic ignored "-Wcompletion-handler" #endif dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(PROBE_TIMEOUT * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ if (!handledCompletion) { #pragma clang diagnostic pop SULog(SULogLevelDefault, @"Timed out while probing installer info data. If your app is sandboxed, please see https://sparkle-project.org/documentation/sandboxing/#testing for the potential cause."); completionHandler(nil); handledCompletion = YES; } [installerStatus invalidate]; }); } @end ================================================ FILE: Sparkle/SPUProbingUpdateDriver.h ================================================ // // SPUProbingUpdateDriver.h // Sparkle // // Created by Mayur Pawashe on 3/18/16. // Copyright © 2016 Sparkle Project. All rights reserved. // #import #import "SPUUpdateDriver.h" NS_ASSUME_NONNULL_BEGIN @class SUHost; @protocol SPUUpdaterDelegate; SPU_OBJC_DIRECT_MEMBERS @interface SPUProbingUpdateDriver : NSObject - (instancetype)initWithHost:(SUHost *)host updater:(id)updater updaterDelegate:(nullable id )updaterDelegate; @end NS_ASSUME_NONNULL_END ================================================ FILE: Sparkle/SPUProbingUpdateDriver.m ================================================ // // SPUProbingUpdateDriver.m // Sparkle // // Created by Mayur Pawashe on 3/18/16. // Copyright © 2016 Sparkle Project. All rights reserved. // #import "SPUProbingUpdateDriver.h" #import "SPUBasicUpdateDriver.h" #include "AppKitPrevention.h" @interface SPUProbingUpdateDriver () @end @implementation SPUProbingUpdateDriver { SPUBasicUpdateDriver *_basicDriver; id _resumableUpdate; } - (instancetype)initWithHost:(SUHost *)host updater:(id)updater updaterDelegate:(id )updaterDelegate { self = [super init]; if (self != nil) { _basicDriver = [[SPUBasicUpdateDriver alloc] initWithHost:host updateCheck:SPUUpdateCheckUpdateInformation updater:updater updaterDelegate:updaterDelegate delegate:self]; } return self; } - (void)setCompletionHandler:(SPUUpdateDriverCompletion)completionBlock { [_basicDriver setCompletionHandler:completionBlock]; } - (void)setUpdateShownHandler:(void (^)(void))updateShownHandler { } - (void)setUpdateWillInstallHandler:(void (^)(void))updateWillInstallHandler { } - (void)checkForUpdatesAtAppcastURL:(NSURL *)appcastURL withUserAgent:(NSString *)userAgent httpHeaders:(NSDictionary * _Nullable)httpHeaders { [_basicDriver checkForUpdatesAtAppcastURL:appcastURL withUserAgent:userAgent httpHeaders:httpHeaders inBackground:YES]; } - (void)resumeInstallingUpdate { [_basicDriver resumeInstallingUpdate]; } - (void)resumeUpdate:(id)resumableUpdate { _resumableUpdate = resumableUpdate; [_basicDriver resumeUpdate:resumableUpdate]; } - (void)basicDriverDidFindUpdateWithAppcastItem:(SUAppcastItem *)__unused appcastItem secondaryAppcastItem:(SUAppcastItem * _Nullable)__unused secondaryAppcastItem systemDomain:(NSNumber * _Nullable)__unused systemDomain { // Stop as soon as we have an answer [self abortUpdate]; } - (BOOL)showingUpdate { return NO; } - (void)basicDriverIsRequestingAbortUpdateWithError:(nullable NSError *)error { [self abortUpdateWithError:error]; } - (void)abortUpdate { [self abortUpdateWithError:nil]; } - (void)abortUpdateWithError:(nullable NSError *)error { [_basicDriver abortUpdateAndShowNextUpdateImmediately:NO resumableUpdate:_resumableUpdate error:error]; } @end ================================================ FILE: Sparkle/SPUResumableUpdate.h ================================================ // // SPUResumableUpdate.h // Sparkle // // Created by Mayur Pawashe on 7/18/16. // Copyright © 2016 Sparkle Project. All rights reserved. // #import NS_ASSUME_NONNULL_BEGIN @class SUAppcastItem; @protocol SPUResumableUpdate @property (nonatomic, readonly) SUAppcastItem *updateItem; @property (nonatomic, readonly, nullable) SUAppcastItem *secondaryUpdateItem; @end NS_ASSUME_NONNULL_END ================================================ FILE: Sparkle/SPUScheduledUpdateDriver.h ================================================ // // SPUScheduledUpdateDriver.h // Sparkle // // Created by Mayur Pawashe on 3/15/16. // Copyright © 2016 Sparkle Project. All rights reserved. // #import #import "SPUUpdateDriver.h" #import "SPUUIBasedUpdateDriver.h" NS_ASSUME_NONNULL_BEGIN @class SUHost; @protocol SPUUserDriver, SPUUpdaterDelegate; SPU_OBJC_DIRECT_MEMBERS @interface SPUScheduledUpdateDriver : NSObject - (instancetype)initWithHost:(SUHost *)host applicationBundle:(NSBundle *)applicationBundle updater:(id)updater userDriver:(id )userDriver updaterDelegate:(nullable id )updaterDelegate; @end NS_ASSUME_NONNULL_END ================================================ FILE: Sparkle/SPUScheduledUpdateDriver.m ================================================ // // SPUScheduledUpdateDriver.m // Sparkle // // Created by Mayur Pawashe on 3/15/16. // Copyright © 2016 Sparkle Project. All rights reserved. // #import "SPUScheduledUpdateDriver.h" #import "SUHost.h" #import "SUErrors.h" #import "SPUUpdaterDelegate.h" #import "SPUUserDriver.h" #include "AppKitPrevention.h" @interface SPUScheduledUpdateDriver() @end @implementation SPUScheduledUpdateDriver { SPUUIBasedUpdateDriver *_uiDriver; void (^_updateDidShowHandler)(void); BOOL _showedUpdate; } - (instancetype)initWithHost:(SUHost *)host applicationBundle:(NSBundle *)applicationBundle updater:(id)updater userDriver:(id )userDriver updaterDelegate:(nullable id )updaterDelegate { self = [super init]; if (self != nil) { _uiDriver = [[SPUUIBasedUpdateDriver alloc] initWithHost:host applicationBundle:applicationBundle updater:updater userDriver:userDriver userInitiated:NO updaterDelegate:updaterDelegate delegate:self]; } return self; } - (void)setCompletionHandler:(SPUUpdateDriverCompletion)completionBlock { [_uiDriver setCompletionHandler:completionBlock]; } - (void)setUpdateShownHandler:(void (^)(void))handler { _updateDidShowHandler = [handler copy]; } - (void)setUpdateWillInstallHandler:(void (^)(void))updateWillInstallHandler { [_uiDriver setUpdateWillInstallHandler:updateWillInstallHandler]; } - (void)checkForUpdatesAtAppcastURL:(NSURL *)appcastURL withUserAgent:(NSString *)userAgent httpHeaders:(NSDictionary * _Nullable)httpHeaders { [_uiDriver checkForUpdatesAtAppcastURL:appcastURL withUserAgent:userAgent httpHeaders:httpHeaders inBackground:YES]; } - (void)resumeInstallingUpdate { [_uiDriver resumeInstallingUpdate]; } - (void)resumeUpdate:(id)resumableUpdate { [_uiDriver resumeUpdate:resumableUpdate]; } - (void)uiDriverDidShowUpdate { _showedUpdate = YES; if (_updateDidShowHandler != nil) { _updateDidShowHandler(); } } - (BOOL)showingUpdate { return _showedUpdate; } - (void)basicDriverIsRequestingAbortUpdateWithError:(nullable NSError *) error { [self abortUpdateWithError:error]; } - (void)coreDriverIsRequestingAbortUpdateWithError:(nullable NSError *) error { [self abortUpdateWithError:error]; } - (void)uiDriverIsRequestingAbortUpdateWithError:(nullable NSError *)error { [self abortUpdateWithError:error]; } - (void)abortUpdate { [self abortUpdateWithError:nil]; } - (void)abortUpdateWithError:(nullable NSError *)error { [_uiDriver abortUpdateWithError:error showErrorToUser:_showedUpdate]; } @end ================================================ FILE: Sparkle/SPUSecureCoding.h ================================================ // // SPUSecureCoding.h // Sparkle // // Created by Mayur Pawashe on 3/24/16. // Copyright © 2016 Sparkle Project. All rights reserved. // #import // If we are using XPC without using XPC Services, our custom classes will not be (de)serialized automatically, // hence needing functions to archive/unarchive NSSecureCoding objects. NS_ASSUME_NONNULL_BEGIN NSData * _Nullable SPUArchiveRootObjectSecurely(id rootObject); id _Nullable SPUUnarchiveRootObjectSecurely(NSData *data, Class klass); NS_ASSUME_NONNULL_END ================================================ FILE: Sparkle/SPUSecureCoding.m ================================================ // // SPUSecureCoding.m // Sparkle // // Created by Mayur Pawashe on 3/24/16. // Copyright © 2016 Sparkle Project. All rights reserved. // #import "SPUSecureCoding.h" #import "SULog.h" #include "AppKitPrevention.h" static NSString *SURootObjectArchiveKey = @"SURootObjectArchive"; NSData * _Nullable SPUArchiveRootObjectSecurely(id rootObject) { NSError *error = nil; NSData *data = [NSKeyedArchiver archivedDataWithRootObject:rootObject requiringSecureCoding:YES error:&error]; if (data == nil) { SULog(SULogLevelError, @"Error while securely archiving object: %@", error); } return data; } id _Nullable SPUUnarchiveRootObjectSecurely(NSData *data, Class klass) { NSError *error = nil; id rootObject = [NSKeyedUnarchiver unarchivedObjectOfClass:klass fromData:data error:&error]; if (rootObject == nil) { SULog(SULogLevelError, @"Error while securely unarchiving object: %@", error); } return rootObject; } ================================================ FILE: Sparkle/SPUSkippedUpdate.h ================================================ // // SPUSkippedUpdate.h // Sparkle // // Created by Mayur Pawashe on 5/8/21. // Copyright © 2021 Sparkle Project. All rights reserved. // #import NS_ASSUME_NONNULL_BEGIN @class SUHost, SUAppcastItem; #ifndef BUILDING_SPARKLE_TESTS #define SPUSkippedUpdateDefinitionAttribute SPU_OBJC_DIRECT_MEMBERS #else #define SPUSkippedUpdateDefinitionAttribute __attribute__((objc_runtime_name("SPUTestSkippedUpdate"))) #endif /* A skipped update tracks an optional minor version and an optional major version the user may skip. The minor and major versions are independent versions, so the user can choose to skip at most two separate versions. The intent is when the user is faced with a major upgrade, they can skip a major version. Otherwise they can choose to skip a minor version. */ SPUSkippedUpdateDefinitionAttribute @interface SPUSkippedUpdate : NSObject + (nullable SPUSkippedUpdate *)skippedUpdateForHost:(SUHost *)host; + (void)clearSkippedUpdateForHost:(SUHost *)host; + (void)skipUpdate:(SUAppcastItem *)updateItem host:(SUHost *)host; // At least one of minorVersion or majorVersion should be non-nil - (instancetype)initWithMinorVersion:(nullable NSString *)minorVersion majorVersion:(nullable NSString *)majorVersion majorSubreleaseVersion:(nullable NSString *)majorSubreleaseVersion; // At least one of these two version properties will be non-nil @property (nonatomic, readonly, nullable) NSString *minorVersion; @property (nonatomic, readonly, nullable) NSString *majorVersion; @property (nonatomic, readonly, nullable) NSString *majorSubreleaseVersion; @end NS_ASSUME_NONNULL_END ================================================ FILE: Sparkle/SPUSkippedUpdate.m ================================================ // // SPUSkippedUpdate.m // Sparkle // // Created by Mayur Pawashe on 5/8/21. // Copyright © 2021 Sparkle Project. All rights reserved. // #import "SPUSkippedUpdate.h" #import "SUHost.h" #import "SUConstants.h" #import "SUAppcastItem.h" #include "AppKitPrevention.h" @implementation SPUSkippedUpdate @synthesize minorVersion = _minorVersion; @synthesize majorVersion = _majorVersion; @synthesize majorSubreleaseVersion = _majorSubreleaseVersion; - (instancetype)initWithMinorVersion:(nullable NSString *)minorVersion majorVersion:(nullable NSString *)majorVersion majorSubreleaseVersion:(nullable NSString *)majorSubreleaseVersion { self = [super init]; if (self != nil) { _minorVersion = [minorVersion copy]; _majorVersion = [majorVersion copy]; _majorSubreleaseVersion = [majorSubreleaseVersion copy]; assert(_minorVersion != nil || _majorVersion != nil); } return self; } + (nullable SPUSkippedUpdate *)skippedUpdateForHost:(SUHost *)host { NSString *minorVersion = [host objectForUserDefaultsKey:SUSkippedMinorVersionKey ofClass:NSString.class]; NSString *majorVersion = [host objectForUserDefaultsKey:SUSkippedMajorVersionKey ofClass:NSString.class]; NSString *majorSubreleaseVersion = [host objectForUserDefaultsKey:SUSkippedMajorSubreleaseVersionKey ofClass:NSString.class]; if (minorVersion != nil || majorVersion != nil) { return [[SPUSkippedUpdate alloc] initWithMinorVersion:minorVersion majorVersion:majorVersion majorSubreleaseVersion:majorSubreleaseVersion]; } else { return nil; } } + (void)clearSkippedUpdateForHost:(SUHost *)host { [host setObject:nil forUserDefaultsKey:SUSkippedMinorVersionKey]; [host setObject:nil forUserDefaultsKey:SUSkippedMajorVersionKey]; [host setObject:nil forUserDefaultsKey:SUSkippedMajorSubreleaseVersionKey]; } + (void)skipUpdate:(SUAppcastItem *)updateItem host:(SUHost *)host { NSString *version = updateItem.versionString; if (updateItem.majorUpgrade) { NSString *majorVersion = updateItem.minimumAutoupdateVersion; assert(majorVersion != nil); [host setObject:majorVersion forUserDefaultsKey:SUSkippedMajorVersionKey]; [host setObject:version forUserDefaultsKey:SUSkippedMajorSubreleaseVersionKey]; } else { [host setObject:version forUserDefaultsKey:SUSkippedMinorVersionKey]; } } @end ================================================ FILE: Sparkle/SPUStandardUpdaterController.h ================================================ // // SPUStandardUpdaterController.h // Sparkle // // Created by Mayur Pawashe on 2/28/16. // Copyright © 2016 Sparkle Project. All rights reserved. // #import #if defined(BUILDING_SPARKLE_SOURCES_EXTERNALLY) // Ignore incorrect warning #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wquoted-include-in-framework-header" #import "SUExport.h" #pragma clang diagnostic pop #else #import #endif NS_ASSUME_NONNULL_BEGIN @class SPUUpdater; @class SPUStandardUserDriver; @class NSMenuItem; @protocol SPUUserDriver, SPUUpdaterDelegate, SPUStandardUserDriverDelegate; /** A controller class that instantiates a `SPUUpdater` and allows binding UI to its updater settings. This class can be instantiated in a nib or created programmatically using `-initWithUpdaterDelegate:userDriverDelegate:` or `-initWithStartingUpdater:updaterDelegate:userDriverDelegate:`. The controller's updater targets the application's main bundle and uses Sparkle's standard user interface. Typically, this class is used by sticking it as a custom NSObject subclass in an Interface Builder nib (probably in MainMenu) but it works well programmatically too. The controller creates an `SPUUpdater` instance using a `SPUStandardUserDriver` and allows hooking up the check for updates action and handling menu item validation. It also allows hooking up the updater's and user driver's delegates. If you need more control over what bundle you want to update, or you want to provide a custom user interface (via `SPUUserDriver`), please use `SPUUpdater` directly instead. This class must be used on the main thread. */ SU_EXPORT NS_SWIFT_UI_ACTOR @interface SPUStandardUpdaterController : NSObject { #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wobjc-interface-ivars" /** * Interface builder outlet for the updater's delegate. */ IBOutlet __weak id updaterDelegate; /** * Interface builder outlet for the user driver's delegate. */ IBOutlet __weak id userDriverDelegate; #pragma clang diagnostic pop } /** Accessible property for the updater. Some properties on the updater can be binded via KVO When instantiated from a nib, don't perform update checks before the application has finished launching in a MainMenu nib (i.e applicationDidFinishLaunching:) or before the corresponding window/view controller has been loaded (i.e, windowDidLoad or viewDidLoad). The updater is not guaranteed to be started yet before these points. */ @property (nonatomic, readonly) SPUUpdater *updater; /** Accessible property for the updater's user driver. */ @property (nonatomic, readonly) SPUStandardUserDriver *userDriver; /** Create a new `SPUStandardUpdaterController` from a nib. You cannot call this initializer directly. You must instantiate a `SPUStandardUpdaterController` inside of a nib (typically the MainMenu nib) to use it. To create a `SPUStandardUpdaterController` programmatically, use `-initWithUpdaterDelegate:userDriverDelegate:` or `-initWithStartingUpdater:updaterDelegate:userDriverDelegate:` instead. */ - (instancetype)init NS_UNAVAILABLE; /** Create a new `SPUStandardUpdaterController` programmatically. The updater is started automatically. See `-startUpdater` for more information. Note the `updaterDelegate` and `userDriverDelegate` are weakly referenced, so you are responsible for keeping them alive. */ - (instancetype)initWithUpdaterDelegate:(nullable id)updaterDelegate userDriverDelegate:(nullable id)userDriverDelegate; /** Create a new `SPUStandardUpdaterController` programmatically allowing you to specify whether or not to start the updater immediately. You can specify whether or not you want to start the updater immediately. If you do not start the updater, you must invoke `-startUpdater` at a later time to start it. Note the `updaterDelegate` and `userDriverDelegate` are weakly referenced, so you are responsible for keeping them alive. */ - (instancetype)initWithStartingUpdater:(BOOL)startUpdater updaterDelegate:(nullable id)updaterDelegate userDriverDelegate:(nullable id)userDriverDelegate; /** Starts the updater if it has not already been started. You should only call this method yourself if you opted out of starting the updater on initialization. Hence, do not call this yourself if you are instantiating this controller from a nib. This invokes `-[SPUUpdater startUpdater:]`. If the application is misconfigured with Sparkle, an error is logged and an alert is shown to the user (after a few seconds) to contact the developer. If you want more control over this behavior, you can create your own `SPUUpdater` instead of using `SPUStandardUpdaterController`. */ - (void)startUpdater; /** Explicitly checks for updates and displays a progress dialog while doing so. This method is meant for a main menu item. Connect any NSMenuItem to this action in Interface Builder or programmatically, and Sparkle will check for updates and report back its findings verbosely when it is invoked. When the target/action of the menu item is set to this controller and this method, this controller also handles enabling/disabling the menu item by checking `-[SPUUpdater canCheckForUpdates]` This action checks updates by invoking `-[SPUUpdater checkForUpdates]` */ - (IBAction)checkForUpdates:(nullable id)sender; @end NS_ASSUME_NONNULL_END ================================================ FILE: Sparkle/SPUStandardUpdaterController.m ================================================ // // SPUStandardUpdaterController.m // Sparkle // // Created by Mayur Pawashe on 2/28/16. // Copyright © 2016 Sparkle Project. All rights reserved. // #if SPARKLE_BUILD_UI_BITS #import "SPUStandardUpdaterController.h" #import "SPUUpdater.h" #import "SUHost.h" #import "SPUStandardUserDriver.h" #import "SUConstants.h" #import "SULog.h" #import "SULocalizations.h" #import // We use public instance variables instead of properties for the updater / user driver delegates // because we want them to be connectable outlets from Interface Builder, but we do not want their setters to be invoked // programmatically. @interface SPUStandardUpdaterController () // Needed for KVO @property (nonatomic) SPUUpdater *updater; @end @implementation SPUStandardUpdaterController @synthesize updater = _updater; @synthesize userDriver = _userDriver; - (void)awakeFromNib { // Note: awakeFromNib might be called more than once // We have to use awakeFromNib otherwise the delegate outlets may not be connected yet, // and we aren't a proper window or view controller, so we don't have a proper "did load" point if (_updater == nil) { [self _initUpdater]; [self startUpdater]; } } - (void)_initUpdater SPU_OBJC_DIRECT { NSBundle *hostBundle = [NSBundle mainBundle]; SPUStandardUserDriver *userDriver = [[SPUStandardUserDriver alloc] initWithHostBundle:hostBundle delegate:self->userDriverDelegate]; SPUUpdater *updater = [[SPUUpdater alloc] initWithHostBundle:hostBundle applicationBundle:hostBundle userDriver:userDriver delegate:self->updaterDelegate]; [self setUpdater:updater]; _userDriver = userDriver; } - (instancetype)initWithUpdaterDelegate:(nullable id)theUpdaterDelegate userDriverDelegate:(nullable id)theUserDriverDelegate { return [self initWithStartingUpdater:YES updaterDelegate:theUpdaterDelegate userDriverDelegate:theUserDriverDelegate]; } - (instancetype)initWithStartingUpdater:(BOOL)startUpdater updaterDelegate:(nullable id)theUpdaterDelegate userDriverDelegate:(nullable id)theUserDriverDelegate { if ((self = [super init])) { self->updaterDelegate = theUpdaterDelegate; self->userDriverDelegate = theUserDriverDelegate; [self _initUpdater]; if (startUpdater) { [self startUpdater]; } } return self; } - (void)startUpdater { NSError *updaterError = nil; if (![_updater startUpdater:&updaterError]) { SULog(SULogLevelError, @"Fatal updater error (%ld): %@", updaterError.code, updaterError.localizedDescription); // Delay the alert a bit to allow other start-up actions dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ NSBundle *hostBundle = [NSBundle mainBundle]; SUHost *host = [[SUHost alloc] initWithBundle:hostBundle]; #if SPARKLE_COPY_LOCALIZATIONS NSBundle *sparkleBundle = SUSparkleBundle(); #endif // This is a developer facing error message which is never actually meant to occur in production // Feel free to provide localizations if you want, but it is not strictly necessary. // Previously, this code path used to be an abort() NSAlert *alert = [[NSAlert alloc] init]; alert.messageText = SULocalizedStringFromTableInBundle(@"Unable to Check For Updates", SPARKLE_TABLE, sparkleBundle, nil); alert.informativeText = [NSString stringWithFormat:SULocalizedStringFromTableInBundle(@"The updater failed to start. Please verify you have the latest version of %@ and contact the app developer if the issue still persists. Check the Console logs for more information.", SPARKLE_TABLE, sparkleBundle, nil), host.name]; [alert runModal]; }); } } - (IBAction)checkForUpdates:(nullable id)__unused sender { [_updater checkForUpdates]; } - (BOOL)validateMenuItem:(NSMenuItem *)item { if ([item action] == @selector(checkForUpdates:)) { return _updater.canCheckForUpdates; } return YES; } @end #endif ================================================ FILE: Sparkle/SPUStandardUserDriver+Private.h ================================================ // // SPUStandardUserDriver+Private.h // Sparkle // // Copyright © 2022 Sparkle Project. All rights reserved. // #ifndef SPUStandardUserDriver_Private_h #define SPUStandardUserDriver_Private_h #if defined(BUILDING_SPARKLE_SOURCES_EXTERNALLY) // Ignore incorrect warning #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wquoted-include-in-framework-header" #import "SPUStandardUserDriver.h" #import "SUExport.h" #pragma clang diagnostic pop #else #import #import #endif @class NSWindowController; NS_ASSUME_NONNULL_BEGIN SU_EXPORT @interface SPUStandardUserDriver (Private) /** Private API for accessing the active update alert's window controller. This is the window controller that shows the update's release notes and install choices. This can be accessed in -[SPUStandardUserDriverDelegate standardUserDriverWillHandleShowingUpdate:forUpdate:state:] */ @property (nonatomic, readonly, nullable) NSWindowController *activeUpdateAlert; @end NS_ASSUME_NONNULL_END #endif /* SPUStandardUserDriver_Private_h */ ================================================ FILE: Sparkle/SPUStandardUserDriver.h ================================================ // // SPUStandardUserDriver.h // Sparkle // // Created by Mayur Pawashe on 2/14/16. // Copyright © 2016 Sparkle Project. All rights reserved. // #import #if defined(BUILDING_SPARKLE_SOURCES_EXTERNALLY) // Ignore incorrect warning #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wquoted-include-in-framework-header" #import "SPUUserDriver.h" #import "SUExport.h" #pragma clang diagnostic pop #else #import #import #endif NS_ASSUME_NONNULL_BEGIN @protocol SPUStandardUserDriverDelegate; /** Sparkle's standard built-in user driver for updater interactions */ SU_EXPORT NS_SWIFT_UI_ACTOR @interface SPUStandardUserDriver : NSObject /** Initializes a Sparkle's standard user driver for user update interactions @param hostBundle The target bundle of the host that is being updated. @param delegate The optional delegate to this user driver. Note the standard user driver weakly references the delegate, so you are responsible for keeping it alive. */ - (instancetype)initWithHostBundle:(NSBundle *)hostBundle delegate:(nullable id)delegate; /** Use initWithHostBundle:delegate: instead. */ - (instancetype)init NS_UNAVAILABLE; @end NS_ASSUME_NONNULL_END ================================================ FILE: Sparkle/SPUStandardUserDriver.m ================================================ // // SPUStandardUserDriver.m // Sparkle // // Created by Mayur Pawashe on 2/14/16. // Copyright © 2016 Sparkle Project. All rights reserved. // #if SPARKLE_BUILD_UI_BITS #import "SPUStandardUserDriver.h" #import "SPUStandardUserDriverDelegate.h" #import "SPUGentleUserDriverReminders.h" #import "SUAppcastItem.h" #import "SUVersionDisplayProtocol.h" #import "SUHost.h" #import "SUUpdatePermissionPrompt.h" #import "SUStatusController.h" #import "SUUpdateAlert.h" #import "SULocalizations.h" #import "SUApplicationInfo.h" #import "SPUUserUpdateState.h" #import "SUErrors.h" #import "SPUInstallationType.h" #import "SPUStandardVersionDisplay.h" #import "SULog.h" #import "SPUNoUpdateFoundInfo.h" #import "SPUUpdaterSettings.h" #import "SPUUpdaterSettings+Debug.h" #include #include #import #import #if __MAC_OS_X_VERSION_MAX_ALLOWED < 140000 @interface NSApplication (ActivationAPIs) - (void)activate; @end #endif @interface SPUStandardUserDriver () // Note: we expose a private interface for activeUpdateAlert property in SPUStandardUserDriver+Private.h as NSWindowController @property (nonatomic, readonly, nullable) NSWindowController *activeUpdateAlert; @end @implementation SPUStandardUserDriver { void (^_retryTerminatingApplication)(void); void (^_installUpdateHandler)(SPUUserUpdateChoice); void (^_cancellation)(void); SUHost *_host; // We must store the oldHostName before the host is potentially replaced // because we may use this property after update has been installed NSString *_oldHostName; NSURL *_oldHostBundleURL; id _applicationBecameActiveAfterUpdateAlertBecameKeyObserver; NSValue *_updateAlertWindowFrameValue; SUStatusController *_checkingController; SUUpdateAlert *_activeUpdateAlert; SPUUpdaterSettings *_updaterSettings; SUStatusController *_statusController; SUUpdatePermissionPrompt *_permissionPrompt; __weak id _delegate; mach_timebase_info_data_t _timebaseInfo; uint64_t _expectedContentLength; uint64_t _bytesDownloaded; double _timeSinceOpportuneUpdateNotice; BOOL _updateAlertWindowWasInactive; BOOL _loggedGentleUpdateReminderWarning; BOOL _regularApplicationUpdate; BOOL _updateReceivedUserAttention; } @synthesize activeUpdateAlert = _activeUpdateAlert; #pragma mark Birth - (instancetype)initWithHostBundle:(NSBundle *)hostBundle delegate:(nullable id)delegate { self = [super init]; if (self != nil) { _host = [[SUHost alloc] initWithBundle:hostBundle]; _updaterSettings = [[SPUUpdaterSettings alloc] initWithHostBundle:hostBundle]; _oldHostName = _host.name; _oldHostBundleURL = hostBundle.bundleURL; _delegate = delegate; kern_return_t timebaseInfoResult = mach_timebase_info(&_timebaseInfo); if (timebaseInfoResult != KERN_SUCCESS) { SULog(SULogLevelError, @"Error: failed to fill mach_timebase_info() with error %d", timebaseInfoResult); _timebaseInfo.numer = 0; _timebaseInfo.denom = 0; } } return self; } - (double)currentTime SPU_OBJC_DIRECT { if (_timebaseInfo.denom > 0) { return (double)(mach_absolute_time() * _timebaseInfo.numer) / (double)_timebaseInfo.denom; } else { return 0.0; } } // This private method is used by SPUUpdater for resetting the opportune time to show an update notice in utmost focus - (void)resetTimeSinceOpportuneUpdateNotice { _timeSinceOpportuneUpdateNotice = [self currentTime]; } #pragma mark Update Permission - (void)_activateApplication SPU_OBJC_DIRECT { if (@available(macOS 14, *)) { [NSApp activate]; } else { [NSApp activateIgnoringOtherApps:YES]; } } - (void)showUpdatePermissionRequest:(SPUUpdatePermissionRequest *)request reply:(void (^)(SUUpdatePermissionResponse *))reply { assert(NSThread.isMainThread); if ([SUApplicationInfo isBackgroundApplication:[NSApplication sharedApplication]]) { [self _activateApplication]; } __weak __typeof__(self) weakSelf = self; _permissionPrompt = [[SUUpdatePermissionPrompt alloc] initPromptWithHost:_host request:request reply:^(SUUpdatePermissionResponse *response) { reply(response); __typeof__(self) strongSelf = weakSelf; if (strongSelf != nil) { strongSelf->_permissionPrompt = nil; } }]; [_permissionPrompt showWindow:nil]; } #pragma mark Update Alert Focus // This private method is used by SPUUpdater when scheduling for update checks - (void)logGentleScheduledUpdateReminderWarningIfNeeded { id delegate = _delegate; if (!_loggedGentleUpdateReminderWarning && (![delegate respondsToSelector:@selector(supportsGentleScheduledUpdateReminders)] || !delegate.supportsGentleScheduledUpdateReminders)) { BOOL isBackgroundApp = [SUApplicationInfo isBackgroundApplication:[NSApplication sharedApplication]]; if (isBackgroundApp) { SULog(SULogLevelError, @"Warning: Background app automatically schedules for update checks but does not implement gentle reminders. As a result, users may not take notice to update alerts that show up in the background. Please visit https://sparkle-project.org/documentation/gentle-reminders for more information. This warning will only be logged once."); _loggedGentleUpdateReminderWarning = YES; } } } // updateItem should be non-nil when showing an update for first time for scheduled updates // If appcastItem is != nil, then state must be != nil - (void)setUpActiveUpdateAlertForScheduledUpdate:(SUAppcastItem * _Nullable)updateItem state:(SPUUserUpdateState * _Nullable)state SPU_OBJC_DIRECT { // Make sure the window is loaded in any case [_activeUpdateAlert window]; [self _removeApplicationBecomeActiveObserver]; if (updateItem == nil) { // This is a user initiated check or a check to bring the already shown update back in focus if (![NSApp isActive]) { // If the user initiated an update check, we should make the app active, // regardless if it's a background running app or not [self _activateApplication]; } [_activeUpdateAlert showWindow:nil]; [_activeUpdateAlert setInstallButtonFocus:YES]; } else { // Handle scheduled update check uint64_t timeElapsedSinceOpportuneUpdateNotice = (uint64_t)([self currentTime] - _timeSinceOpportuneUpdateNotice); // Give scheduled update alerts priority if 3 or less seconds have passed since our last opportune time BOOL appNearUpdaterInitialization = (timeElapsedSinceOpportuneUpdateNotice <= 3000000000ULL); // We will always show an update alert at the right time [_activeUpdateAlert setInstallButtonFocus:YES]; // If the delegate doesn't override our behavior: // For regular applications, only show the update alert if the app is active and if it's an an opportune time, otherwise, we'll wait until the app becomes active again. // For background applications, if the app is active, we will show the update window ordered back. // If the app is inactive, we'll show the update alert in the background behind other running apps // But we are near app launch, we will activate the app and show the alert as key BOOL backgroundApp = [SUApplicationInfo isBackgroundApplication:NSApp]; BOOL driverShowingUpdateNow; BOOL immediateFocus; BOOL showingUpdateInBack; BOOL activatingApp; if ([NSApp isActive]) { BOOL systemHasBeenIdle; { // If the system has been inactive for several minutes, allow the update alert to show up immediately. We assume it's likely the user isn't at their computer in this case. // Note this is not done for background running applications. CFTimeInterval timeSinceLastEvent; if (!appNearUpdaterInitialization && !backgroundApp) { timeSinceLastEvent = CGEventSourceSecondsSinceLastEventType(kCGEventSourceStateHIDSystemState, kCGAnyInputEventType); NSTimeInterval scheduledUpdateIdleEventLeewayInterval = _updaterSettings.standardUIScheduledUpdateIdleEventLeewayInterval; if (timeSinceLastEvent >= scheduledUpdateIdleEventLeewayInterval) { // Make sure there's no active power management assertions preventing // the display from sleeping by the current application. // If there is, then the app may still actively be in use CFDictionaryRef cfAssertions = NULL; if (IOPMCopyAssertionsByProcess(&cfAssertions) == kIOReturnSuccess) { NSDictionary *> *> *assertions = CFBridgingRelease(cfAssertions); pid_t currentProcessIdentifier = NSRunningApplication.currentApplication.processIdentifier; NSNumber *processIdentifierKey = @(currentProcessIdentifier); NSArray *> *currentProcessAssertions = assertions[processIdentifierKey]; BOOL foundNoDisplaySleepAssertion = NO; for (NSDictionary *assertion in currentProcessAssertions) { NSString *assertionType = assertion[(NSString *)kIOPMAssertionTypeKey]; NSNumber *assertionLevel = assertion[(NSString *)kIOPMAssertionLevelKey]; if ([assertionType isEqualToString:(NSString *)kIOPMAssertionTypeNoDisplaySleep] && [assertionLevel isEqual:@(kIOPMAssertionLevelOn)]) { foundNoDisplaySleepAssertion = YES; break; } } systemHasBeenIdle = !foundNoDisplaySleepAssertion; } else { systemHasBeenIdle = NO; } } else { systemHasBeenIdle = NO; } } else { systemHasBeenIdle = NO; } } if (appNearUpdaterInitialization || systemHasBeenIdle) { driverShowingUpdateNow = YES; immediateFocus = YES; showingUpdateInBack = NO; activatingApp = backgroundApp; } else { driverShowingUpdateNow = backgroundApp; immediateFocus = NO; // If there is a key window active in the app, show the update alert behind other windows showingUpdateInBack = backgroundApp && ([NSApp keyWindow] != nil); activatingApp = NO; } } else { // For regular applications, we will show the update alert when the user comes back to the app // For background applications, we will show the update alert right away but in the background, // unless focus is requested if (!backgroundApp) { driverShowingUpdateNow = NO; immediateFocus = NO; showingUpdateInBack = NO; activatingApp = NO; } else { driverShowingUpdateNow = YES; immediateFocus = appNearUpdaterInitialization; showingUpdateInBack = NO; activatingApp = appNearUpdaterInitialization; } } id delegate = _delegate; BOOL handleShowingUpdates; if ([delegate respondsToSelector:@selector(standardUserDriverShouldHandleShowingScheduledUpdate:andInImmediateFocus:)]) { handleShowingUpdates = [delegate standardUserDriverShouldHandleShowingScheduledUpdate:(SUAppcastItem * _Nonnull)updateItem andInImmediateFocus:immediateFocus]; } else { handleShowingUpdates = YES; } if (!handleShowingUpdates) { // Delay a runloop cycle to make sure the update can properly be checked __weak __typeof__(self) weakSelf = self; dispatch_async(dispatch_get_main_queue(), ^{ __typeof__(self) strongSelf = weakSelf; if (strongSelf != nil) { id innerDelegate = strongSelf->_delegate; if ([innerDelegate respondsToSelector:@selector(standardUserDriverWillHandleShowingUpdate:forUpdate:state:)]) { [innerDelegate standardUserDriverWillHandleShowingUpdate:handleShowingUpdates forUpdate:(SUAppcastItem * _Nonnull)updateItem state:(SPUUserUpdateState * _Nonnull)state]; } else { SULog(SULogLevelError, @"Error: Delegate <%@> is handling showing scheduled update but does not implement %@", innerDelegate, NSStringFromSelector(@selector(standardUserDriverWillHandleShowingUpdate:forUpdate:state:))); } } }); } else { // The update will be shown, but not necessarily immediately if !driverShowingUpdateNow // It is useful to post this early in case the delegate wants to post a notification if ([delegate respondsToSelector:@selector(standardUserDriverWillHandleShowingUpdate:forUpdate:state:)]) { [delegate standardUserDriverWillHandleShowingUpdate:handleShowingUpdates forUpdate:(SUAppcastItem * _Nonnull)updateItem state:(SPUUserUpdateState * _Nonnull)state]; } if (!driverShowingUpdateNow) { [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(applicationDidBecomeActive:) name:NSApplicationDidBecomeActiveNotification object:NSApp]; } else { if (activatingApp) { [self _activateApplication]; } if (showingUpdateInBack) { [_activeUpdateAlert.window orderBack:nil]; } else { [_activeUpdateAlert showWindow:nil]; } } } } } - (void)_removeApplicationBecomeActiveObserver SPU_OBJC_DIRECT { [[NSNotificationCenter defaultCenter] removeObserver:self name:NSApplicationDidBecomeActiveNotification object:NSApp]; } - (void)applicationDidBecomeActive:(NSNotification *)__unused aNotification { [_activeUpdateAlert showWindow:nil]; [_activeUpdateAlert setInstallButtonFocus:YES]; [self _removeApplicationBecomeActiveObserver]; } #pragma mark Update Found - (void)showUpdateFoundWithAppcastItem:(SUAppcastItem *)appcastItem state:(SPUUserUpdateState *)state reply:(void (^)(SPUUserUpdateChoice))reply { assert(NSThread.isMainThread); [self closeCheckingWindow]; if (_activeUpdateAlert != nil) { SULog(SULogLevelError, @"Error: -[%@ %@] should not be called when _activeUpdateAlert != nil:\n%@", NSStringFromClass([self class]), NSStringFromSelector(_cmd), NSThread.callStackSymbols); } id delegate = _delegate; id customVersionDisplayer = nil; if ([delegate respondsToSelector:@selector(standardUserDriverRequestsVersionDisplayer)]) { customVersionDisplayer = [delegate standardUserDriverRequestsVersionDisplayer]; } id versionDisplayer = (customVersionDisplayer != nil) ? customVersionDisplayer : [SPUStandardVersionDisplay standardVersionDisplay]; BOOL needsToObserveUserAttention = [delegate respondsToSelector:@selector(standardUserDriverDidReceiveUserAttentionForUpdate:)]; __weak __typeof__(self) weakSelf = self; __weak id weakDelegate = delegate; _activeUpdateAlert = [[SUUpdateAlert alloc] initWithAppcastItem:appcastItem state:state host:_host versionDisplayer:versionDisplayer updaterSettings:_updaterSettings delegate:delegate completionBlock:^(SPUUserUpdateChoice choice, NSRect windowFrame, BOOL wasKeyWindow) { reply(choice); __typeof__(self) strongSelf = weakSelf; if (strongSelf != nil) { if (needsToObserveUserAttention && !strongSelf->_updateReceivedUserAttention) { strongSelf->_updateReceivedUserAttention = YES; id strongDelegate = weakDelegate; // needsToObserveUserAttention already checks delegate responds to this selector [strongDelegate standardUserDriverDidReceiveUserAttentionForUpdate:appcastItem]; } // Record the window frame of the update alert right before we deallocate it // So we can center future status window to where the update alert last was. // Also record if the window was inactive at the time a response was made // (the window may not be key if the window e.g. holds command while clicking on a response button) strongSelf->_updateAlertWindowFrameValue = [NSValue valueWithRect:windowFrame]; strongSelf->_updateAlertWindowWasInactive = !wasKeyWindow; strongSelf->_activeUpdateAlert = nil; } } didBecomeKeyBlock:^{ if (!needsToObserveUserAttention) { return; } if ([NSApp isActive]) { __typeof__(self) strongSelf = weakSelf; if (strongSelf != nil && !strongSelf->_updateReceivedUserAttention) { strongSelf->_updateReceivedUserAttention = YES; id strongDelegate = weakDelegate; // needsToObserveUserAttention already checks delegate responds to this selector [strongDelegate standardUserDriverDidReceiveUserAttentionForUpdate:appcastItem]; } } else { // We need to listen for when the app becomes active again, and then test if the window alert // is still key. if it is, let the delegate know. Remove the observation after that. __typeof__(self) strongSelfOuter = weakSelf; if (strongSelfOuter != nil && strongSelfOuter->_applicationBecameActiveAfterUpdateAlertBecameKeyObserver == nil) { strongSelfOuter->_applicationBecameActiveAfterUpdateAlertBecameKeyObserver = [[NSNotificationCenter defaultCenter] addObserverForName:NSApplicationDidBecomeActiveNotification object:NSApp queue:NSOperationQueue.mainQueue usingBlock:^(NSNotification * _Nonnull __unused note) { __typeof__(self) strongSelf = weakSelf; if (strongSelf != nil) { if (!strongSelf->_updateReceivedUserAttention && [strongSelf->_activeUpdateAlert.window isKeyWindow]) { strongSelf->_updateReceivedUserAttention = YES; id strongDelegate = weakDelegate; // needsToObserveUserAttention already checks delegate responds to this selector [strongDelegate standardUserDriverDidReceiveUserAttentionForUpdate:appcastItem]; } if (strongSelf->_applicationBecameActiveAfterUpdateAlertBecameKeyObserver != nil) { [[NSNotificationCenter defaultCenter] removeObserver:strongSelf->_applicationBecameActiveAfterUpdateAlertBecameKeyObserver]; strongSelf->_applicationBecameActiveAfterUpdateAlertBecameKeyObserver = nil; } } }]; } } }]; _regularApplicationUpdate = [appcastItem.installationType isEqualToString:SPUInstallationTypeApplication]; // For user initiated checks, let the delegate know we'll be showing an update // For scheduled checks, -setUpActiveUpdateAlertForUpdate:state: below will handle this if (state.userInitiated && [delegate respondsToSelector:@selector(standardUserDriverWillHandleShowingUpdate:forUpdate:state:)]) { [delegate standardUserDriverWillHandleShowingUpdate:YES forUpdate:appcastItem state:state]; } [self setUpActiveUpdateAlertForScheduledUpdate:(state.userInitiated ? nil : appcastItem) state:state]; } - (void)showUpdateReleaseNotesWithDownloadData:(SPUDownloadData *)downloadData { assert(NSThread.isMainThread); [_activeUpdateAlert showUpdateReleaseNotesWithDownloadData:downloadData]; } - (void)showUpdateReleaseNotesFailedToDownloadWithError:(NSError *)error { assert(NSThread.isMainThread); // I don't want to expose SULog here because it's more of a user driver facing error // For our purposes we just ignore it and continue on.. NSLog(@"Failed to download release notes with error: %@", error); [_activeUpdateAlert showReleaseNotesFailedToDownloadWithError:error]; } - (void)showUpdateInFocus { BOOL mayNeedToActivateApp; if (_activeUpdateAlert != nil) { [self setUpActiveUpdateAlertForScheduledUpdate:nil state:nil]; mayNeedToActivateApp = NO; } else if (_permissionPrompt != nil) { [_permissionPrompt showWindow:nil]; mayNeedToActivateApp = YES; } else if (_statusController != nil) { [_statusController showWindow:nil]; mayNeedToActivateApp = YES; } else if (_checkingController != nil) { [_checkingController showWindow:nil]; mayNeedToActivateApp = YES; } else if (_retryTerminatingApplication != nil) { [self _showAndConfigureStatusControllerForReadyToInstallWithAction:@selector(retryTermination:) closable:YES]; mayNeedToActivateApp = YES; } else { mayNeedToActivateApp = NO; } if (mayNeedToActivateApp && ![NSApp isActive]) { // Make the app active if it's not already active, e.g, from a menu bar extra [self _activateApplication]; } } #pragma mark Install & Relaunch Update - (void)_showAndConfigureStatusControllerForReadyToInstallWithAction:(SEL)selector closable:(BOOL)closable SPU_OBJC_DIRECT { [self createAndShowStatusControllerWithClosable:closable]; #if SPARKLE_COPY_LOCALIZATIONS NSBundle *sparkleBundle = SUSparkleBundle(); #endif [_statusController beginActionWithTitle:SULocalizedStringFromTableInBundle(@"Ready to Install", SPARKLE_TABLE, sparkleBundle, nil) maxProgressValue:1.0 statusText:nil]; [_statusController setProgressValue:1.0]; // Fill the bar. [_statusController setButtonEnabled:YES]; [_statusController setButtonTitle:SULocalizedStringFromTableInBundle(@"Install and Relaunch", SPARKLE_TABLE, sparkleBundle, nil) target:self action:selector isDefault:YES accessibilityIdentifier:@"SUStatusInstallAndRelaunch"]; } - (void)showReadyToInstallAndRelaunch:(void (^)(SPUUserUpdateChoice))installUpdateHandler { assert(NSThread.isMainThread); [self _showAndConfigureStatusControllerForReadyToInstallWithAction:@selector(installAndRestart:) closable:NO]; [NSApp requestUserAttention:NSInformationalRequest]; _installUpdateHandler = [installUpdateHandler copy]; } - (void)installAndRestart:(id)__unused sender { if (_installUpdateHandler != nil) { _installUpdateHandler(SPUUserUpdateChoiceInstall); _installUpdateHandler = nil; } } - (void)retryTermination:(id)__unused sender { if (_retryTerminatingApplication != nil) { _retryTerminatingApplication(); } } #pragma mark Check for Updates - (void)showUserInitiatedUpdateCheckWithCancellation:(void (^)(void))cancellation { assert(NSThread.isMainThread); _cancellation = [cancellation copy]; #if SPARKLE_COPY_LOCALIZATIONS NSBundle *sparkleBundle = SUSparkleBundle(); #endif _checkingController = [[SUStatusController alloc] initWithHost:_host windowTitle:SULocalizedStringFromTableInBundle(@"Software Update", SPARKLE_TABLE, sparkleBundle, nil) centerPointValue:nil minimizable:NO closable:NO]; [[_checkingController window] center]; // Force the checking controller to load its window. [_checkingController beginActionWithTitle:SULocalizedStringFromTableInBundle(@"Checking for updates…", SPARKLE_TABLE, sparkleBundle, nil) maxProgressValue:0.0 statusText:nil]; [_checkingController setButtonTitle:SULocalizedStringFromTableInBundle(@"Cancel", SPARKLE_TABLE, sparkleBundle, nil) target:self action:@selector(cancelCheckForUpdates:) isDefault:NO accessibilityIdentifier:@"SUStatusCancel"]; // For background applications, obtain focus. // Useful if the update check is requested from another app like System Preferences. if ([SUApplicationInfo isBackgroundApplication:[NSApplication sharedApplication]]) { [self _activateApplication]; } [_checkingController showWindow:self]; } - (void)closeCheckingWindow SPU_OBJC_DIRECT { if (_checkingController != nil) { [_checkingController close]; _checkingController = nil; _cancellation = nil; } } - (void)cancelCheckForUpdates:(id)__unused sender { if (_cancellation != nil) { _cancellation(); _cancellation = nil; } [self closeCheckingWindow]; } #pragma mark Update Errors - (void)showUpdaterError:(NSError *)error acknowledgement:(void (^)(void))acknowledgement { assert(NSThread.isMainThread); [self closeCheckingWindow]; [_statusController close]; _statusController = nil; #if SPARKLE_COPY_LOCALIZATIONS NSBundle *sparkleBundle = SUSparkleBundle(); #endif // Ideally we should use -[NSAlert alertWithError:] however // unfortunately Sparkle may return error messages with descriptions that contain // recovery suggestions. So we will check if an explicit recovery suggestion exists, // and set the mesage and informative text appropriately. // In the future we should audit potential error messages and make them consistent. NSAlert *alert = [[NSAlert alloc] init]; NSString *recoverySuggestion = [error localizedRecoverySuggestion]; if (recoverySuggestion != nil) { alert.messageText = error.localizedDescription; alert.informativeText = recoverySuggestion; } else { alert.messageText = SULocalizedStringFromTableInBundle(@"Update Error!", SPARKLE_TABLE, sparkleBundle, nil); alert.informativeText = error.localizedDescription; } [alert addButtonWithTitle:SULocalizedStringFromTableInBundle(@"Cancel Update", SPARKLE_TABLE, sparkleBundle, nil)]; [self showAlert:alert secondaryAction:nil]; acknowledgement(); } - (void)showUpdateNotFoundWithError:(NSError *)error acknowledgement:(void (^)(void))acknowledgement { assert(NSThread.isMainThread); [self closeCheckingWindow]; id delegate = _delegate; id customVersionDisplayer; if ([delegate respondsToSelector:@selector(standardUserDriverRequestsVersionDisplayer)]) { customVersionDisplayer = [delegate standardUserDriverRequestsVersionDisplayer]; } else { customVersionDisplayer = nil; } SPUNoUpdateFoundReason reason = (SPUNoUpdateFoundReason)[(NSNumber *)error.userInfo[SPUNoUpdateFoundReasonKey] integerValue]; SUAppcastItem *latestAppcastItem = error.userInfo[SPULatestAppcastItemFoundKey]; #if SPARKLE_COPY_LOCALIZATIONS NSBundle *sparkleBundle = SUSparkleBundle(); #else NSBundle *sparkleBundle = nil; #endif // If we have a custom version displayer, then override the recovery suggestion using the // proper version display NSError *presentationError; if (customVersionDisplayer != nil) { NSString *recoverySuggestion = SPUNoUpdateFoundRecoverySuggestion(reason, latestAppcastItem, _host, customVersionDisplayer, sparkleBundle); NSMutableDictionary *userInfo = [error.userInfo mutableCopy]; userInfo[NSLocalizedRecoverySuggestionErrorKey] = recoverySuggestion; presentationError = [NSError errorWithDomain:error.domain code:error.code userInfo:[userInfo copy]]; } else { presentationError = error; } NSAlert *alert = [NSAlert alertWithError:presentationError]; alert.alertStyle = NSAlertStyleInformational; // Can we give more information to the user? void (^secondaryAction)(void) = nil; if (latestAppcastItem != nil) { switch (reason) { case SPUNoUpdateFoundReasonOnLatestVersion: case SPUNoUpdateFoundReasonOnNewerThanLatestVersion: { // Show the user the past version history if available // Check if the delegate allows showing the Version History BOOL shouldShowVersionHistory = (![delegate respondsToSelector:@selector(standardUserDriverShouldShowVersionHistoryForAppcastItem:)] || [delegate standardUserDriverShouldShowVersionHistoryForAppcastItem:latestAppcastItem]); if (shouldShowVersionHistory) { NSString *localizedButtonTitle = SULocalizedStringFromTableInBundle(@"Version History", SPARKLE_TABLE, sparkleBundle, nil); // Check if the delegate implements its own Version History action if ([delegate respondsToSelector:@selector(standardUserDriverShowVersionHistoryForAppcastItem:)]) { [alert addButtonWithTitle:localizedButtonTitle]; secondaryAction = ^{ [delegate standardUserDriverShowVersionHistoryForAppcastItem:latestAppcastItem]; }; } else if (latestAppcastItem.fullReleaseNotesURL != nil) { // Open the full release notes URL if informed [alert addButtonWithTitle:localizedButtonTitle]; secondaryAction = ^{ [[NSWorkspace sharedWorkspace] openURL:(NSURL * _Nonnull)latestAppcastItem.fullReleaseNotesURL]; }; } else if (latestAppcastItem.releaseNotesURL != nil) { // Fall back to opening the release notes URL [alert addButtonWithTitle:localizedButtonTitle]; secondaryAction = ^{ [[NSWorkspace sharedWorkspace] openURL:(NSURL * _Nonnull)latestAppcastItem.releaseNotesURL]; }; } } break; } case SPUNoUpdateFoundReasonSystemIsTooOld: case SPUNoUpdateFoundReasonSystemIsTooNew: case SPUNoUpdateFoundReasonHardwareDoesNotSupportARM64: if (latestAppcastItem.infoURL != nil) { // Show the user the product's link if available [alert addButtonWithTitle:SULocalizedStringFromTableInBundle(@"Learn More…", SPARKLE_TABLE, sparkleBundle, nil)]; secondaryAction = ^{ [[NSWorkspace sharedWorkspace] openURL:(NSURL * _Nonnull)latestAppcastItem.infoURL]; }; } break; case SPUNoUpdateFoundReasonUnknown: break; } } [self showAlert:alert secondaryAction:secondaryAction]; acknowledgement(); } - (void)showAlert:(NSAlert *)alert secondaryAction:(void (^ _Nullable)(void))secondaryAction SPU_OBJC_DIRECT { id delegate = _delegate; if ([delegate respondsToSelector:@selector(standardUserDriverWillShowModalAlert)]) { [delegate standardUserDriverWillShowModalAlert]; } [alert setIcon:[SUApplicationInfo bestIconForHost:_host]]; NSModalResponse response = [alert runModal]; if (response == NSAlertSecondButtonReturn && secondaryAction != nil) { secondaryAction(); } if ([delegate respondsToSelector:@selector(standardUserDriverDidShowModalAlert)]) { [delegate standardUserDriverDidShowModalAlert]; } } #pragma mark Download & Install Updates - (void)createAndShowStatusControllerWithClosable:(BOOL)closable SPU_OBJC_DIRECT { if (_statusController == nil) { // We will make the status window minimizable for regular app updates which are often // quick and atomic to install on quit. But we won't do this for package based updates. id delegate = _delegate; BOOL minimizable; if (!_regularApplicationUpdate) { minimizable = NO; } else if ([delegate respondsToSelector:@selector(standardUserDriverAllowsMinimizableStatusWindow)]) { minimizable = [delegate standardUserDriverAllowsMinimizableStatusWindow]; } else { minimizable = YES; } NSValue *centerPointValue; if (_updateAlertWindowFrameValue != nil) { NSRect updateAlertFrame = _updateAlertWindowFrameValue.rectValue; NSPoint centerPoint = NSMakePoint(updateAlertFrame.origin.x + updateAlertFrame.size.width / 2.0, updateAlertFrame.origin.y + updateAlertFrame.size.height / 2.0); centerPointValue = [NSValue valueWithPoint:centerPoint]; } else { centerPointValue = nil; } _statusController = [[SUStatusController alloc] initWithHost:_host windowTitle:[NSString stringWithFormat:SULocalizedStringFromTableInBundle(@"Updating %@", SPARKLE_TABLE, SUSparkleBundle(), nil), _host.name] centerPointValue:centerPointValue minimizable:minimizable closable:closable]; if (_updateAlertWindowWasInactive) { [_statusController.window orderFront:nil]; } else { [_statusController showWindow:self]; } } } - (void)showDownloadInitiatedWithCancellation:(void (^)(void))cancellation { assert(NSThread.isMainThread); _cancellation = [cancellation copy]; [self createAndShowStatusControllerWithClosable:NO]; #if SPARKLE_COPY_LOCALIZATIONS NSBundle *sparkleBundle = SUSparkleBundle(); #endif [_statusController beginActionWithTitle:SULocalizedStringFromTableInBundle(@"Downloading update…", SPARKLE_TABLE, sparkleBundle, @"Take care not to overflow the status window.") maxProgressValue:1.0 statusText:nil]; [_statusController setProgressValue:0.0]; [_statusController setButtonTitle:SULocalizedStringFromTableInBundle(@"Cancel", SPARKLE_TABLE, sparkleBundle, nil) target:self action:@selector(cancelDownload:) isDefault:NO accessibilityIdentifier:@"SUStatusCancel"]; _bytesDownloaded = 0; } - (void)cancelDownload:(id)__unused sender { if (_cancellation != nil) { _cancellation(); _cancellation = nil; } } - (void)showDownloadDidReceiveExpectedContentLength:(uint64_t)expectedContentLength { assert(NSThread.isMainThread); _expectedContentLength = expectedContentLength; if (expectedContentLength == 0) { [_statusController setMaxProgressValue:0.0]; } } - (void)showDownloadDidReceiveDataOfLength:(uint64_t)length { assert(NSThread.isMainThread); _bytesDownloaded += length; NSByteCountFormatter *formatter = [[NSByteCountFormatter alloc] init]; [formatter setZeroPadsFractionDigits:YES]; #if SPARKLE_COPY_LOCALIZATIONS NSBundle *sparkleBundle = SUSparkleBundle(); #endif if (_expectedContentLength > 0) { double newProgressValue = (double)_bytesDownloaded / (double)_expectedContentLength; [_statusController setProgressValue:MIN(newProgressValue, 1.0)]; [_statusController setStatusText:[NSString stringWithFormat:SULocalizedStringFromTableInBundle(@"%@ of %@", SPARKLE_TABLE, sparkleBundle, @"The download progress in units of bytes, e.g. 100 KB of 1,0 MB"), [formatter stringFromByteCount:(long long)_bytesDownloaded], [formatter stringFromByteCount:(long long)MAX(_bytesDownloaded, _expectedContentLength)]]]; } else { [_statusController setStatusText:[NSString stringWithFormat:SULocalizedStringFromTableInBundle(@"%@ downloaded", SPARKLE_TABLE, sparkleBundle, @"The download progress in a unit of bytes, e.g. 100 KB"), [formatter stringFromByteCount:(long long)_bytesDownloaded]]]; } } - (void)showDownloadDidStartExtractingUpdate { assert(NSThread.isMainThread); _cancellation = nil; [self createAndShowStatusControllerWithClosable:NO]; #if SPARKLE_COPY_LOCALIZATIONS NSBundle *sparkleBundle = SUSparkleBundle(); #endif [_statusController beginActionWithTitle:SULocalizedStringFromTableInBundle(@"Extracting update…", SPARKLE_TABLE, sparkleBundle, @"Take care not to overflow the status window.") maxProgressValue:1.0 statusText:nil]; [_statusController setProgressValue:0.0]; [_statusController setButtonTitle:SULocalizedStringFromTableInBundle(@"Cancel", SPARKLE_TABLE, sparkleBundle, nil) target:nil action:nil isDefault:NO accessibilityIdentifier:@"SUStatusCancel"]; [_statusController setButtonEnabled:NO]; } - (void)showExtractionReceivedProgress:(double)progress { assert(NSThread.isMainThread); [_statusController setProgressValue:progress]; } - (void)showInstallingUpdateWithApplicationTerminated:(BOOL)applicationTerminated retryTerminatingApplication:(void (^)(void))retryTerminatingApplication { assert(NSThread.isMainThread); if (applicationTerminated) { // Note this will only show up if -showReadyToInstallAndRelaunch: was called beforehand [_statusController beginActionWithTitle:SULocalizedStringFromTableInBundle(@"Installing update…", SPARKLE_TABLE, SUSparkleBundle(), @"Take care not to overflow the status window.") maxProgressValue:0.0 statusText:nil]; [_statusController setButtonEnabled:NO]; } else { // The "quit" event can always be canceled or delayed by the application we're updating // So we can't easily predict how long the installation will take or if it won't happen right away // We close our status window because we don't want it persisting for too long and have it obscure other windows [_statusController close]; _statusController = nil; // Keep retry handler in case user tries to show update in focus again _retryTerminatingApplication = [retryTerminatingApplication copy]; } } - (void)showUpdateInstalledAndRelaunched:(BOOL)relaunched acknowledgement:(void (^)(void))acknowledgement { assert(NSThread.isMainThread); // Close window showing update is installing [_statusController close]; _statusController = nil; // Only show installed prompt when the app is not relaunched // When the app is relaunched, there is enough of a UI from relaunching the app. if (!relaunched) { #if SPARKLE_COPY_LOCALIZATIONS NSBundle *sparkleBundle = SUSparkleBundle(); #endif NSAlert *alert = [[NSAlert alloc] init]; alert.messageText = SULocalizedStringFromTableInBundle(@"Update Installed", SPARKLE_TABLE, sparkleBundle, nil); // Extract information from newly updated bundle if available NSString *hostName; NSString *hostVersion; NSBundle *newBundle = [NSBundle bundleWithURL:_oldHostBundleURL]; if (newBundle != nil) { SUHost *newHost = [[SUHost alloc] initWithBundle:newBundle]; hostName = newHost.name; hostVersion = newHost.displayVersion; } else { // This may happen if Sparkle's normalization is enabled hostName = _oldHostName; hostVersion = nil; } if (hostVersion != nil) { alert.informativeText = [NSString stringWithFormat:SULocalizedStringFromTableInBundle(@"%@ is now updated to version %@!", SPARKLE_TABLE, sparkleBundle, nil), hostName, hostVersion]; } else { alert.informativeText = [NSString stringWithFormat:SULocalizedStringFromTableInBundle(@"%@ is now updated!", SPARKLE_TABLE, sparkleBundle, nil), hostName]; } [self showAlert:alert secondaryAction:nil]; } acknowledgement(); } #pragma mark Aborting Everything - (void)dismissUpdateInstallation { assert(NSThread.isMainThread); id delegate = _delegate; if ([delegate respondsToSelector:@selector(standardUserDriverWillFinishUpdateSession)]) { [delegate standardUserDriverWillFinishUpdateSession]; } if (_applicationBecameActiveAfterUpdateAlertBecameKeyObserver != nil) { [[NSNotificationCenter defaultCenter] removeObserver:_applicationBecameActiveAfterUpdateAlertBecameKeyObserver]; _applicationBecameActiveAfterUpdateAlertBecameKeyObserver = nil; } _updateReceivedUserAttention = NO; _installUpdateHandler = nil; _cancellation = nil; _retryTerminatingApplication = nil; [self closeCheckingWindow]; if (_permissionPrompt) { [_permissionPrompt close]; _permissionPrompt = nil; } if (_statusController) { [_statusController close]; _statusController = nil; } if (_activeUpdateAlert) { [_activeUpdateAlert close]; _activeUpdateAlert = nil; } [self _removeApplicationBecomeActiveObserver]; } @end #endif ================================================ FILE: Sparkle/SPUStandardUserDriverDelegate.h ================================================ // // SPUStandardUserDriverDelegate.h // Sparkle // // Created by Mayur Pawashe on 3/3/16. // Copyright © 2016 Sparkle Project. All rights reserved. // #import #if defined(BUILDING_SPARKLE_SOURCES_EXTERNALLY) // Ignore incorrect warning #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wquoted-include-in-framework-header" #import "SUExport.h" #pragma clang diagnostic pop #else #import #endif NS_ASSUME_NONNULL_BEGIN @protocol SUVersionDisplay; @class SUAppcastItem; @class SPUUserUpdateState; /** A protocol for Sparkle's standard user driver's delegate This includes methods related to UI interactions */ SU_EXPORT @protocol SPUStandardUserDriverDelegate @optional /** Called before showing a modal alert window, to give the opportunity to hide attached windows that may get in the way. */ - (void)standardUserDriverWillShowModalAlert; /** Called after showing a modal alert window, to give the opportunity to hide attached windows that may get in the way. */ - (void)standardUserDriverDidShowModalAlert; /** Returns an object that formats version numbers for display to the user. If you don't implement this method or return @c nil, the standard version formatter will be used. */ - (_Nullable id )standardUserDriverRequestsVersionDisplayer; /** Decides whether or not the standard user driver should provide an option to show full release notes to the user. When a user checks for new updates and no new update is found, Sparkle by default will offer to show the application's version history to the user by providing a "Version History" button in the no new update available alert. If this delegate method is implemented to return `NO`, then Sparkle will not provide an option to show full release notes to the user. @param item The appcast item corresponding to the latest version available. @return @c YES to allow Sparkle to show full release notes to the user, otherwise @c NO to disallow this. */ - (BOOL)standardUserDriverShouldShowVersionHistoryForAppcastItem:(SUAppcastItem *)item; /** Handles showing the full release notes to the user. When a user checks for new updates and no new update is found, Sparkle will offer to show the application's version history to the user by providing a "Version History" button in the no new update available alert. If this delegate method is not implemented, Sparkle will instead offer to open the `fullReleaseNotesLink` (or `releaseNotesLink` if the former is unavailable) from the appcast's latest `item` in the user's web browser. If this delegate method is implemented, Sparkle will instead ask the delegate to show the full release notes to the user. A delegate may want to implement this method if they want to show in-app or offline release notes. @param item The appcast item corresponding to the latest version available. */ - (void)standardUserDriverShowVersionHistoryForAppcastItem:(SUAppcastItem *)item; /** Specifies whether or not the download, extraction, and installing status windows allows to be minimized. By default, the status window showing the current status of the update (download, extraction, ready to install) is allowed to be minimized for regular application bundle updates. @return @c YES if the status window is allowed to be minimized (default behavior), otherwise @c NO. */ - (BOOL)standardUserDriverAllowsMinimizableStatusWindow; /** Declares whether or not gentle scheduled update reminders are supported. The delegate may implement scheduled update reminders that are presented in a gentle manner by implementing one or both of: `-standardUserDriverWillHandleShowingUpdate:forUpdate:state:` and `-standardUserDriverShouldHandleShowingScheduledUpdate:andInImmediateFocus:` Visit https://sparkle-project.org/documentation/gentle-reminders for more information and examples. @return @c YES if gentle scheduled update reminders are implemented by standard user driver delegate, otherwise @c NO (default). */ @property (nonatomic, readonly) BOOL supportsGentleScheduledUpdateReminders; /** Specifies if the standard user driver should handle showing a new scheduled update, or if its delegate should handle showing the update instead. If you implement this method and return @c NO the delegate is then responsible for showing the update, which must be implemented and done in `-standardUserDriverWillHandleShowingUpdate:forUpdate:state:` The motivation for the delegate being responsible for showing updates is to override Sparkle's default behavior and add gentle reminders for new updates. Returning @c YES is the default behavior and allows the standard user driver to handle showing the update. If the standard user driver handles showing the update, `immediateFocus` reflects whether or not it will show the update in immediate and utmost focus. The standard user driver may choose to show the update in immediate and utmost focus when the app was launched recently or the system has been idle for some time. If `immediateFocus` is @c NO the standard user driver may want to defer showing the update until the user comes back to the app. For background running applications, when `immediateFocus` is @c NO the standard user driver will always want to show the update alert immediately, but behind other running applications or behind the app's own windows if it's currently active. There should be no side effects made when implementing this method so you should just return @c YES or @c NO You will also want to implement `-standardUserDriverWillHandleShowingUpdate:forUpdate:state:` for adding additional update reminders. This method is not called for user-initiated update checks. The standard user driver always handles those. Visit https://sparkle-project.org/documentation/gentle-reminders for more information and examples. @param update The update the standard user driver should show. @param immediateFocus If @c immediateFocus is @c YES, then the standard user driver proposes to show the update in immediate and utmost focus. See discussion for more details. @return @c YES if the standard user should handle showing the scheduled update (default behavior), otherwise @c NO if the delegate handles showing it. */ - (BOOL)standardUserDriverShouldHandleShowingScheduledUpdate:(SUAppcastItem *)update andInImmediateFocus:(BOOL)immediateFocus; /** Called before an update will be shown to the user. If the standard user driver handles showing the update, `handleShowingUpdate` will be `YES`. Please see `-standardUserDriverShouldHandleShowingScheduledUpdate:andInImmediateFocus:` for how the standard user driver may handle showing scheduled updates when `handleShowingUpdate` is `YES` and `state.userInitiated` is `NO`. If the delegate declared it handles showing the update by returning @c NO in `-standardUserDriverShouldHandleShowingScheduledUpdate:andInImmediateFocus:` then the delegate should handle showing update reminders in this method, or at some later point. In this case, `handleShowingUpdate` will be @c NO. To bring the update alert in focus, you may call `-[SPUStandardUpdaterController checkForUpdates:]` or `-[SPUUpdater checkForUpdates]`. You may want to show additional UI indicators in your application that will show this update in focus and want to dismiss additional UI indicators in `-standardUserDriverWillFinishUpdateSession` or `-standardUserDriverDidReceiveUserAttentionForUpdate:` If `state.userInitiated` is @c YES then the standard user driver always handles showing the new update and `handleShowingUpdate` will be @c YES. In this case, it may still be useful for the delegate to intercept this method right before a new update will be shown. This method is not called when bringing an update that has already been presented back in focus. Visit https://sparkle-project.org/documentation/gentle-reminders for more information and examples. @param handleShowingUpdate @c YES if the standard user driver handles showing the update, otherwise @c NO if the delegate handles showing the update. @param update The update that will be shown. @param state The user state of the update which includes if the update check was initiated by the user. */ - (void)standardUserDriverWillHandleShowingUpdate:(BOOL)handleShowingUpdate forUpdate:(SUAppcastItem *)update state:(SPUUserUpdateState *)state; /** Called when a new update first receives attention from the user. This occurs either when the user first brings the update alert in utmost focus or when the user makes a choice to install an update or dismiss/skip it. This may be useful to intercept for dismissing custom attention-based UI indicators (e.g, user notifications) introduced when implementing `-standardUserDriverWillHandleShowingUpdate:forUpdate:state:` For custom UI indicators that need to still be on screen after the user has started to install an update, please see `-standardUserDriverWillFinishUpdateSession`. @param update The new update that the user gave attention to. */ - (void)standardUserDriverDidReceiveUserAttentionForUpdate:(SUAppcastItem *)update; /** Called before the standard user driver session will finish its current update session. This may occur after the user has dismissed / skipped a new update or after an update error has occurred. For updaters updating external/other bundles, this may also be called after an update has been successfully installed. This may be useful to intercept for dismissing custom UI indicators introduced when implementing `-standardUserDriverWillHandleShowingUpdate:forUpdate:state:` For UI indicators that need to be dismissed when the user has given attention to a new update alert, please see `-standardUserDriverDidReceiveUserAttentionForUpdate:` */ - (void)standardUserDriverWillFinishUpdateSession; /** Called before the standard user driver shows plain-text or markdown release notes text to the user. The delegate has the opportunity to return a new attributed string for the release notes text that will be shown to the user. The `bundleDisplayVersion` and `bundleVersion` are supplied in case they're useful for creating a new attributed string. This method will not be invoked for HTML release notes. It is only applicable to plain-text and markdown release notes. @param releaseNotesAttributedString The release notes text that the standard user driver wants to show to the user. @param update The new update the release notes will be shown for. @param bundleDisplayVersion The current display version (or `CFBundleShortVersionString`) of the bundle that is being updated. @param bundleVersion The current version (or `CFBundleVersion`) of the bundle that is being updated. @return A new attributed string for the release notes text to show, or @c nil if the `releaseNotesAttributedString` should still be used. */ - (NSAttributedString * _Nullable)standardUserDriverWillShowReleaseNotesText:(NSAttributedString *)releaseNotesAttributedString forUpdate:(SUAppcastItem *)update withBundleDisplayVersion:(NSString *)bundleDisplayVersion bundleVersion:(NSString *)bundleVersion; @end NS_ASSUME_NONNULL_END ================================================ FILE: Sparkle/SPUStandardVersionDisplay.h ================================================ // // SPUStandardVersionDisplay.h // Sparkle // // Created on 2/18/23. // Copyright © 2023 Sparkle Project. All rights reserved. // #import #import "SUVersionDisplayProtocol.h" NS_ASSUME_NONNULL_BEGIN SPU_OBJC_DIRECT_MEMBERS @interface SPUStandardVersionDisplay : NSObject + (instancetype)standardVersionDisplay; @end NS_ASSUME_NONNULL_END ================================================ FILE: Sparkle/SPUStandardVersionDisplay.m ================================================ // // SPUStandardVersionDisplay.m // Sparkle // // Created on 2/18/23. // Copyright © 2023 Sparkle Project. All rights reserved. // #import "SPUStandardVersionDisplay.h" #import "SUAppcastItem.h" #include "AppKitPrevention.h" @implementation SPUStandardVersionDisplay + (instancetype)standardVersionDisplay { static SPUStandardVersionDisplay *versionDisplay = nil; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ versionDisplay = [[SPUStandardVersionDisplay alloc] init]; }); return versionDisplay; } - (NSString *)formatUpdateDisplayVersionFromUpdate:(SUAppcastItem *)update andBundleDisplayVersion:(NSString * _Nonnull __autoreleasing * _Nonnull)inOutBundleDisplayVersion withBundleVersion:(NSString *)bundleVersion { NSString *outUpdateDisplayVersion; NSString *outBundleDisplayVersion; NSString *updateDisplayVersion = update.displayVersionString; NSString *bundleDisplayVersion = *inOutBundleDisplayVersion; NSString *updateVersion = update.versionString; // If the display versions are the same, then append the internal versions to differentiate them if ([updateDisplayVersion isEqualToString:bundleDisplayVersion]) { outUpdateDisplayVersion = [updateDisplayVersion stringByAppendingFormat:@" (%@)", updateVersion]; outBundleDisplayVersion = [bundleDisplayVersion stringByAppendingFormat:@" (%@)", bundleVersion]; } else { outUpdateDisplayVersion = updateDisplayVersion; outBundleDisplayVersion = bundleDisplayVersion; } *inOutBundleDisplayVersion = outBundleDisplayVersion; return outUpdateDisplayVersion; } @end ================================================ FILE: Sparkle/SPUUIBasedUpdateDriver.h ================================================ // // SPUUIBasedUpdateDriver.h // Sparkle // // Created by Mayur Pawashe on 3/18/16. // Copyright © 2016 Sparkle Project. All rights reserved. // #import #import "SPUUpdateDriver.h" NS_ASSUME_NONNULL_BEGIN @protocol SPUUIBasedUpdateDriverDelegate - (void)basicDriverIsRequestingAbortUpdateWithError:(nullable NSError *)error; - (void)coreDriverIsRequestingAbortUpdateWithError:(nullable NSError *)error; - (void)uiDriverIsRequestingAbortUpdateWithError:(nullable NSError *)error; @optional - (void)uiDriverDidShowUpdate; - (void)basicDriverDidFinishLoadingAppcast; @end @class SUHost; @protocol SPUUserDriver, SPUUpdaterDelegate; SPU_OBJC_DIRECT_MEMBERS @interface SPUUIBasedUpdateDriver : NSObject - (instancetype)initWithHost:(SUHost *)host applicationBundle:(NSBundle *)applicationBundle updater:(id)updater userDriver:(id )userDriver userInitiated:(BOOL)userInitiated updaterDelegate:(nullable id )updaterDelegate delegate:(id)delegate; - (void)setCompletionHandler:(SPUUpdateDriverCompletion)completionBlock; - (void)setUpdateWillInstallHandler:(void (^)(void))updateWillInstallHandler; - (void)checkForUpdatesAtAppcastURL:(NSURL *)appcastURL withUserAgent:(NSString *)userAgent httpHeaders:(NSDictionary * _Nullable)httpHeaders inBackground:(BOOL)background; - (void)resumeInstallingUpdate; - (void)resumeUpdate:(id)resumableUpdate; - (void)abortUpdateWithError:(nullable NSError *)error showErrorToUser:(BOOL)showedUserProgress; @end NS_ASSUME_NONNULL_END ================================================ FILE: Sparkle/SPUUIBasedUpdateDriver.m ================================================ // // SPUUIBasedUpdateDriver.m // Sparkle // // Created by Mayur Pawashe on 3/18/16. // Copyright © 2016 Sparkle Project. All rights reserved. // #import "SPUUIBasedUpdateDriver.h" #import "SPUCoreBasedUpdateDriver.h" #import "SPUUserDriver.h" #import "SUHost.h" #import "SUConstants.h" #import "SPUUpdaterDelegate.h" #import "SUAppcastItem.h" #import "SUErrors.h" #import "SPUDownloadData.h" #import "SPUDownloadDataPrivate.h" #import "SPUExtractSignedFeed.h" #import "SPUResumableUpdate.h" #import "SPUDownloadDriver.h" #import "SPUSkippedUpdate.h" #import "SPUUserUpdateState+Private.h" #import "SUAppcastItem+Private.h" #import "SUSignatures.h" #import "SUSignatureVerifier.h" #import "SPUVerifierInformation.h" #import "SULocalizations.h" #include "AppKitPrevention.h" // Private class for downloading release notes @interface SPUReleaseNotesDriver: NSObject @end @implementation SPUReleaseNotesDriver { SPUDownloadDriver *_downloadDriver; SUHost *_host; SUSignatures *_signatures; void (^_completionHandler)(SPUDownloadData * _Nullable, NSError * _Nullable); uint64_t _contentLength; } - (instancetype)initWithReleaseNotesURL:(NSURL *)releaseNotesURL contentLength:(uint64_t)contentLength signatures:(SUSignatures * _Nullable)signatures httpHeaders:(NSDictionary * _Nullable)httpHeaders userAgent:(NSString * _Nullable)userAgent host:(SUHost *)host completionHandler:(void (^)(SPUDownloadData * _Nullable, NSError * _Nullable))completionHandler SPU_OBJC_DIRECT { self = [super init]; if (self != nil) { _host = host; _signatures = signatures; _contentLength = contentLength; _downloadDriver = [[SPUDownloadDriver alloc] initWithRequestURL:releaseNotesURL host:host userAgent:userAgent httpHeaders:httpHeaders inBackground:NO delegate:self]; _completionHandler = [completionHandler copy]; } else { assert(false); } return self; } - (void)startDownload SPU_OBJC_DIRECT { [_downloadDriver downloadFile]; } - (void)downloadDriverDidDownloadData:(SPUDownloadData *)downloadDataToValidate { if (_completionHandler != nil) { SPUDownloadData *downloadDataToPassToUserDriver; // Strip out any sign warning comment prefix for markdown data so that user drivers // will not have to deal with parsing them (if their markdown parsers don't handle decoding HTML) NSString *MIMEType = downloadDataToValidate.MIMEType; NSString *pathExtension = _downloadDriver.request.URL.pathExtension; if ([MIMEType isEqualToString:@"text/markdown"] || [MIMEType isEqualToString:@"text/x-markdown"] || [pathExtension caseInsensitiveCompare:@"md"] == NSOrderedSame || [pathExtension caseInsensitiveCompare:@"markdown"] == NSOrderedSame) { NSData *contentData = SPUExtractReleaseNotesContent(downloadDataToValidate.data); if (contentData.length != downloadDataToValidate.data.length) { downloadDataToPassToUserDriver = [[SPUDownloadData alloc] initWithData:contentData URL:downloadDataToValidate.URL textEncodingName:downloadDataToValidate.textEncodingName MIMEType:downloadDataToValidate.MIMEType]; } else { downloadDataToPassToUserDriver = downloadDataToValidate; } } else { downloadDataToPassToUserDriver = downloadDataToValidate; } if (_host.requiresSignedAppcast) { SUSignatureVerifier *signatureVerifier = [[SUSignatureVerifier alloc] initWithPublicKeys:_host.publicKeys]; SPUVerifierInformation *verifierInformation = [[SPUVerifierInformation alloc] initWithExpectedVersion:nil expectedContentLength:_contentLength]; verifierInformation.actualContentLength = downloadDataToValidate.data.length; NSError *verifierError = nil; if (![signatureVerifier verifyData:downloadDataToValidate.data signatures:_signatures fileKind:@"release notes" verifierInformation:verifierInformation error:&verifierError]) { NSMutableDictionary *userInfo = [NSMutableDictionary dictionaryWithDictionary:@{NSLocalizedDescriptionKey:SULocalizedStringFromTableInBundle(@"The release notes is improperly signed and could not be validated. Please contact the app developer for more information.", SPARKLE_TABLE, SUSparkleBundle(), nil)}]; if (verifierError != nil) { [userInfo setObject:verifierError forKey:NSUnderlyingErrorKey]; } _completionHandler(nil, [NSError errorWithDomain:SUSparkleErrorDomain code:SUDownloadError userInfo:userInfo]); } else { _completionHandler(downloadDataToPassToUserDriver, nil); } } else { _completionHandler(downloadDataToPassToUserDriver, nil); } _completionHandler = nil; } } - (void)downloadDriverDidFailToDownloadFileWithError:(nonnull NSError *)error { if (_completionHandler != nil) { NSMutableDictionary *userInfo = [NSMutableDictionary dictionaryWithDictionary:@{NSLocalizedDescriptionKey:SULocalizedStringFromTableInBundle(@"An error occurred while downloading the release notes.", SPARKLE_TABLE, SUSparkleBundle(), nil)}]; if (error != nil) { [userInfo setObject:error forKey:NSUnderlyingErrorKey]; } _completionHandler(nil, [NSError errorWithDomain:SUSparkleErrorDomain code:SUDownloadError userInfo:userInfo]); _completionHandler = nil; } } - (void)cleanup:(void (^)(void))cleanupHandler SPU_OBJC_DIRECT { _completionHandler = nil; [_downloadDriver cleanup:cleanupHandler]; } @end @interface SPUUIBasedUpdateDriver() @end @implementation SPUUIBasedUpdateDriver { SPUCoreBasedUpdateDriver *_coreDriver; SUHost *_host; id _userDriver; SPUReleaseNotesDriver *_releaseNotesDriver; NSDictionary *_httpHeaders; NSString *_userAgent; __weak id _updater; __weak id _updaterDelegate; __weak id _delegate; BOOL _userInitiated; BOOL _resumingInstallingUpdate; BOOL _resumingDownloadedInfoOrUpdate; } - (instancetype)initWithHost:(SUHost *)host applicationBundle:(NSBundle *)applicationBundle updater:(id)updater userDriver:(id )userDriver userInitiated:(BOOL)userInitiated updaterDelegate:(nullable id )updaterDelegate delegate:(id)delegate { self = [super init]; if (self != nil) { _userDriver = userDriver; _delegate = delegate; _updater = updater; _userInitiated = userInitiated; _updaterDelegate = updaterDelegate; _host = host; SPUUpdateCheck updateCheck = userInitiated ? SPUUpdateCheckUpdates : SPUUpdateCheckUpdatesInBackground; _coreDriver = [[SPUCoreBasedUpdateDriver alloc] initWithHost:host applicationBundle:applicationBundle updateCheck:updateCheck updater:updater updaterDelegate:updaterDelegate delegate:self]; } return self; } - (void)setCompletionHandler:(SPUUpdateDriverCompletion)completionBlock { [_coreDriver setCompletionHandler:completionBlock]; } - (void)setUpdateWillInstallHandler:(void (^)(void))updateWillInstallHandler { [_coreDriver setUpdateWillInstallHandler:updateWillInstallHandler]; } - (void)_clearSkippedUpdatesIfUserInitiated SPU_OBJC_DIRECT { if (_userInitiated) { [SPUSkippedUpdate clearSkippedUpdateForHost:_host]; } } - (void)checkForUpdatesAtAppcastURL:(NSURL *)appcastURL withUserAgent:(NSString *)userAgent httpHeaders:(NSDictionary * _Nullable)httpHeaders inBackground:(BOOL)background { _httpHeaders = httpHeaders; _userAgent = userAgent; [self _clearSkippedUpdatesIfUserInitiated]; [_coreDriver checkForUpdatesAtAppcastURL:appcastURL withUserAgent:userAgent httpHeaders:httpHeaders inBackground:background requiresSilentInstall:NO]; } - (void)resumeInstallingUpdate { [self _clearSkippedUpdatesIfUserInitiated]; _resumingInstallingUpdate = YES; [_coreDriver resumeInstallingUpdate]; } - (void)resumeUpdate:(id)resumableUpdate { [self _clearSkippedUpdatesIfUserInitiated]; _resumingDownloadedInfoOrUpdate = YES; [_coreDriver resumeUpdate:resumableUpdate]; } - (void)basicDriverDidFinishLoadingAppcast { id delegate = _delegate; if ([delegate respondsToSelector:@selector(basicDriverDidFinishLoadingAppcast)]) { [delegate basicDriverDidFinishLoadingAppcast]; } } - (void)basicDriverDidFindUpdateWithAppcastItem:(SUAppcastItem *)updateItem secondaryAppcastItem:(SUAppcastItem * _Nullable)secondaryUpdateItem { id updaterDelegate = _updaterDelegate; id delegate = _delegate; SPUUserUpdateStage stage; // Major upgrades and information only updates are not downloaded automatically, as well as feeds that failed signing validation if (_resumingDownloadedInfoOrUpdate && !updateItem.majorUpgrade && !updateItem.informationOnlyUpdate && updateItem.signingValidationStatus != SPUAppcastSigningValidationStatusFailed) { stage = SPUUserUpdateStageDownloaded; } else if (_resumingInstallingUpdate) { stage = SPUUserUpdateStageInstalling; } else { stage = SPUUserUpdateStageNotDownloaded; } SPUUserUpdateState *state = [[SPUUserUpdateState alloc] initWithStage:stage userInitiated:_userInitiated]; [_userDriver showUpdateFoundWithAppcastItem:updateItem state:state reply:^(SPUUserUpdateChoice userChoice) { dispatch_async(dispatch_get_main_queue(), ^{ // Rule out invalid choices SPUUserUpdateChoice validatedChoice; if (updateItem.isInformationOnlyUpdate && userChoice == SPUUserUpdateChoiceInstall) { validatedChoice = SPUUserUpdateChoiceDismiss; } else { validatedChoice = userChoice; } id updater = self->_updater; if (updater != nil) { if ([updaterDelegate respondsToSelector:@selector(updater:userDidMakeChoice:forUpdate:state:)]) { [updaterDelegate updater:updater userDidMakeChoice:validatedChoice forUpdate:updateItem state:state]; } else if (validatedChoice == SPUUserUpdateChoiceSkip && [updaterDelegate respondsToSelector:@selector(updater:userDidSkipThisVersion:)]) { #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdeprecated-declarations" [updaterDelegate updater:updater userDidSkipThisVersion:updateItem]; #pragma clang diagnostic pop } } switch (validatedChoice) { case SPUUserUpdateChoiceInstall: { switch (stage) { case SPUUserUpdateStageDownloaded: [self->_coreDriver extractDownloadedUpdate]; break; case SPUUserUpdateStageInstalling: [self->_coreDriver finishInstallationWithResponse:validatedChoice displayingUserInterface:YES]; break; case SPUUserUpdateStageNotDownloaded: [self->_coreDriver downloadUpdateFromAppcastItem:updateItem secondaryAppcastItem:secondaryUpdateItem inBackground:NO]; break; } break; } case SPUUserUpdateChoiceSkip: { [SPUSkippedUpdate skipUpdate:updateItem host:self->_host]; switch (stage) { case SPUUserUpdateStageDownloaded: case SPUUserUpdateStageNotDownloaded: // Informational and major updates can be resumed too, so make sure we check // self->_resumingDownloadedInfoOrUpdate instead of the stage we pass to user driver if (self->_resumingDownloadedInfoOrUpdate) { [self->_coreDriver clearDownloadedUpdate]; } [delegate uiDriverIsRequestingAbortUpdateWithError:nil]; break; case SPUUserUpdateStageInstalling: [self->_coreDriver finishInstallationWithResponse:validatedChoice displayingUserInterface:YES]; break; } break; } case SPUUserUpdateChoiceDismiss: { switch (stage) { case SPUUserUpdateStageDownloaded: case SPUUserUpdateStageNotDownloaded: { [self->_delegate uiDriverIsRequestingAbortUpdateWithError:nil]; break; } case SPUUserUpdateStageInstalling: { [self->_coreDriver finishInstallationWithResponse:validatedChoice displayingUserInterface:YES]; break; } } break; } } }); }]; if ([delegate respondsToSelector:@selector(uiDriverDidShowUpdate)]) { [delegate uiDriverDidShowUpdate]; } if (updateItem.releaseNotesURL != nil && (![updaterDelegate respondsToSelector:@selector(updater:shouldDownloadReleaseNotesForUpdate:)] || [updaterDelegate updater:_updater shouldDownloadReleaseNotesForUpdate:updateItem])) { __weak __typeof__(self) weakSelf = self; _releaseNotesDriver = [[SPUReleaseNotesDriver alloc] initWithReleaseNotesURL:updateItem.releaseNotesURL contentLength:updateItem.releaseNotesContentLength signatures:updateItem.releaseNotesSignatures httpHeaders:_httpHeaders userAgent:_userAgent host:_host completionHandler:^(SPUDownloadData * _Nullable downloadData, NSError * _Nullable error) { __typeof__(self) strongSelf = weakSelf; if (strongSelf != nil) { id userDriver = strongSelf->_userDriver; if (downloadData != nil) { [userDriver showUpdateReleaseNotesWithDownloadData:(SPUDownloadData * _Nonnull)downloadData]; } else { [userDriver showUpdateReleaseNotesFailedToDownloadWithError:(NSError * _Nonnull)error]; } } }]; [_releaseNotesDriver startDownload]; } } - (void)downloadDriverWillBeginDownload { void (^cancelDownload)(void) = ^{ dispatch_async(dispatch_get_main_queue(), ^{ id updaterDelegate = self->_updaterDelegate; if ([updaterDelegate respondsToSelector:@selector((userDidCancelDownload:))]) { [updaterDelegate userDidCancelDownload:self->_updater]; } [self->_delegate uiDriverIsRequestingAbortUpdateWithError:nil]; }); }; [_userDriver showDownloadInitiatedWithCancellation:cancelDownload]; } - (void)downloadDriverDidReceiveExpectedContentLength:(uint64_t)expectedContentLength { [_userDriver showDownloadDidReceiveExpectedContentLength:expectedContentLength]; } - (void)downloadDriverDidReceiveDataOfLength:(uint64_t)length { [_userDriver showDownloadDidReceiveDataOfLength:length]; } - (void)coreDriverDidStartExtractingUpdate { [_userDriver showDownloadDidStartExtractingUpdate]; } - (void)installerDidStartInstallingWithApplicationTerminated:(BOOL)applicationTerminated { if ([_userDriver respondsToSelector:@selector(showInstallingUpdateWithApplicationTerminated:retryTerminatingApplication:)]) { __weak __typeof__(self) weakSelf = self; [_userDriver showInstallingUpdateWithApplicationTerminated:applicationTerminated retryTerminatingApplication:^{ if (!applicationTerminated) { dispatch_async(dispatch_get_main_queue(), ^{ __typeof__(self) strongSelf = weakSelf; if (strongSelf != nil) { [strongSelf->_coreDriver finishInstallationWithResponse:SPUUserUpdateChoiceInstall displayingUserInterface:YES]; } }); } }]; } else if ([_userDriver respondsToSelector:@selector(showInstallingUpdateWithApplicationTerminated:)]) { #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdeprecated-declarations" [_userDriver showInstallingUpdateWithApplicationTerminated:applicationTerminated]; #pragma clang diagnostic pop } else { #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdeprecated-declarations" if ([_userDriver respondsToSelector:@selector(showInstallingUpdate)]) { [_userDriver showInstallingUpdate]; } if (!applicationTerminated) { if ([_userDriver respondsToSelector:@selector(showSendingTerminationSignal)]) { [_userDriver showSendingTerminationSignal]; } } #pragma clang diagnostic pop } } - (void)installerDidExtractUpdateWithProgress:(double)progress { [_userDriver showExtractionReceivedProgress:progress]; } - (void)installerDidFinishPreparationAndWillInstallImmediately:(BOOL)willInstallImmediately { if (!willInstallImmediately) { [_userDriver showReadyToInstallAndRelaunch:^(SPUUserUpdateChoice choice) { dispatch_async(dispatch_get_main_queue(), ^{ [self->_coreDriver finishInstallationWithResponse:choice displayingUserInterface:YES]; }); }]; } } - (void)installerDidFinishInstallationAndRelaunched:(BOOL)relaunched acknowledgement:(void(^)(void))acknowledgement { if ([_userDriver respondsToSelector:@selector(showUpdateInstalledAndRelaunched:acknowledgement:)]) { [_userDriver showUpdateInstalledAndRelaunched:relaunched acknowledgement:acknowledgement]; } else { #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdeprecated-declarations" [_userDriver showUpdateInstallationDidFinishWithAcknowledgement:acknowledgement]; #pragma clang diagnostic pop } } - (void)basicDriverIsRequestingAbortUpdateWithError:(nullable NSError *)error { // A delegate may want to handle this type of error specially [_delegate basicDriverIsRequestingAbortUpdateWithError:error]; } - (void)coreDriverIsRequestingAbortUpdateWithError:(NSError *)error { // A delegate may want to handle this type of error specially [_delegate coreDriverIsRequestingAbortUpdateWithError:error]; } - (void)_abortUpdateWithError:(nullable NSError *)error showErrorToUser:(BOOL)showErrorToUser SPU_OBJC_DIRECT { void (^abortUpdate)(void) = ^{ if (showErrorToUser) { [self->_userDriver dismissUpdateInstallation]; } [self->_coreDriver abortUpdateAndShowNextUpdateImmediately:NO error:error]; }; if (error != nil && showErrorToUser) { NSError *nonNullError = error; if (error.code == SUNoUpdateError) { if ([_userDriver respondsToSelector:@selector(showUpdateNotFoundWithError:acknowledgement:)]) { [_userDriver showUpdateNotFoundWithError:(NSError * _Nonnull)error acknowledgement:^{ dispatch_async(dispatch_get_main_queue(), ^{ abortUpdate(); }); }]; } else if ([_userDriver respondsToSelector:@selector(showUpdateNotFoundWithAcknowledgement:)]) { // Eventually we should remove this fallback once clients adopt -showUpdateNotFoundWithError:acknowledgement: #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdeprecated-declarations" [_userDriver showUpdateNotFoundWithAcknowledgement:^{ #pragma clang diagnostic pop dispatch_async(dispatch_get_main_queue(), ^{ abortUpdate(); }); }]; } } else if (error.code == SUInstallationCanceledError || error.code == SUInstallationAuthorizeLaterError) { abortUpdate(); } else { [_userDriver showUpdaterError:nonNullError acknowledgement:^{ dispatch_async(dispatch_get_main_queue(), ^{ abortUpdate(); }); }]; } } else { abortUpdate(); } } - (void)abortUpdateWithError:(nullable NSError *)error showErrorToUser:(BOOL)showErrorToUser { if (_releaseNotesDriver != nil) { [_releaseNotesDriver cleanup:^{ [self _abortUpdateWithError:error showErrorToUser:showErrorToUser]; }]; } else { [self _abortUpdateWithError:error showErrorToUser:showErrorToUser]; } } @end ================================================ FILE: Sparkle/SPUUpdateCheck.h ================================================ // // SPUUpdateCheck.h // SPUUpdateCheck // // Created by Mayur Pawashe on 8/28/21. // Copyright © 2021 Sparkle Project. All rights reserved. // #ifndef SPUUpdateCheck_h #define SPUUpdateCheck_h /** Describes the type of update check being performed. Each update check corresponds to an update check method on `SPUUpdater`. */ typedef NS_ENUM(NSInteger, SPUUpdateCheck) { /** The user-initiated update check corresponding to `-[SPUUpdater checkForUpdates]`. */ SPUUpdateCheckUpdates = 0, /** The background scheduled update check corresponding to `-[SPUUpdater checkForUpdatesInBackground]`. */ SPUUpdateCheckUpdatesInBackground = 1, /** The informational probe update check corresponding to `-[SPUUpdater checkForUpdateInformation]`. */ SPUUpdateCheckUpdateInformation = 2 }; #endif /* SPUUpdateCheck_h */ ================================================ FILE: Sparkle/SPUUpdateDriver.h ================================================ // // SPUUpdateDriver.h // Sparkle // // Created by Mayur Pawashe on 3/15/16. // Copyright © 2016 Sparkle Project. All rights reserved. // NS_ASSUME_NONNULL_BEGIN @protocol SPUResumableUpdate; typedef void (^SPUUpdateDriverCompletion)(BOOL shouldShowUpdateImmediately, id _Nullable resumableUpdate, NSError * _Nullable error); // This protocol describes an update driver that drives updates // An update driver may have multiple levels of other controller components (eg: basic update driver, core based update driver, ui based update driver, appcast driver, etc) // The update driver and the components the driver has communicates via parameter passing and delegation.. // The old Sparkle architecture communicated via subclassing and method overriding, but this lead to bugs due to high coupling, and complexity of not being aware of methods being executed. // The newer architecture is still complex but should be more reliable to maintain and extend. @protocol SPUUpdateDriver - (void)setCompletionHandler:(SPUUpdateDriverCompletion)completionBlock; - (void)setUpdateShownHandler:(void (^)(void))updateShownHandler; - (void)setUpdateWillInstallHandler:(void (^)(void))updateWillInstallHandler; - (void)checkForUpdatesAtAppcastURL:(NSURL *)appcastURL withUserAgent:(NSString *)userAgent httpHeaders:(NSDictionary * _Nullable)httpHeaders; - (void)resumeInstallingUpdate; - (void)resumeUpdate:(id)resumableUpdate; @property (nonatomic, readonly) BOOL showingUpdate; // A likely implementation of -abortUpdate is invoking -abortUpdateWithError: by passing nil - (void)abortUpdate; // This should be invoked on the update driver to finish the update driver's work - (void)abortUpdateWithError:(NSError * _Nullable)error; @end NS_ASSUME_NONNULL_END ================================================ FILE: Sparkle/SPUUpdatePermissionRequest.h ================================================ // // SPUUpdatePermissionRequest.h // Sparkle // // Created by Mayur Pawashe on 8/14/16. // Copyright © 2016 Sparkle Project. All rights reserved. // #import #if defined(BUILDING_SPARKLE_SOURCES_EXTERNALLY) // Ignore incorrect warning #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wquoted-include-in-framework-header" #import "SUExport.h" #pragma clang diagnostic pop #else #import #endif NS_ASSUME_NONNULL_BEGIN /** This class represents information needed to make a permission request for checking updates. */ SU_EXPORT NS_SWIFT_SENDABLE @interface SPUUpdatePermissionRequest : NSObject /** Initializes a new update permission request instance. @param systemProfile The system profile information. */ - (instancetype)initWithSystemProfile:(NSArray *> *)systemProfile; /** A read-only property for the user's system profile. */ @property (nonatomic, readonly) NSArray *> *systemProfile; @end NS_ASSUME_NONNULL_END ================================================ FILE: Sparkle/SPUUpdatePermissionRequest.m ================================================ // // SPUUpdatePermissionRequest.m // Sparkle // // Created by Mayur Pawashe on 8/14/16. // Copyright © 2016 Sparkle Project. All rights reserved. // #import "SPUUpdatePermissionRequest.h" #include "AppKitPrevention.h" static NSString *SPUUpdatePermissionRequestSystemProfileKey = @"SPUUpdatePermissionRequestSystemProfile"; @implementation SPUUpdatePermissionRequest @synthesize systemProfile = _systemProfile; + (BOOL)supportsSecureCoding { return YES; } - (instancetype)initWithCoder:(NSCoder *)decoder { NSArray *> *systemProfile = [decoder decodeObjectOfClasses:[NSSet setWithArray:@[[NSArray class], [NSDictionary class], [NSString class]]] forKey:SPUUpdatePermissionRequestSystemProfileKey]; if (systemProfile == nil) { return nil; } return [self initWithSystemProfile:systemProfile]; } - (void)encodeWithCoder:(NSCoder *)encoder { [encoder encodeObject:_systemProfile forKey:SPUUpdatePermissionRequestSystemProfileKey]; } - (instancetype)initWithSystemProfile:(NSArray *> *)systemProfile { self = [super init]; if (self != nil) { _systemProfile = systemProfile; } return self; } @end ================================================ FILE: Sparkle/SPUUpdater.h ================================================ // // SPUUpdater.h // Sparkle // // Created by Andy Matuschak on 1/4/06. // Copyright 2006 Andy Matuschak. All rights reserved. // #import #if defined(BUILDING_SPARKLE_SOURCES_EXTERNALLY) // Ignore incorrect warning #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wquoted-include-in-framework-header" #import "SUExport.h" #import "SPUUserDriver.h" #pragma clang diagnostic pop #else #import #import #endif NS_ASSUME_NONNULL_BEGIN @class SUAppcastItem, SUAppcast; @protocol SPUUpdaterDelegate; /** The main API in Sparkle for controlling the update mechanism. This class is used to configure the update parameters as well as manually and automatically schedule and control checks for updates. For convenience, you can create a standard or nib instantiable updater by using `SPUStandardUpdaterController`. Prefer to set initial properties in your bundle's Info.plist as described in [Customizing Sparkle](https://sparkle-project.org/documentation/customization/). Otherwise only if you need dynamic behavior for user settings should you set properties on the updater such as: - `automaticallyChecksForUpdates` - `updateCheckInterval` - `automaticallyDownloadsUpdates` - `feedURL` Please view the documentation on each of these properties for more detail if you are to configure them dynamically. This class must be used on the main thread. */ SU_EXPORT NS_SWIFT_UI_ACTOR @interface SPUUpdater : NSObject /** Initializes a new `SPUUpdater` instance This creates an updater, but to start it and schedule update checks `-startUpdater:` needs to be invoked first. Related: See `SPUStandardUpdaterController` which wraps a `SPUUpdater` instance and is suitable for instantiating inside of nib files. @param hostBundle The bundle that should be targeted for updating. @param applicationBundle The application bundle that should be waited for termination and relaunched (unless overridden). Usually this can be the same as hostBundle. This may differ when updating a plug-in or other non-application bundle. @param userDriver The user driver that Sparkle uses for user update interaction. @param delegate The delegate for `SPUUpdater`. Note the updater weakly references the delegate, so you are responsible for keeping it alive. */ - (instancetype)initWithHostBundle:(NSBundle *)hostBundle applicationBundle:(NSBundle *)applicationBundle userDriver:(id )userDriver delegate:(nullable id)delegate; /** Use `-initWithHostBundle:applicationBundle:userDriver:delegate:` or `SPUStandardUpdaterController` standard adapter instead. If you want to drop an updater into a nib, use `SPUStandardUpdaterController`. */ - (instancetype)init NS_UNAVAILABLE; /** Starts the updater. This method first checks if Sparkle is configured properly. A valid feed URL should be set before this method is invoked. If the configuration is valid, an update cycle is started in the next main runloop cycle. During this cycle, a permission prompt may be brought up (if needed) for checking if the user wants automatic update checking. Otherwise if automatic update checks are enabled, a scheduled update alert may be brought up if enough time has elapsed since the last check. See `automaticallyChecksForUpdates` for more information. After starting the updater and before the next runloop cycle, one of `-checkForUpdates`, `-checkForUpdatesInBackground`, or `-checkForUpdateInformation` can be invoked. This may be useful if you want to check for updates immediately or without showing a potential permission prompt. If the updater cannot be started (i.e, due to a configuration issue in the application), you may want to fall back appropriately. For example, the standard updater controller (`SPUStandardUpdaterController`) alerts the user that the app is misconfigured and to contact the developer. This must be called on the main thread. @param error The error that is populated if this method fails. Pass NULL if not interested in the error information. @return YES if the updater started otherwise NO with a populated error */ - (BOOL)startUpdater:(NSError * __autoreleasing *)error; /** Checks for new updates, and displays progress while doing so if needed. This is meant for users initiating a new update check or checking the current update progress. If an update hasn't started, the user may be shown that a new check for updates is occurring. If an update has already been downloaded or begun installing from a previous session, the user may be presented to install that update. If the user is already being presented with an update or update permission prompt, that notice may be shown to the user in active focus (as long as the user driver is the standard `SPUStandardUserDriver` or if it implements `-[SPUUserDriver showUpdateInFocus]`). This will find updates that the user has previously opted into skipping. See `canCheckForUpdates` property which can determine when this method may be invoked. This must be called on the main thread. */ - (void)checkForUpdates; /** Checks for new updates in the background. You usually should not call this method directly. By default Sparkle calls this method automatically on a scheduled basis if automatic update checks are enabled. This is done by checking the current state of `automaticallyChecksForUpdates`, `updateCheckInterval`, `lastUpdateCheckDate`, and `SUScheduledImpatientCheckInterval`. If you want to additionally force an update check on every app launch though, it's recommended to only call this method immediately after starting the updater, and only when automatic update checks are enabled (by checking `automaticallyChecksForUpdates` is `YES`). Calling this method at later points could interfere with Sparkle's scheduler in unexpected ways. If you want to reset the updater's cycle after an updater setting change, please use `resetUpdateCycle` or `resetUpdateCycleAfterShortDelay` instead. Updates that are found may not be presented immediately to the user, either due to automatic downloading/installing of updates being on or due to gentle reminders https://sparkle-project.org/documentation/gentle-reminders/ for example. Updates that have been skipped by the user will not be found. This method does not do anything if there is a `sessionInProgress`. This must be called on the main thread. */ - (void)checkForUpdatesInBackground; /** Begins a "probing" check for updates which will not actually offer to update to that version. However, the delegate methods `-[SPUUpdaterDelegate updater:didFindValidUpdate:]` and `-[SPUUpdaterDelegate updaterDidNotFindUpdate:]` will be called, so you can use that information in your UI. `-[SPUUpdaterDelegate updater:didFinishUpdateCycleForUpdateCheck:error:]` will be called when this probing check is completed. Updates that have been skipped by the user will not be found. This method does not do anything if there is a `sessionInProgress`. This must be called on the main thread. */ - (void)checkForUpdateInformation; /** A property indicating whether or not updates can be checked by the user. An update check can be made by the user when an update session isn't in progress. An update check can also be made when an update or its progress is being shown to the user (as long as the user driver is the standard `SPUStandardUserDriver` or if it implements `-[SPUUserDriver showUpdateInFocus]`). A user cannot check for updates when data (such as the feed or an update) is still being downloaded automatically in the background. This property is suitable to use for menu item validation for seeing if `-checkForUpdates` can be invoked. This property is also KVO-compliant. Note this property does not reflect whether or not an update session is in progress. Please see `sessionInProgress` property instead. */ @property (nonatomic, readonly) BOOL canCheckForUpdates; /** A property indicating whether or not an update session is in progress. An update session is in progress when the appcast is being downloaded, an update is being downloaded, an update is being shown, update permission is being requested, or the installer is being started. An active session is when Sparkle's fired scheduler is running. Note an update session may not be running even though Sparkle's installer (ran as a separate process) may be running, or even though the update has been downloaded but the installation has been deferred. In both of these cases, a new update session may be activated with the update resumed at a later point (automatically or manually). See also: - `canCheckForUpdates` property which is more suited for menu item validation and deciding if the user can initiate update checks. - `-[SPUUpdaterDelegate updater:didFinishUpdateCycleForUpdateCheck:error:]` which lets the updater delegate know when an update cycle and session finishes. */ @property (nonatomic, readonly) BOOL sessionInProgress; /** A property indicating whether or not to check for updates automatically. By default, Sparkle asks users on second launch for permission if they want automatic update checks enabled and sets this property based on their response. If `SUEnableAutomaticChecks` is set in the Info.plist, this permission request is not performed however. Setting this property will persist in the host bundle's user defaults. Hence developers shouldn't maintain an additional user default for this property. Only set this property if the user wants to change the default via a user settings option. Do not always set it on launch unless you want to ignore the user's preference. For testing environments, you can disable update checks by passing `-SUEnableAutomaticChecks NO` to your app's command line arguments instead of setting this property. The update schedule cycle will be reset in a short delay after the property's new value is set. This is to allow reverting this property without kicking off a schedule change immediately. This property is KVO compliant. This property must be called on the main thread. */ @property (nonatomic) BOOL automaticallyChecksForUpdates; /** A property indicating the current automatic update check interval in seconds. Prefer to set SUScheduledCheckInterval directly in your Info.plist for setting the initial value. Setting this property will persist in the host bundle's user defaults. Hence developers shouldn't maintain an additional user default for this property. Only set this property if the user wants to change the default via a user settings option. Do not always set it on launch unless you want to ignore the user's preference. The update schedule cycle will be reset in a short delay after the property's new value is set. This is to allow reverting this property without kicking off a schedule change immediately. This property is KVO compliant. This property must be called on the main thread. */ @property (nonatomic) NSTimeInterval updateCheckInterval; /** A property indicating whether or not updates can be automatically downloaded in the background. By default, updates are not automatically downloaded. By default starting from Sparkle 2.4, users are provided an option to opt in to automatically downloading and installing updates when they are asked if they want automatic update checks enabled. The default value for this option is based on what the developer sets `SUAutomaticallyUpdate` in their Info.plist. This is not done if `SUEnableAutomaticChecks` is set in the Info.plist however. Please check `automaticallyChecksForUpdates` property for more details. Note that the developer can disallow automatic downloading of updates from being enabled (via `SUAllowsAutomaticUpdates` Info.plist key). In this case, this property will return NO regardless of how this property is set. Prefer to set `SUAutomaticallyUpdate` directly in your Info.plist for setting the initial value. Setting this property will persist in the host bundle's user defaults. Hence developers shouldn't maintain an additional user default for this property. Only set this property if the user wants to change the default via a user settings option. Do not always set it on launch unless you want to ignore the user's preference. This property is KVO compliant. This property must be called on the main thread. */ @property (nonatomic) BOOL automaticallyDownloadsUpdates; /** A property indicating whether or not the *option* to automatically download updates in the background can be turned on. This property can be used to determine whether an option to automatically download/install updates should be enabled. Its value depends on `automaticallyChecksForUpdates`, or the `SUAllowsAutomaticUpdates`in the host bundle's Info.plist if specified. Don't set `SUAllowsAutomaticUpdates` in the Info.plist unless you need custom behavior. This property is KVO compliant. This property must be called on the main thread. */ @property (nonatomic, readonly) BOOL allowsAutomaticUpdates; /** The URL of the appcast used to download update information. If the updater's delegate implements `-[SPUUpdaterDelegate feedURLStringForUpdater:]`, this will return that feed URL. Otherwise if the feed URL has been set before using `-[SPUUpdater setFeedURL:]`, the feed URL returned will be retrieved from the host bundle's user defaults. Otherwise the feed URL in the host bundle's Info.plist will be returned. If no feed URL can be retrieved, returns nil. For setting a primary feed URL, please set the `SUFeedURL` property in your Info.plist. For setting an alternative feed URL, please prefer `-[SPUUpdaterDelegate feedURLStringForUpdater:]` over `-setFeedURL:`. Please see the documentation for `-setFeedURL:` for migrating away from that API. This property must be called on the main thread; calls from background threads will return nil. */ @property (nonatomic, readonly, nullable) NSURL *feedURL; /** Set the URL of the appcast used to download update information. This method is deprecated. Setting this property will persist in the host bundle's user defaults. To avoid this undesirable behavior, please consider implementing `-[SPUUpdaterDelegate feedURLStringForUpdater:]` instead of using this method. Calling `-clearFeedURLFromUserDefaults` will remove any feed URL that has been set in the host bundle's user defaults. Passing nil to this method can also do this, but using `-clearFeedURLFromUserDefaults` is preferred. To migrate away from using this API, you must clear and remove any feed URLs set in the user defaults through this API. If you do not need to alternate between multiple feeds, set the SUFeedURL in your Info.plist instead of invoking this method. For beta updates, you may consider migrating to `-[SPUUpdaterDelegate allowedChannelsForUpdater:]` in the future. Updaters that update other developer's bundles should not call this method. This method must be called on the main thread; calls from background threads will have no effect. */ - (void)setFeedURL:(nullable NSURL *)feedURL __deprecated_msg("Please call -[SPUUpdater clearFeedURLFromUserDefaults] to migrate away from using this API and transition to either specifying the feed URL in your Info.plist, using channels in Sparkle 2, or using -[SPUUpdaterDelegate feedURLStringForUpdater:] to specify the dynamic feed URL at runtime"); /** Clears any feed URL from the host bundle's user defaults that was set via `-setFeedURL:` You should call this method if you have used `-setFeedURL:` in the past and want to stop using that API. Otherwise for compatibility Sparkle will prefer to use the feed URL that was set in the user defaults over the one that was specified in the host bundle's Info.plist, which is often undesirable (except for testing purposes). If a feed URL is found stored in the host bundle's user defaults (from calling `-setFeedURL:`) before it gets cleared, then that previously set URL is returned from this method. This method should be called as soon as possible, after your application finished launching or right after the updater has been started if you manually manage starting the updater. Updaters that update other developer's bundles should not call this method. This method must be called on the main thread. @return A previously set feed URL in the host bundle's user defaults, if available, otherwise this returns `nil` */ - (nullable NSURL *)clearFeedURLFromUserDefaults; /** The host bundle that is being updated. */ @property (nonatomic, readonly) NSBundle *hostBundle; /** The user agent used when checking for updates. By default the user agent string returned is in the format: `$(BundleDisplayName)/$(BundleDisplayVersion) Sparkle/$(SparkleDisplayVersion)` BundleDisplayVersion is derived from the main application's Info.plist's CFBundleShortVersionString. Note if Sparkle is being used to update another application, the bundle information retrieved is from the main application performing the updating. This default implementation can be overridden. */ @property (nonatomic, copy) NSString *userAgentString; /** The HTTP headers used when checking for updates, downloading release notes, and downloading updates. The keys of this dictionary are HTTP header fields and values are corresponding values. */ @property (nonatomic, copy, nullable) NSDictionary *httpHeaders; /** A property indicating whether or not the user's system profile information is sent when checking for updates. Setting this property will persist in the host bundle's user defaults. This property is KVO compliant. This property must be called on the main thread. */ @property (nonatomic) BOOL sendsSystemProfile; /** The date of the last update check or nil if no check has been performed yet. For testing purposes, the last update check is stored in the `SULastCheckTime` key in the host bundle's user defaults. For example, `defaults delete my-bundle-id SULastCheckTime` can be invoked to clear the last update check time and test if update checks are automatically scheduled. This property must be called on the main thread. */ @property (nonatomic, readonly, copy, nullable) NSDate *lastUpdateCheckDate; /** Appropriately re-schedules the update checking timer according to the current updater settings. This method should only be called in response to a user changing updater settings. This method may trigger a new update check to occur in the background if an updater setting such as the updater's feed or allowed channels has changed. If the `updateCheckInterval` or `automaticallyChecksForUpdates` properties are changed, this method is automatically invoked after a short delay using `-resetUpdateCycleAfterShortDelay`. In these cases, manually resetting the update cycle is not necessary. See also `-resetUpdateCycleAfterShortDelay` which gives the user a short delay before triggering a cycle reset. This must be called on the main thread. */ - (void)resetUpdateCycle; /** Appropriately re-schedules the update checking timer according to the current updater settings after a short cancellable delay. This method calls `resetUpdateCycle` after a short delay to give the user a short amount of time to cancel changing an updater setting. If this method is called again, any previous reset request that is still inflight will be cancelled. For example, if the user changes the `automaticallyChecksForUpdates` setting to `YES`, but quickly undoes their change then no cycle reset will be done. If the `updateCheckInterval` or `automaticallyChecksForUpdates` properties are changed, this method is automatically invoked. In these cases, manually resetting the update cycle is not necessary. This must be called on the main thread. */ - (void)resetUpdateCycleAfterShortDelay; /** The system profile information that is sent when checking for updates. */ @property (nonatomic, readonly, copy) NSArray *> *systemProfileArray; @end NS_ASSUME_NONNULL_END ================================================ FILE: Sparkle/SPUUpdater.m ================================================ // // SPUUpdater.m // Sparkle // // Created by Andy Matuschak on 1/4/06. // Copyright 2006 Andy Matuschak. All rights reserved. // #import "SPUUpdater.h" #import "SPUUpdaterDelegate.h" #import "SPUUpdaterSettings.h" #import "SPUUpdaterSettings+Debug.h" #import "SUHost.h" #import "SPUUpdatePermissionRequest.h" #import "SUUpdatePermissionResponse.h" #import "SPUUpdateDriver.h" #import "SUConstants.h" #import "SULog.h" #import "SULog+NSError.h" #import "SUCodeSigningVerifier.h" #import "SUSystemProfiler.h" #import "SPUScheduledUpdateDriver.h" #import "SPUProbingUpdateDriver.h" #import "SPUUserInitiatedUpdateDriver.h" #import "SPUAutomaticUpdateDriver.h" #import "SPUProbeInstallStatus.h" #import "SUAppcastItem.h" #import "SPUInstallationInfo.h" #import "SUErrors.h" #import "SPUXPCServiceInfo.h" #import "SPUUpdaterCycle.h" #import "SPUUpdaterTimer.h" #import "SPUResumableUpdate.h" #import "SUSignatures.h" #import "SPUUserAgent+Private.h" #import "SPUGentleUserDriverReminders.h" #include "AppKitPrevention.h" NSString *const SUUpdaterDidFinishLoadingAppCastNotification = @"SUUpdaterDidFinishLoadingAppCastNotification"; NSString *const SUUpdaterDidFindValidUpdateNotification = @"SUUpdaterDidFindValidUpdateNotification"; NSString *const SUUpdaterDidNotFindUpdateNotification = @"SUUpdaterDidNotFindUpdateNotification"; NSString *const SUUpdaterWillRestartNotification = @"SUUpdaterWillRestartNotificationName"; NSString *const SUUpdaterAppcastItemNotificationKey = @"SUUpdaterAppcastItemNotificationKey"; NSString *const SUUpdaterAppcastNotificationKey = @"SUUpdaterAppCastNotificationKey"; @interface SPUUpdater () // These two properties are needed for KVO @property (nonatomic) BOOL sessionInProgress; @property (nonatomic) BOOL canCheckForUpdates; @end @implementation SPUUpdater { id _userDriver; id _driver; SUHost *_host; SUHost *_mainBundleHost; NSBundle *_applicationBundle; NSBundle *_sparkleBundle; SPUUpdaterSettings *_updaterSettings; SPUUpdaterCycle *_updaterCycle; SPUUpdaterTimer *_updaterTimer; id _resumableUpdate; NSDate *_updateLastCheckedDate; NSURL *_lastCheckedFeedURL; NSSet * _lastAllowedChannels; __weak id _delegate; BOOL _startedUpdater; BOOL _sessionInProgress; BOOL _canCheckForUpdates; BOOL _showingPermissionRequest; BOOL _loggedATSWarning; BOOL _loggedNoSecureKeyWarning; BOOL _loggedUpdateSecurityPolicyWarning; BOOL _updatingMainBundle; } @synthesize userAgentString = _userAgentString; @synthesize httpHeaders = _httpHeaders; @synthesize sessionInProgress = _sessionInProgress; @synthesize canCheckForUpdates = _canCheckForUpdates; - (instancetype)initWithHostBundle:(NSBundle *)hostBundle applicationBundle:(NSBundle *)applicationBundle userDriver:(id )userDriver delegate:(id _Nullable)delegate { self = [super init]; if (self != nil) { // Use explicit class to use the correct bundle even when subclassed _sparkleBundle = [NSBundle bundleForClass:[SPUUpdater class]]; _host = [[SUHost alloc] initWithBundle:hostBundle]; _applicationBundle = applicationBundle; _updaterSettings = [[SPUUpdaterSettings alloc] initWithHostBundle:hostBundle]; _updaterCycle = [[SPUUpdaterCycle alloc] initWithDelegate:self]; _updaterTimer = [[SPUUpdaterTimer alloc] initWithDelegate:self]; _userDriver = userDriver; _delegate = delegate; NSBundle *mainBundle = [NSBundle mainBundle]; _updatingMainBundle = [hostBundle isEqualTo:mainBundle]; // Set up default user agent // Use the main bundle rather than the bundle to update for retrieving user agent information from // We want the user agent to reflect the updater that is doing the updating SUHost *mainBundleHost = [[SUHost alloc] initWithBundle:mainBundle]; _userAgentString = SPUMakeUserAgentWithHost(mainBundleHost, nil); _mainBundleHost = mainBundleHost; } return self; } // To prevent subclasses from doing something bad based on older Sparkle code - (instancetype)initForBundle:(NSBundle *)__unused bundle { NSString *reason = [NSString stringWithFormat:@"-[%@ initForBundle:] is not implemented anymore in Sparkle 2.", NSStringFromClass([self class])]; SULog(SULogLevelError, @"%@", reason); NSException *exception = [NSException exceptionWithName:@"SUIncorrectAPIUsageException" reason:reason userInfo:nil]; @throw exception; return nil; } // To prevent trying to stick an SUUpdater in a nib or initializing it in an incorrect way - (instancetype)init { NSString *reason = [NSString stringWithFormat:@"-[%@ init] is not implemented. If you want to drop an updater into a nib, see SPUStandardUpdaterController.", NSStringFromClass([self class])]; SULog(SULogLevelError, @"%@", reason); NSException *exception = [NSException exceptionWithName:@"SUIncorrectAPIUsageException" reason:reason userInfo:nil]; @throw exception; return nil; } - (BOOL)startUpdater:(NSError * __autoreleasing *)error { if (![NSThread isMainThread]) { if (error != NULL) { *error = [NSError errorWithDomain:SUSparkleErrorDomain code:SUInvalidUpdaterError userInfo:@{ NSLocalizedDescriptionKey: @"-[SPUUpdater startUpdater:] must be called on the main thread]"}]; } return NO; } if (_startedUpdater) { return YES; } if (![self checkIfConfiguredProperlyAndRequireFeedURL:NO validateXPCServices:YES error:error]) { return NO; } if ([_userDriver respondsToSelector:@selector(resetTimeSinceOpportuneUpdateNotice)]) { [(id)_userDriver resetTimeSinceOpportuneUpdateNotice]; } _startedUpdater = YES; [self setCanCheckForUpdates:YES]; [NSNotificationCenter.defaultCenter addObserver:self selector:@selector(updateAutomaticCheckSettingChanged:) name:SUUpdateAutomaticCheckSettingChangedNotification object:nil]; // Start updater on next update cycle so we make sure the application invoking the updater is ready // This also gives the developer a cycle to check for updates before Sparkle's update cycle scheduler kicks in dispatch_async(dispatch_get_main_queue(), ^{ // Check if legacy/deprecated feed URL from user defaults is being used // We perform this check one runloop cycle after starting the updater to give the developer // a chance to call -clearFeedURLFromUserDefaults before this warning can show up if (self->_updatingMainBundle) { NSString *appcastUserDefaultsString = [self->_host objectForUserDefaultsKey:SUFeedURLKey ofClass:NSString.class]; if (appcastUserDefaultsString != nil) { SULog(SULogLevelError, @"Warning: A feed URL was found stored in user defaults for %@. This was likely set using -[SPUUpdater setFeedURL:] which is deprecated. Please migrate away from using this API and call -[SPUUpdater clearFeedURLFromUserDefaults] to remove any stored defaults, otherwise Sparkle may continue to use the feed stored from the defaults. If the feed url was set via a defaults write command for testing purposes, then please ignore this warning.", self->_host.name); } } if (!self->_sessionInProgress) { [self startUpdateCycle]; } }); return YES; } - (BOOL)checkATSIssueForBundle:(NSBundle * _Nullable)bundle getBundleExists:(BOOL *)bundleExists SPU_OBJC_DIRECT { if (bundleExists != NULL) { *bundleExists = (bundle != nil); } if (bundle == nil) { return NO; } return ([bundle objectForInfoDictionaryKey:@"NSAppTransportSecurity"] == nil); } - (BOOL)checkIfConfiguredProperlyAndRequireFeedURL:(BOOL)requireFeedURL validateXPCServices:(BOOL)validateXPCServices error:(NSError * __autoreleasing *)error SPU_OBJC_DIRECT { NSString *hostName = _host.name; if (_sparkleBundle == nil) { if (error != NULL) { *error = [NSError errorWithDomain:SUSparkleErrorDomain code:SUInvalidUpdaterError userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"%@ can't find Sparkle.framework it belongs to in %@.", NSStringFromClass([self class]), hostName] }]; } return NO; } if ([[self hostBundle] bundleIdentifier] == nil) { if (error != NULL) { *error = [NSError errorWithDomain:SUSparkleErrorDomain code:SUInvalidHostBundleIdentifierError userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Sparkle cannot target a bundle that does not have a valid bundle identifier for %@.", hostName] }]; } return NO; } if (!_host.validVersion) { if (error != NULL) { *error = [NSError errorWithDomain:SUSparkleErrorDomain code:SUInvalidHostVersionError userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Sparkle cannot target a bundle that does not have a valid version for %@.", hostName] }]; } return NO; } SUHost *mainBundleHost = _mainBundleHost; if (validateXPCServices) { // Check that all enabled XPC Services are embedded NSArray *xpcServiceIDs = @[@INSTALLER_LAUNCHER_NAME, @DOWNLOADER_NAME, @INSTALLER_CONNECTION_NAME, @INSTALLER_STATUS_NAME]; NSArray *xpcServiceEnabledKeys = @[SUEnableInstallerLauncherServiceKey, SUEnableDownloaderServiceKey, SUEnableInstallerConnectionServiceKey, SUEnableInstallerStatusServiceKey]; NSUInteger xpcServiceCount = xpcServiceIDs.count; for (NSUInteger xpcServiceIndex = 0; xpcServiceIndex < xpcServiceCount; xpcServiceIndex++) { NSString *xpcServiceEnabledKey = xpcServiceEnabledKeys[xpcServiceIndex]; NSString *xpcServiceBundleName = [xpcServiceIDs[xpcServiceIndex] stringByAppendingPathExtension:@"xpc"]; if ([mainBundleHost boolForInfoDictionaryKey:xpcServiceEnabledKey]) { NSURL *xpcServiceBundleURL = [[_sparkleBundle.bundleURL URLByAppendingPathComponent:@"XPCServices"] URLByAppendingPathComponent:xpcServiceBundleName]; if (![xpcServiceBundleURL checkResourceIsReachableAndReturnError:NULL]) { if (error != NULL) { *error = [NSError errorWithDomain:SUSparkleErrorDomain code:SUInvalidUpdaterError userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"XPC Service is enabled (%@) but does not exist: %@", xpcServiceEnabledKey, xpcServiceBundleURL.path] }]; } return NO; } } // Make sure the app isn't bundling XPC Services directly NSURL *mainBundleXPCServiceURL = [[[mainBundleHost.bundle.bundleURL URLByAppendingPathComponent:@"Contents"] URLByAppendingPathComponent:@"XPCServices"] URLByAppendingPathComponent:xpcServiceBundleName]; if ([mainBundleXPCServiceURL checkResourceIsReachableAndReturnError:NULL]) { if (error != NULL) { *error = [NSError errorWithDomain:SUSparkleErrorDomain code:SUInvalidUpdaterError userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"XPC Service (%@) must be in the Sparkle framework, not in the application bundle (%@). Please visit https://sparkle-project.org/documentation/sandboxing/ for up to date Sandboxing instructions.", xpcServiceBundleName, mainBundleXPCServiceURL.path] }]; } return NO; } } } BOOL servingOverHttps = NO; NSError *feedError = nil; NSURL *feedURL = [self retrieveFeedURL:&feedError]; if (feedURL == nil) { if (requireFeedURL) { if (error != NULL) { *error = feedError; } return NO; } } if (feedURL != nil) { _lastCheckedFeedURL = feedURL; _lastAllowedChannels = [self allowedChannels]; servingOverHttps = [[[feedURL scheme] lowercaseString] isEqualToString:@"https"]; if (!servingOverHttps && !_loggedATSWarning) { BOOL foundXPCDownloaderService = NO; NSBundle *downloaderBundle; if ([mainBundleHost boolForInfoDictionaryKey:SUEnableDownloaderServiceKey]) { NSURL *downloaderServiceBundleURL = [[[_sparkleBundle.bundleURL URLByAppendingPathComponent:@"XPCServices"] URLByAppendingPathComponent:@DOWNLOADER_NAME] URLByAppendingPathExtension:@"xpc"]; downloaderBundle = [NSBundle bundleWithURL:downloaderServiceBundleURL]; } else { downloaderBundle = nil; } BOOL foundATSPersistentIssue = [self checkATSIssueForBundle:downloaderBundle getBundleExists:&foundXPCDownloaderService]; BOOL foundATSMainBundleIssue = NO; if (!foundATSPersistentIssue && !foundXPCDownloaderService) { BOOL foundATSIssue = ([mainBundleHost objectForInfoDictionaryKey:@"NSAppTransportSecurity" ofClass:NSDictionary.class] == nil); if (_updatingMainBundle) { // The only way we'll know for sure if there is an issue is if the main bundle is the same as the one we're updating // We don't want to generate false positives.. foundATSMainBundleIssue = foundATSIssue; } } if (foundATSPersistentIssue || foundATSMainBundleIssue) { // Just log a warning. Don't outright fail in case we are wrong (eg: app is linked on an old SDK where ATS doesn't take effect) SULog(SULogLevelDefault, @"The feed URL (%@) may need to change to use HTTPS. If the feed URL is using local networking for testing, this warning may be incorrect and ignored however.\nFor more information: https://sparkle-project.org/documentation/app-transport-security", [feedURL absoluteString]); _loggedATSWarning = YES; } } } SUPublicKeys *publicKeys = _host.publicKeys; BOOL hasAnyPublicKey = publicKeys.hasAnyKeys; #if SPARKLE_BUILD_LEGACY_DSA_SUPPORT if (!hasAnyPublicKey) { // If we failed to retrieve a DSA key but the bundle specifies a path to one, we should consider this a configuration failure NSString *publicDSAKeyFileKey = [_host publicDSAKeyFileKey]; if (publicDSAKeyFileKey != nil) { if (error != NULL) { *error = [NSError errorWithDomain:SUSparkleErrorDomain code:SUNoPublicDSAFoundError userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"The DSA public key '%@' could not be found for %@.", publicDSAKeyFileKey, hostName] }]; } return NO; } } #endif // Don't allow invalid EdDSA public keys if (publicKeys.ed25519PubKeyStatus == SUSigningInputStatusInvalid) { if (error != NULL) { *error = [NSError errorWithDomain:SUSparkleErrorDomain code:SUNoPublicDSAFoundError userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"The EdDSA public key is not valid for %@.", hostName] }]; } return NO; } if (!hasAnyPublicKey) { if ((feedURL != nil && !servingOverHttps) || ![SUCodeSigningVerifier bundleAtURLIsCodeSigned:[[self hostBundle] bundleURL]]) { if (error != NULL) { *error = [NSError errorWithDomain:SUSparkleErrorDomain code:SUNoPublicDSAFoundError userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"For security reasons, updates need to be signed with an EdDSA key for %@. Visit Sparkle's documentation for more information.", hostName] }]; } return NO; } else { BOOL verifyBeforeExtraction = [_host boolForInfoDictionaryKey:SUVerifyUpdateBeforeExtractionKey]; if (verifyBeforeExtraction) { if (error != NULL) { *error = [NSError errorWithDomain:SUSparkleErrorDomain code:SUNoPublicDSAFoundError userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"For security reasons, updates need to be signed with an EdDSA key because %@ is specified for %@. Visit Sparkle's documentation for more information.", SUVerifyUpdateBeforeExtractionKey, hostName] }]; } return NO; } else if (_updatingMainBundle && !_loggedNoSecureKeyWarning) { SULog(SULogLevelError, @"Error: Serving updates without an EdDSA key and only using Apple Code Signing is deprecated and may be unsupported in a future release. Visit Sparkle's documentation for more information: https://sparkle-project.org/documentation/#3-segue-for-security-concerns"); _loggedNoSecureKeyWarning = YES; } } } else if (publicKeys.ed25519PubKey == nil) { // No EdDSA key is available, so app must be using DSA if (_updatingMainBundle) { if (error != NULL) { *error = [NSError errorWithDomain:SUSparkleErrorDomain code:SUNoPublicDSAFoundError userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"For security reasons, updates need to be signed with an EdDSA key for %@. Please migrate to using EdDSA (ed25519). Visit Sparkle's documentation for migration information: https://sparkle-project.org/documentation/#3-segue-for-security-concerns.", hostName] }]; } return NO; } } // This is a policy decision // If developers want greater security by signing appcasts, we can also enforce greater security // in validating updates before extracting them. BOOL requiresSignedFeed = [_host boolForInfoDictionaryKey:SURequireSignedFeedKey]; if (requiresSignedFeed) { BOOL verifyBeforeExtraction = [_host boolForInfoDictionaryKey:SUVerifyUpdateBeforeExtractionKey]; if (!verifyBeforeExtraction) { if (error != NULL) { *error = [NSError errorWithDomain:SUSparkleErrorDomain code:SUInvalidUpdaterError userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"For security reasons, %@ needs to also be enabled if %@ is enabled for %@. Visit Sparkle's documentation for more information: https://sparkle-project.org/documentation/customization/", SUVerifyUpdateBeforeExtractionKey, SURequireSignedFeedKey, hostName] }]; } return NO; } } if (_updatingMainBundle) { if (!_loggedUpdateSecurityPolicyWarning && mainBundleHost.hasUpdateSecurityPolicy) { SULog(SULogLevelDefault, @"Warning: %@ has a custom NSUpdateSecurityPolicy in its Info.plist. This may cause issues when installing updates. Please consider removing this key for your builds using Sparkle if you do not really require a custom update security policy.", hostName); _loggedUpdateSecurityPolicyWarning = YES; } } return YES; } - (NSString *)description { return [NSString stringWithFormat:@"%@ <%@>", [self class], [_host bundlePath]]; } - (void)startUpdateCycle SPU_OBJC_DIRECT { BOOL shouldPrompt = NO; BOOL hasLaunchedBefore = [_host boolForUserDefaultsKey:SUHasLaunchedBeforeKey]; id delegate = _delegate; // If the user has been asked about automatic checks or the developer has overridden the setting, don't bother prompting // When the user answers to the permission prompt, this will be set to either @YES or @NO instead of nil if ([_host boolNumberForKey:SUEnableAutomaticChecksKey] != nil) { shouldPrompt = NO; } // Does the delegate want to take care of the logic for when we should ask permission to update? else if ([delegate respondsToSelector:@selector((updaterShouldPromptForPermissionToCheckForUpdates:))]) { shouldPrompt = [delegate updaterShouldPromptForPermissionToCheckForUpdates:self]; } else { // We wait until the second launch of the updater for this host bundle, unless explicitly overridden via SUPromptUserOnFirstLaunchKey. shouldPrompt = hasLaunchedBefore || [_host boolForInfoDictionaryKey:SUPromptUserOnFirstLaunchKey]; } if (!hasLaunchedBefore) { [_host setBool:YES forUserDefaultsKey:SUHasLaunchedBeforeKey]; } if (shouldPrompt) { NSArray *> *profileInfo = self.systemProfileArray; // Always say we're sending the system profile here so that the delegate displays the parameters it would send. if ([delegate respondsToSelector:@selector((feedParametersForUpdater:sendingSystemProfile:))]) { NSArray *feedParameters = [delegate feedParametersForUpdater:self sendingSystemProfile:YES]; if (feedParameters != nil) { profileInfo = [profileInfo arrayByAddingObjectsFromArray:feedParameters]; } } SPUUpdatePermissionRequest *updatePermissionRequest = [[SPUUpdatePermissionRequest alloc] initWithSystemProfile:profileInfo]; _showingPermissionRequest = YES; [self setSessionInProgress:YES]; BOOL canShowUserDriverInFocusDuringPermissionPrompt = [_userDriver respondsToSelector:@selector(showUpdateInFocus)]; [self setCanCheckForUpdates:canShowUserDriverInFocusDuringPermissionPrompt]; __weak __typeof__(self) weakSelf = self; [_userDriver showUpdatePermissionRequest:updatePermissionRequest reply:^(SUUpdatePermissionResponse *response) { dispatch_async(dispatch_get_main_queue(), ^{ __typeof__(self) strongSelf = weakSelf; if (strongSelf != nil) { [strongSelf setSessionInProgress:NO]; strongSelf->_showingPermissionRequest = NO; [strongSelf updatePermissionRequestFinishedWithResponse:response]; if (!canShowUserDriverInFocusDuringPermissionPrompt) { [strongSelf setCanCheckForUpdates:YES]; } // Schedule checks, but make sure we ignore the delayed call from KVO [strongSelf resetUpdateCycle]; } }); }]; // We start the update checks and register as observer for changes after the prompt finishes } 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 scheduleNextUpdateCheckFiringImmediately:NO usingCurrentDate:YES]; } } - (void)updatePermissionRequestFinishedWithResponse:(SUUpdatePermissionResponse *)response SPU_OBJC_DIRECT { [self setSendsSystemProfile:response.sendSystemProfile]; [self setAutomaticallyChecksForUpdates:response.automaticUpdateChecks]; NSNumber *automaticUpdateDownloading = response.automaticUpdateDownloading; if (automaticUpdateDownloading != nil) { [self setAutomaticallyDownloadsUpdates:automaticUpdateDownloading.boolValue]; } } - (NSDate *)lastUpdateCheckDate { if (![NSThread isMainThread]) { SULog(SULogLevelError, @"Error: -[SPUUpdater lastUpdateCheckDate] must be called on the main thread."); } if (_updateLastCheckedDate == nil) { _updateLastCheckedDate = [_host objectForUserDefaultsKey:SULastCheckTimeKey ofClass:NSDate.class]; } return _updateLastCheckedDate; } - (void)updateLastUpdateCheckDate SPU_OBJC_DIRECT { [self willChangeValueForKey:NSStringFromSelector(@selector((lastUpdateCheckDate)))]; // We use an intermediate property for last update check date due to https://github.com/sparkle-project/Sparkle/pull/1135 _updateLastCheckedDate = [NSDate date]; [_host setObject:_updateLastCheckedDate forUserDefaultsKey:SULastCheckTimeKey]; [self didChangeValueForKey:NSStringFromSelector(@selector((lastUpdateCheckDate)))]; } // Note this method is never called when sessionInProgress is YES - (void)scheduleNextUpdateCheckFiringImmediately:(BOOL)firingImmediately usingCurrentDate:(BOOL)usingCurrentDate SPU_OBJC_DIRECT { [_updaterTimer invalidate]; id delegate = _delegate; if (!firingImmediately && ![self automaticallyChecksForUpdates]) { if ([delegate respondsToSelector:@selector(updaterWillNotScheduleUpdateCheck:)]) { [delegate updaterWillNotScheduleUpdateCheck:self]; } return; } if (firingImmediately) { [self _checkForUpdatesInBackground]; } else { // This may not return the same update check interval as the developer has configured // Notably it may differ when we have an update that has been already downloaded and needs to resume, // as well as if that update is marked critical or not void (^retrieveNextUpdateCheckInterval)(void (^)(NSTimeInterval)) = ^(void (^completionHandler)(NSTimeInterval)) { NSString *hostBundleIdentifier = self->_host.bundle.bundleIdentifier; assert(hostBundleIdentifier != nil); [SPUProbeInstallStatus probeInstallerUpdateItemForHostBundleIdentifier:hostBundleIdentifier completion:^(SPUInstallationInfo * _Nullable installationInfo) { dispatch_async(dispatch_get_main_queue(), ^{ NSTimeInterval regularCheckInterval = [self updateCheckInterval]; NSTimeInterval impatientCheckInterval = [self->_updaterSettings impatientUpdateCheckInterval]; if (installationInfo == nil) { // Proceed as normal if there's no resumable updates completionHandler(regularCheckInterval); } else { if ([installationInfo.appcastItem isCriticalUpdate] || [installationInfo.appcastItem isInformationOnlyUpdate]) { completionHandler(MIN(regularCheckInterval, impatientCheckInterval)); } else { completionHandler(MAX(regularCheckInterval, impatientCheckInterval)); } } }); }]; }; self.canCheckForUpdates = NO; self.sessionInProgress = YES; retrieveNextUpdateCheckInterval(^(NSTimeInterval updateCheckInterval) { [self setCanCheckForUpdates:YES]; [self setSessionInProgress:NO]; // This callback is asynchronous, so the timer may be set. Invalidate to make sure it isn't. [self->_updaterTimer invalidate]; NSTimeInterval intervalSinceCheck; if (usingCurrentDate) { // How long has it been since last we checked for an update? NSDate *lastCheckDate = [self lastUpdateCheckDate]; if (!lastCheckDate) { lastCheckDate = [NSDate distantPast]; } intervalSinceCheck = [[NSDate date] timeIntervalSinceDate:lastCheckDate]; if (intervalSinceCheck < 0) { // Last update check date is in the future and bogus, so reset it to current date [self updateLastUpdateCheckDate]; intervalSinceCheck = 0; } } else { intervalSinceCheck = 0; } NSTimeInterval minimumUpdateCheckInterval = self->_updaterSettings.minimumUpdateCheckInterval; // Now we want to figure out how long until we check again. if (updateCheckInterval < minimumUpdateCheckInterval) updateCheckInterval = minimumUpdateCheckInterval; if (intervalSinceCheck < updateCheckInterval) { NSTimeInterval delayUntilCheck = (updateCheckInterval - intervalSinceCheck); // It hasn't been long enough. if ([delegate respondsToSelector:@selector(updater:willScheduleUpdateCheckAfterDelay:)]) { [delegate updater:self willScheduleUpdateCheckAfterDelay:delayUntilCheck]; } if ([self->_userDriver respondsToSelector:@selector(logGentleScheduledUpdateReminderWarningIfNeeded)]) { [(id)self->_userDriver logGentleScheduledUpdateReminderWarningIfNeeded]; } uint64_t leewayUpdateCheckInterval = self->_updaterSettings.leewayUpdateCheckInterval; [self->_updaterTimer startAndFireAfterDelay:delayUntilCheck leewayUpdateCheckInterval:leewayUpdateCheckInterval]; } else { // We're overdue! Run one now. [self _checkForUpdatesInBackground]; } }); } } - (void)updaterTimerDidFire { // User can perform a checkForUpdates check around the same time the timer is ready to fire if (!_sessionInProgress) { [self _checkForUpdatesInBackground]; } } - (void)_checkForUpdatesInBackground SPU_OBJC_DIRECT { [self setSessionInProgress:YES]; [self setCanCheckForUpdates:NO]; // We don't want the probe check to act on the driver if the updater is going near death __weak __typeof__(self) weakSelf = self; NSString *hostBundleIdentifier = _host.bundle.bundleIdentifier; assert(hostBundleIdentifier != nil); [SPUProbeInstallStatus probeInstallerInProgressForHostBundleIdentifier:hostBundleIdentifier completion:^(BOOL installerIsRunning) { dispatch_async(dispatch_get_main_queue(), ^{ __typeof__(self) strongSelf = weakSelf; if (strongSelf == nil) { return; } id delegate = strongSelf->_delegate; id updateDriver; if (!installerIsRunning && [strongSelf automaticallyDownloadsUpdates] && strongSelf->_resumableUpdate == nil) { updateDriver = [[SPUAutomaticUpdateDriver alloc] initWithHost:strongSelf->_host applicationBundle:strongSelf->_applicationBundle updater:strongSelf userDriver:strongSelf->_userDriver updaterDelegate:delegate]; } else { updateDriver = [[SPUScheduledUpdateDriver alloc] initWithHost:strongSelf->_host applicationBundle:strongSelf->_applicationBundle updater:strongSelf userDriver:strongSelf->_userDriver updaterDelegate:delegate]; } [strongSelf checkForUpdatesWithDriver:updateDriver updateCheck:SPUUpdateCheckUpdatesInBackground installerInProgress:installerIsRunning]; }); }]; } // This is the developer-facing checkForUpdatesInBackground // Sparkle internally uses _checkForUpdatesInBackground - (void)checkForUpdatesInBackground { if (![NSThread isMainThread]) { SULog(SULogLevelError, @"Error: -[SPUUpdater checkForUpdatesInBackground] can only be called on the main thread"); // Try to be nice and dispatch on main thread anyway dispatch_async(dispatch_get_main_queue(), ^{ [self checkForUpdatesInBackground]; }); return; } if (!_startedUpdater) { SULog(SULogLevelError, @"Error: checkForUpdatesInBackground - updater hasn't been started yet. Please call -startUpdater: first"); return; } if (_sessionInProgress) { SULog(SULogLevelError, @"Error: -checkForUpdatesInBackground called but .sessionInProgress == YES"); return; } if (_updatingMainBundle) { // Check if Sparkle is configured to ask the user's permission to enable automatic update checks NSNumber *automaticChecksInInfoPlist = [_host boolNumberForInfoDictionaryKey:SUEnableAutomaticChecksKey]; if (automaticChecksInInfoPlist == nil) { // Check if automatic update checking is disabled or if the user hasn't given permission for Sparkle to check BOOL automaticChecksInDefaults = [self automaticallyChecksForUpdates]; if (!automaticChecksInDefaults) { SULog(SULogLevelError, @"Error: Calling -[SPUUpdater checkForUpdatesInBackground] for your own bundle when Sparkle is set to ask the user permission to check for updates in the background automatically and when automaticallyChecksForUpdates is NO leads to incorrect behavior. Recommendation: remove call to checkForUpdatesInBackground"); } } } [self _checkForUpdatesInBackground]; } - (void)checkForUpdates { if (![NSThread isMainThread]) { SULog(SULogLevelError, @"Error: -[SPUUpdater checkForUpdates] can only be called on the main thread"); // Try to be nice and dispatch on main thread anyway dispatch_async(dispatch_get_main_queue(), ^{ [self checkForUpdates]; }); return; } if (_showingPermissionRequest || _driver.showingUpdate) { if ([_userDriver respondsToSelector:@selector(showUpdateInFocus)]) { [_userDriver showUpdateInFocus]; } else { NSString *noticeType = _showingPermissionRequest ? @"permission request" : @"update"; SULog(SULogLevelError, @"Error: checkForUpdates called but %@ is being shown and %@ does not implement -[SPUUserDriver showUpdateInFocus]", noticeType, _userDriver); } return; } if (!_startedUpdater) { SULog(SULogLevelError, @"Error: checkForUpdates - updater hasn't been started yet. Please call -startUpdater: first"); return; } if (_sessionInProgress) { SULog(SULogLevelError, @"Error: -checkForUpdates called but .sessionInProgress == YES"); return; } if (_driver != nil) { return; } [self setSessionInProgress:YES]; [self setCanCheckForUpdates:NO]; id theUpdateDriver = [[SPUUserInitiatedUpdateDriver alloc] initWithHost:_host applicationBundle:_applicationBundle updater:self userDriver:_userDriver updaterDelegate:_delegate]; NSString *bundleIdentifier = _host.bundle.bundleIdentifier; assert(bundleIdentifier != nil); __weak __typeof__(self) weakSelf = self; [SPUProbeInstallStatus probeInstallerInProgressForHostBundleIdentifier:bundleIdentifier completion:^(BOOL installerInProgress) { dispatch_async(dispatch_get_main_queue(), ^{ __typeof__(self) strongSelf = weakSelf; if (strongSelf != nil) { [strongSelf checkForUpdatesWithDriver:theUpdateDriver updateCheck:SPUUpdateCheckUpdates installerInProgress:installerInProgress]; } }); }]; } - (void)checkForUpdateInformation { if (![NSThread isMainThread]) { SULog(SULogLevelError, @"Error: -[SPUUpdater checkForUpdateInformation] can only be called on the main thread"); // Try to be nice and dispatch on main thread anyway dispatch_async(dispatch_get_main_queue(), ^{ [self checkForUpdateInformation]; }); return; } __weak __typeof__(self) weakSelf = self; if (!_startedUpdater) { SULog(SULogLevelError, @"Error: checkForUpdateInformation - updater hasn't been started yet. Please call -startUpdater: first"); return; } if (_sessionInProgress) { SULog(SULogLevelError, @"Error: -checkForUpdateInformation called but .sessionInProgress == YES"); return; } [self setSessionInProgress:YES]; [self setCanCheckForUpdates:NO]; NSString *bundleIdentifier = _host.bundle.bundleIdentifier; assert(bundleIdentifier != nil); [SPUProbeInstallStatus probeInstallerInProgressForHostBundleIdentifier:bundleIdentifier completion:^(BOOL installerInProgress) { dispatch_async(dispatch_get_main_queue(), ^{ __typeof__(self) strongSelf = weakSelf; if (strongSelf != nil) { [strongSelf checkForUpdatesWithDriver:[[SPUProbingUpdateDriver alloc] initWithHost:strongSelf->_host updater:strongSelf updaterDelegate:strongSelf->_delegate] updateCheck:SPUUpdateCheckUpdateInformation installerInProgress:installerInProgress]; } }); }]; } - (void)checkForUpdatesWithDriver:(id )d updateCheck:(SPUUpdateCheck)updateCheck installerInProgress:(BOOL)installerInProgress SPU_OBJC_DIRECT { if (_driver != nil) { SULog(SULogLevelError, @"Error: checkForUpdatesWithDriver:updateCheck:installerInProgress: called when _driver != nil"); return; } [_updaterTimer invalidate]; [self updateLastUpdateCheckDate]; _driver = d; assert(_driver != nil); void (^notifyDelegateOfDriverCompletion)(NSError * _Nullable, BOOL) = ^(NSError * _Nullable error, BOOL shouldShowUpdateImmediately) { id delegate = self->_delegate; if (error != nil) { if (error.code != SUNoUpdateError && error.code != SUInstallationCanceledError && error.code != SUInstallationAuthorizeLaterError) { // Let's not bother logging this. SULogError(error); } // Notify host app that update driver has aborted if a non-recoverable error occurs if (error.code != SUInstallationAuthorizeLaterError && [delegate respondsToSelector:@selector((updater:didAbortWithError:))]) { [delegate updater:self didAbortWithError:(NSError * _Nonnull)error]; } } // Notify host app that update driver has finished // As long as we're not going to immediately kick off a new check if (!shouldShowUpdateImmediately && [delegate respondsToSelector:@selector((updater:didFinishUpdateCycleForUpdateCheck:error:))]) { [delegate updater:self didFinishUpdateCycleForUpdateCheck:updateCheck error:error]; } }; void (^abortUpdateDriver)(NSError * _Nullable , BOOL) = ^(NSError * _Nullable abortError, BOOL shouldScheduleNextUpdateCheck) { __weak __typeof__(self) weakSelf = self; [self->_driver setCompletionHandler:^(BOOL __unused shouldShowUpdateImmediately, id _Nullable __unused resumableUpdate, NSError * _Nullable error) { __typeof__(self) strongSelf = weakSelf; if (strongSelf != nil) { strongSelf->_driver = nil; [strongSelf updateLastUpdateCheckDate]; strongSelf.sessionInProgress = NO; strongSelf.canCheckForUpdates = YES; notifyDelegateOfDriverCompletion(error, NO); // Ensure the delegate doesn't start a new session when being notified of the previous one ending if (!strongSelf->_sessionInProgress) { if (shouldScheduleNextUpdateCheck) { [strongSelf scheduleNextUpdateCheckFiringImmediately:NO usingCurrentDate:NO]; } else { SULog(SULogLevelDefault, @"Disabling scheduled updates.."); } } } }]; [self->_driver abortUpdateWithError:abortError]; }; // Check if the delegate wants to defer checking for updates id delegate = _delegate; NSError *mayCheckForUpdatesError = nil; if ( ([delegate respondsToSelector:@selector(updater:mayPerformUpdateCheck:error:)] && ![delegate updater:self mayPerformUpdateCheck:updateCheck error:&mayCheckForUpdatesError]) || #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdeprecated-declarations" ([delegate respondsToSelector:@selector((updaterMayCheckForUpdates:))] && ![delegate updaterMayCheckForUpdates:self])) #pragma clang diagnostic pop { abortUpdateDriver(mayCheckForUpdatesError, YES); return; } // Because an application can change the configuration (eg: the feed url) at any point, we should always check if it's valid // We will not schedule a next update check if the bundle is misconfigured NSError *configurationError = nil; if (![self checkIfConfiguredProperlyAndRequireFeedURL:YES validateXPCServices:NO error:&configurationError]) { SULog(SULogLevelError, @"Sparkle configuration error (%ld): %@", (long)configurationError.code, configurationError.localizedDescription); abortUpdateDriver(configurationError, NO); return; } // Run our update driver and schedule next update check on its completion __weak __typeof__(self) weakSelf = self; [_driver setCompletionHandler:^(BOOL shouldShowUpdateImmediately, id _Nullable resumableUpdate, NSError * _Nullable error) { __typeof__(self) strongSelf = weakSelf; if (strongSelf != nil) { strongSelf->_resumableUpdate = resumableUpdate; strongSelf->_driver = nil; [strongSelf updateLastUpdateCheckDate]; [strongSelf setSessionInProgress:NO]; [strongSelf setCanCheckForUpdates:YES]; if (!strongSelf->_updatingMainBundle && error == nil && !shouldShowUpdateImmediately && resumableUpdate == nil) { // If we're not updating the main bundle, a potentially new installed bundle may have different info [NSNotificationCenter.defaultCenter postNotificationName:SUUpdateSettingsNeedsSynchronizationNotification object:nil userInfo:@{SUUpdateBundlePathUserInfoKey: strongSelf->_host.bundlePath}]; } notifyDelegateOfDriverCompletion(error, shouldShowUpdateImmediately); // Ensure the delegate doesn't start a new session when being notified of the previous one ending if (!strongSelf->_sessionInProgress) { [strongSelf scheduleNextUpdateCheckFiringImmediately:shouldShowUpdateImmediately usingCurrentDate:NO]; } } }]; if ([_userDriver respondsToSelector:@selector(showUpdateInFocus)]) { [_driver setUpdateShownHandler:^{ weakSelf.canCheckForUpdates = YES; }]; } [_driver setUpdateWillInstallHandler:^{ [weakSelf updateLastUpdateCheckDate]; }]; if (installerInProgress) { // Resume an update that has already begun installing in the background [_driver resumeInstallingUpdate]; } else if (_resumableUpdate != nil) { // Resume an update or info that has already been downloaded [_driver resumeUpdate:(id _Nonnull)_resumableUpdate]; } else { // Check that the parameterized feed URL is valid NSURL *theFeedURL = [self parameterizedFeedURL]; if (theFeedURL == nil) { // I think this is really unlikely to occur but better be safe // We will not schedule a next update check if the feed URL cannot be formed SULog(SULogLevelError, @"Error: failed to retrieve feed URL for bundle"); abortUpdateDriver([NSError errorWithDomain:SUSparkleErrorDomain code:SUInvalidFeedURLError userInfo:@{ NSLocalizedDescriptionKey: @"Sparkle cannot form a valid feed URL." }], NO); } else { // Check for new updates [_driver checkForUpdatesAtAppcastURL:theFeedURL withUserAgent:_userAgentString httpHeaders:_httpHeaders]; } } } - (void)cancelNextUpdateCycle { [_updaterCycle cancelNextUpdateCycle]; } - (NSSet *)allowedChannels SPU_OBJC_DIRECT { id delegate = _delegate; if ([delegate respondsToSelector:@selector(allowedChannelsForUpdater:)]) { return [delegate allowedChannelsForUpdater:self]; } else { return [NSSet set]; } } - (void)resetUpdateCycle { if (![NSThread isMainThread]) { SULog(SULogLevelError, @"Error: -[SPUUpdater resetUpdateCycle] must be called on the main thread."); // Try to be nice and dispatch on main thread anyway dispatch_async(dispatch_get_main_queue(), ^{ [self resetUpdateCycle]; }); return; } if (!_startedUpdater) { SULog(SULogLevelError, @"Error: resetUpdateCycle - updater hasn't been started yet. Please call -startUpdater: first"); return; // not even ready yet } // Note this resets the opportune time when user grants Sparkle permission to check for updates // and when the user changes preferences on a Sparkle setting such as automatically checking for updates if ([_userDriver respondsToSelector:@selector(resetTimeSinceOpportuneUpdateNotice)]) { [(id)_userDriver resetTimeSinceOpportuneUpdateNotice]; } if (!_sessionInProgress) { [self cancelNextUpdateCycle]; // If the non-parameterized feed URL or allowed set of channels have been changed by the user since the last update check, // we can fire an update check in the background immediately BOOL fireUpdateCheckImmediately = NO; NSURL *lastCheckedFeedURL = _lastCheckedFeedURL; if (lastCheckedFeedURL != nil) { NSURL *currentFeedURL = [self retrieveFeedURL:NULL]; if (currentFeedURL != nil) { if (![currentFeedURL isEqual:lastCheckedFeedURL]) { fireUpdateCheckImmediately = YES; } else if (_lastAllowedChannels != nil) { NSSet *currentAllowedChannels = [self allowedChannels]; if (currentAllowedChannels != nil && ![currentAllowedChannels isEqualToSet:_lastAllowedChannels]) { fireUpdateCheckImmediately = YES; } } } } [self scheduleNextUpdateCheckFiringImmediately:fireUpdateCheckImmediately usingCurrentDate:YES]; } } - (void)resetUpdateCycleAfterShortDelay { if (![NSThread isMainThread]) { SULog(SULogLevelError, @"Error: -[SPUUpdater resetUpdateCycleAfterShortDelay] must be called on the main thread."); // Try to be nice and dispatch on main thread anyway dispatch_async(dispatch_get_main_queue(), ^{ [self resetUpdateCycleAfterShortDelay]; }); return; } [self cancelNextUpdateCycle]; [_updaterCycle resetUpdateCycleAfterDelay]; } - (void)setAutomaticallyChecksForUpdates:(BOOL)automaticallyCheckForUpdates { if (![NSThread isMainThread]) { SULog(SULogLevelError, @"Error: -[SPUUpdater setAutomaticallyChecksForUpdates:] must be called on the main thread."); } _updaterSettings.automaticallyChecksForUpdates = automaticallyCheckForUpdates; } - (BOOL)automaticallyChecksForUpdates { if (![NSThread isMainThread]) { SULog(SULogLevelError, @"Error: -[SPUUpdater automaticallyChecksForUpdates] must be called on the main thread."); } return [_updaterSettings automaticallyChecksForUpdates]; } - (void)updateAutomaticCheckSettingChanged:(NSNotification *)notification { NSString *bundlePath = notification.userInfo[SUUpdateBundlePathUserInfoKey]; if (![bundlePath isEqualToString:_host.bundlePath]) { return; } if (_startedUpdater && !_sessionInProgress) { // Provide a small delay in case multiple preferences are being updated simultaneously. [self resetUpdateCycleAfterShortDelay]; } } + (NSSet *)keyPathsForValuesAffectingAutomaticallyChecksForUpdates { return [NSSet setWithObject:@"updaterSettings.automaticallyChecksForUpdates"]; } + (BOOL)automaticallyNotifiesObserversOfAutomaticallyChecksForUpdates { return NO; } - (void)setAutomaticallyDownloadsUpdates:(BOOL)automaticallyUpdates { if (![NSThread isMainThread]) { SULog(SULogLevelError, @"Error: -[SPUUpdater setAutomaticallyDownloadsUpdates:] must be called on the main thread."); } _updaterSettings.automaticallyDownloadsUpdates = automaticallyUpdates; } - (BOOL)automaticallyDownloadsUpdates { if (![NSThread isMainThread]) { SULog(SULogLevelError, @"Error: -[SPUUpdater automaticallyDownloadsUpdates] must be called on the main thread."); } return [_updaterSettings automaticallyDownloadsUpdates]; } + (NSSet *)keyPathsForValuesAffectingAutomaticallyDownloadsUpdates { return [NSSet setWithObject:@"updaterSettings.automaticallyDownloadsUpdates"]; } + (BOOL)automaticallyNotifiesObserversOfAutomaticallyDownloadsUpdates { return NO; } - (BOOL)allowsAutomaticUpdates { if (![NSThread isMainThread]) { SULog(SULogLevelError, @"Error: -[SPUUpdater allowsAutomaticUpdates] must be called on the main thread."); } return [_updaterSettings allowsAutomaticUpdates]; } + (NSSet *)keyPathsForValuesAffectingAllowsAutomaticUpdates { return [NSSet setWithObject:@"updaterSettings.allowsAutomaticUpdates"]; } + (BOOL)automaticallyNotifiesObserversOfAllowsAutomaticUpdates { return NO; } - (void)setFeedURL:(NSURL * _Nullable)feedURL { if (![NSThread isMainThread]) { SULog(SULogLevelError, @"Error: -[SPUUpdater setFeedURL:] must be called on the main thread. The call from a background thread was ignored."); return; } // When feedURL is nil, -absoluteString will return nil and will remove the user default key [_host setObject:[feedURL absoluteString] forUserDefaultsKey:SUFeedURLKey]; } - (nullable NSURL *)clearFeedURLFromUserDefaults { if (![NSThread isMainThread]) { // We will allow continuing even if not called on main thread SULog(SULogLevelError, @"Error: -[SPUUpdater clearFeedURLFromUserDefaults] must be called on the main thread."); } NSString *appcastString = [_host objectForUserDefaultsKey:SUFeedURLKey ofClass:NSString.class]; [_host setObject:nil forUserDefaultsKey:SUFeedURLKey]; if (appcastString == nil) { return nil; } // -retrieveFeedURL: strips the feed URL so we should do the same NSString *castUrlStr = [self _stripFeedString:appcastString hostName:@"" error:NULL]; if (castUrlStr == nil) { return nil; } return [NSURL URLWithString:castUrlStr]; } - (NSString * _Nullable)_stripFeedString:(NSString *)appcastString hostName:(NSString *)hostName error:(NSError * __autoreleasing *)error SPU_OBJC_DIRECT { NSCharacterSet *quoteSet = [NSCharacterSet characterSetWithCharactersInString:@"\"\'"]; // Some feed publishers add quotes; strip 'em. NSString *castUrlStr = [appcastString stringByTrimmingCharactersInSet:quoteSet]; if (castUrlStr == nil || [castUrlStr length] == 0) { if (error != NULL) { *error = [NSError errorWithDomain:SUSparkleErrorDomain code:SUInvalidFeedURLError userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Appcast feed (%@) after trimming it of quotes is empty for %@!", appcastString, hostName] }]; } return nil; } return castUrlStr; } - (NSURL * _Nullable)retrieveFeedURL:(NSError * __autoreleasing *)error SPU_OBJC_DIRECT { NSString *hostName = _host.name; if (![NSThread isMainThread]) { SULog(SULogLevelError, @"Error: -[SPUUpdater retrieveFeedURL:error:] must be called on the main thread."); if (error != NULL) { *error = [NSError errorWithDomain:SUSparkleErrorDomain code:SUIncorrectAPIUsageError userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"SUUpdater -retrieveFeedURL:error: must be called on the main thread for %@", hostName]}]; } return nil; } // Delegate gets first priority for determining the feed URL NSString *appcastString = [_host objectForKey:SUFeedURLKey ofClass:NSString.class]; id delegate = _delegate; if ([delegate respondsToSelector:@selector((feedURLStringForUpdater:))]) { NSString *delegateAppcastString = [delegate feedURLStringForUpdater:self]; if (delegateAppcastString != nil) { appcastString = delegateAppcastString; } } // A value in the user defaults overrides one in the Info.plist // (as this used to be used for setting alternative feed URLs but is now deprecated) if (appcastString == nil) { appcastString = [_host objectForKey:SUFeedURLKey ofClass:NSString.class]; } if (appcastString == nil) { // Can't find an appcast string! if (error != NULL) { *error = [NSError errorWithDomain:SUSparkleErrorDomain code:SUInvalidFeedURLError userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"You must specify the URL of the appcast as the %@ key in either the Info.plist, or with -[SPUUpdaterDelegate feedURLStringForUpdater:] for %@!", SUFeedURLKey, hostName] }]; } return nil; } NSString *castUrlStr = [self _stripFeedString:appcastString hostName:hostName error:error]; if (castUrlStr == nil) { return nil; } NSURL *feedURL = [NSURL URLWithString:castUrlStr]; if (feedURL == nil) { if (error != NULL) { *error = [NSError errorWithDomain:SUSparkleErrorDomain code:SUInvalidFeedURLError userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Appcast feed (%@) after converting it to a URL is invalid for %@!", appcastString, hostName] }]; } return nil; } return feedURL; } // A client may call this method but do not invoke this method ourselves because it's unsafe - (NSURL * _Nullable)feedURL { NSError *feedError = nil; NSURL *feedURL = [self retrieveFeedURL:&feedError]; if (feedURL == nil) { SULog(SULogLevelError, @"Feed Error (%ld): %@", feedError.code, feedError.localizedDescription); return nil; } return feedURL; } - (void)setSendsSystemProfile:(BOOL)sendsSystemProfile { if (![NSThread isMainThread]) { SULog(SULogLevelError, @"Error: -[SPUUpdater setSendsSystemProfile:] must be called on the main thread."); } _updaterSettings.sendsSystemProfile = sendsSystemProfile; } - (BOOL)sendsSystemProfile { if (![NSThread isMainThread]) { SULog(SULogLevelError, @"Error: -[SPUUpdater sendsSystemProfile] must be called on the main thread."); } return [_updaterSettings sendsSystemProfile]; } + (NSSet *)keyPathsForValuesAffectingSendsSystemProfile { return [NSSet setWithObject:@"updaterSettings.sendsSystemProfile"]; } + (BOOL)automaticallyNotifiesObserversOfSendsSystemProfile { return NO; } static NSString *escapeURLComponent(NSString *str) { NSString *escapedString = [str stringByAddingPercentEncodingWithAllowedCharacters:NSCharacterSet.URLQueryAllowedCharacterSet]; return [[[escapedString stringByReplacingOccurrencesOfString:@"=" withString:@"%3d"] stringByReplacingOccurrencesOfString:@"&" withString:@"%26"] stringByReplacingOccurrencesOfString:@"+" withString:@"%2b"]; } // Precondition: The feed URL should be valid - (NSURL * _Nullable)parameterizedFeedURL SPU_OBJC_DIRECT { NSURL *baseFeedURL = [self retrieveFeedURL:NULL]; if (baseFeedURL == nil) { SULog(SULogLevelError, @"Unexpected error: base feed URL is invalid during -parameterizedFeedURL"); return nil; } // Determine all the parameters we're attaching to the base feed URL. BOOL sendingSystemProfile = [self sendsSystemProfile]; // Let's only send the system profiling information once per week at most, so we normalize daily-checkers vs. biweekly-checkers and the such. if (sendingSystemProfile) { NSDate *lastSubmitDate = [_host objectForUserDefaultsKey:SULastProfileSubmitDateKey ofClass:NSDate.class]; if (!lastSubmitDate) { lastSubmitDate = [NSDate distantPast]; } const NSTimeInterval oneWeek = 60 * 60 * 24 * 7; NSTimeInterval timeSinceLastSubmission = [lastSubmitDate timeIntervalSinceNow] * -1; if (timeSinceLastSubmission < oneWeek) { sendingSystemProfile = NO; } } id delegate = _delegate; NSArray *> *parameters = @[]; if ([delegate respondsToSelector:@selector((feedParametersForUpdater:sendingSystemProfile:))]) { NSArray *feedParameters = [delegate feedParametersForUpdater:self sendingSystemProfile:sendingSystemProfile]; if (feedParameters != nil) { parameters = [parameters arrayByAddingObjectsFromArray:feedParameters]; } } if (sendingSystemProfile) { parameters = [parameters arrayByAddingObjectsFromArray:[self systemProfileArray]]; [_host setObject:[NSDate date] forUserDefaultsKey:SULastProfileSubmitDateKey]; } if ([parameters count] == 0) { return baseFeedURL; } // Build up the parameterized URL. NSMutableArray *parameterStrings = [NSMutableArray array]; for (NSDictionary *currentProfileInfo in parameters) { [parameterStrings addObject:[NSString stringWithFormat:@"%@=%@", escapeURLComponent([currentProfileInfo objectForKey:@"key"]), escapeURLComponent([currentProfileInfo objectForKey:@"value"])]]; } NSString *separatorCharacter = @"?"; if ([baseFeedURL query]) { separatorCharacter = @"&"; // In case the URL is already http://foo.org/baz.xml?bat=4 } NSString *appcastStringWithProfile = [NSString stringWithFormat:@"%@%@%@", [baseFeedURL absoluteString], separatorCharacter, [parameterStrings componentsJoinedByString:@"&"]]; // Clean it up so it's a valid URL NSURL *parameterizedFeedURL = [NSURL URLWithString:appcastStringWithProfile]; if (parameterizedFeedURL == nil) { SULog(SULogLevelError, @"Unexpected error: parameterized feed URL formed from %@ is invalid", appcastStringWithProfile); } return parameterizedFeedURL; } - (NSArray *> *)systemProfileArray { id delegate = _delegate; NSArray *systemProfile = [SUSystemProfiler systemProfileArrayForHost:_host]; if ([delegate respondsToSelector:@selector(allowedSystemProfileKeysForUpdater:)]) { NSArray * allowedKeys = [delegate allowedSystemProfileKeysForUpdater:self]; if (allowedKeys != nil) { NSMutableArray *filteredProfile = [NSMutableArray array]; for (NSDictionary *profileElement in systemProfile) { NSString *key = [profileElement objectForKey:@"key"]; if (key && [allowedKeys containsObject:key]) { [filteredProfile addObject:profileElement]; } } systemProfile = [filteredProfile copy]; } } return systemProfile; } - (void)setUpdateCheckInterval:(NSTimeInterval)updateCheckInterval { if (![NSThread isMainThread]) { SULog(SULogLevelError, @"Error: -[SPUUpdater setUpdateCheckInterval:] must be called on the main thread."); } _updaterSettings.updateCheckInterval = updateCheckInterval; } - (NSTimeInterval)updateCheckInterval { if (![NSThread isMainThread]) { SULog(SULogLevelError, @"Error: -[SPUUpdater updateCheckInterval] must be called on the main thread."); } return [_updaterSettings updateCheckInterval]; } + (NSSet *)keyPathsForValuesAffectingUpdateCheckInterval { return [NSSet setWithObject:@"updaterSettings.updateCheckInterval"]; } + (BOOL)automaticallyNotifiesObserversOfUpdateCheckInterval { return NO; } - (void)dealloc { if (_startedUpdater) { [NSNotificationCenter.defaultCenter removeObserver:self name:SUUpdateAutomaticCheckSettingChangedNotification object:nil]; } // Stop checking for updates [self cancelNextUpdateCycle]; [_updaterTimer invalidate]; // Abort any on-going updates // A driver could be retained by another object (eg: a timer), // so not aborting could mean it stays alive longer than we'd want [_driver abortUpdate]; _driver = nil; } - (NSBundle *)hostBundle { return _host.bundle; } @end ================================================ FILE: Sparkle/SPUUpdaterCycle.h ================================================ // // SPUUpdaterCycle.h // Sparkle // // Created by Mayur Pawashe on 6/11/16. // Copyright © 2016 Sparkle Project. All rights reserved. // #import NS_ASSUME_NONNULL_BEGIN @protocol SPUUpdaterCycleDelegate - (void)resetUpdateCycle; @end // This notifies the updater for (re-)starting and canceling update cycles // This class is used so that an updater instance isn't kept alive by a pending update cycle SPU_OBJC_DIRECT_MEMBERS @interface SPUUpdaterCycle : NSObject // This delegate is weakly referenced - (instancetype)initWithDelegate:(id)delegate; - (void)resetUpdateCycleAfterDelay; - (void)cancelNextUpdateCycle; @end NS_ASSUME_NONNULL_END ================================================ FILE: Sparkle/SPUUpdaterCycle.m ================================================ // // SPUUpdaterCycle.m // Sparkle // // Created by Mayur Pawashe on 6/11/16. // Copyright © 2016 Sparkle Project. All rights reserved. // #import "SPUUpdaterCycle.h" #include "AppKitPrevention.h" @implementation SPUUpdaterCycle { __weak id _delegate; } - (instancetype)initWithDelegate:(id)delegate { self = [super init]; if (self != nil) { _delegate = delegate; } return self; } - (void)resetUpdateCycle { [_delegate resetUpdateCycle]; } - (void)resetUpdateCycleAfterDelay { [self performSelector:@selector(resetUpdateCycle) withObject:nil afterDelay:1]; } - (void)cancelNextUpdateCycle { [[self class] cancelPreviousPerformRequestsWithTarget:self selector:@selector(resetUpdateCycle) object:nil]; } @end ================================================ FILE: Sparkle/SPUUpdaterDelegate.h ================================================ // // SPUUpdaterDelegate.h // Sparkle // // Created by Mayur Pawashe on 8/12/16. // Copyright © 2016 Sparkle Project. All rights reserved. // #import #if defined(BUILDING_SPARKLE_SOURCES_EXTERNALLY) // Ignore incorrect warning #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wquoted-include-in-framework-header" #import "SUExport.h" #import "SPUUpdateCheck.h" #import "SPUUserUpdateState.h" #pragma clang diagnostic pop #else #import #import #import #endif @protocol SUVersionComparison; @class SPUUpdater, SUAppcast, SUAppcastItem, SPUUserUpdateState; NS_ASSUME_NONNULL_BEGIN // ----------------------------------------------------------------------------- // SUUpdater Notifications for events that might be interesting to more than just the delegate // The updater will be the notification object // ----------------------------------------------------------------------------- SU_EXPORT extern NSString *const SUUpdaterDidFinishLoadingAppCastNotification; SU_EXPORT extern NSString *const SUUpdaterDidFindValidUpdateNotification; SU_EXPORT extern NSString *const SUUpdaterDidNotFindUpdateNotification; SU_EXPORT extern NSString *const SUUpdaterWillRestartNotification; #define SUUpdaterWillRelaunchApplicationNotification SUUpdaterWillRestartNotification; #define SUUpdaterWillInstallUpdateNotification SUUpdaterWillRestartNotification; // Key for the SUAppcastItem object in the SUUpdaterDidFindValidUpdateNotification userInfo SU_EXPORT extern NSString *const SUUpdaterAppcastItemNotificationKey; // Key for the SUAppcast object in the SUUpdaterDidFinishLoadingAppCastNotification userInfo SU_EXPORT extern NSString *const SUUpdaterAppcastNotificationKey; // ----------------------------------------------------------------------------- // System Profile Keys // ----------------------------------------------------------------------------- SU_EXPORT extern NSString *const SUSystemProfilerApplicationNameKey; SU_EXPORT extern NSString *const SUSystemProfilerApplicationVersionKey; SU_EXPORT extern NSString *const SUSystemProfilerCPU64bitKey; SU_EXPORT extern NSString *const SUSystemProfilerCPUCountKey; SU_EXPORT extern NSString *const SUSystemProfilerCPUFrequencyKey; SU_EXPORT extern NSString *const SUSystemProfilerCPUTypeKey; SU_EXPORT extern NSString *const SUSystemProfilerCPUSubtypeKey; SU_EXPORT extern NSString *const SUSystemProfilerHardwareModelKey; SU_EXPORT extern NSString *const SUSystemProfilerMemoryKey; SU_EXPORT extern NSString *const SUSystemProfilerOperatingSystemVersionKey; SU_EXPORT extern NSString *const SUSystemProfilerPreferredLanguageKey; // ----------------------------------------------------------------------------- // SPUUpdater Delegate: // ----------------------------------------------------------------------------- /** Provides delegation methods to control the behavior of an `SPUUpdater` object. */ NS_SWIFT_UI_ACTOR @protocol SPUUpdaterDelegate @optional /** Returns whether to allow Sparkle to check for updates. For example, this may be used to prevent Sparkle from interrupting a setup assistant. Alternatively, you may want to consider starting the updater after eg: the setup assistant finishes. Note in Swift, this method returns Void and is marked with the throws keyword. If this method doesn't throw an error, the updater may perform an update check. Otherwise if an error is thrown (we recommend using an NSError), then the updater may not perform an update check. @param updater The updater instance. @param updateCheck The type of update check that will be performed if the updater is allowed to check for updates. @param error The populated error object if the updater may not perform a new update check. The @c NSLocalizedDescriptionKey user info key should be populated indicating a description of the error. @return @c YES if the updater is allowed to check for updates, otherwise @c NO */ - (BOOL)updater:(SPUUpdater *)updater mayPerformUpdateCheck:(SPUUpdateCheck)updateCheck error:(NSError * __autoreleasing *)error; /** Returns the set of Sparkle channels the updater is allowed to find new updates from. An appcast item can specify a channel the update is posted to. Without specifying a channel, the appcast item is posted to the default channel. For instance: ``` 2.0 Beta 1 beta ``` This example posts an update to the @c beta channel, so only updaters that are allowed to use the @c beta channel can find this update. If the @c element is not present, the update item is posted to the default channel and can be found by any updater. You can pick any name you'd like for the channel. The valid characters for channel names are letters, numbers, dashes, underscores, and periods. Note to use this feature, all app versions that your users may update from in your feed must use a version of Sparkle that supports this feature. This feature was added in Sparkle 2. @return The set of channel names the updater is allowed to find new updates in. An empty set is the default behavior, which means the updater will only look for updates in the default channel. The default channel is always included in the allowed set. */ - (NSSet *)allowedChannelsForUpdater:(SPUUpdater *)updater; /** Returns a custom appcast URL used for checking for new updates. Override this to dynamically specify the feed URL. @param updater The updater instance. @return An appcast feed URL to check for new updates in, or @c nil for the default behavior and if you don't want to be delegated this task. */ - (nullable NSString *)feedURLStringForUpdater:(SPUUpdater *)updater; /** Returns additional parameters to append to the appcast URL's query string. This is potentially based on whether or not Sparkle will also be sending along the system profile. @param updater The updater instance. @param sendingProfile Whether the system profile will also be sent. @return An array of dictionaries with keys: `key`, `value`, `displayKey`, `displayValue`, the latter two being specifically for display to the user. */ - (NSArray *> *)feedParametersForUpdater:(SPUUpdater *)updater sendingSystemProfile:(BOOL)sendingProfile; /** Returns whether Sparkle should prompt the user about checking for new updates automatically. Use this to override the default behavior, which is to prompt for permission to check for updates on second app launch (if SUEnableAutomaticChecks is not specified). This method is not called if SUEnableAutomaticChecks is defined in Info.plist or if the user has responded to a permission prompt before. @param updater The updater instance. @return @c YES if the updater should prompt for permission to check for new updates automatically, otherwise @c NO */ - (BOOL)updaterShouldPromptForPermissionToCheckForUpdates:(SPUUpdater *)updater; /** Returns an allowed list of system profile keys to be appended to the appcast URL's query string. By default all keys will be included. This method allows overriding which keys should only be allowed. @param updater The updater instance. @return An array of system profile keys to include in the appcast URL's query string. Elements must be one of the `SUSystemProfiler*Key` constants. Return @c nil for the default behavior and if you don't want to be delegated this task. */ - (nullable NSArray *)allowedSystemProfileKeysForUpdater:(SPUUpdater *)updater; /** Called after Sparkle has downloaded the appcast from the remote server. Implement this if you want to do some special handling with the appcast once it finishes loading. @param updater The updater instance. @param appcast The appcast that was downloaded from the remote server. */ - (void)updater:(SPUUpdater *)updater didFinishLoadingAppcast:(SUAppcast *)appcast; /** Called when a new valid update is found by the update driver. @param updater The updater instance. @param item The appcast item corresponding to the update that is proposed to be installed. */ - (void)updater:(SPUUpdater *)updater didFindValidUpdate:(SUAppcastItem *)item; /** Called when a valid new update is not found. There are various reasons a new update is unavailable and can't be installed. The userInfo dictionary on the error is populated with three keys: - `SPULatestAppcastItemFoundKey`: if available, this may provide the latest `SUAppcastItem` that was found. This will be @c nil if it's unavailable. - `SPUNoUpdateFoundReasonKey`: This will provide the `SPUNoUpdateFoundReason`. For example the reason could be because the latest version in the feed requires a newer OS version or could be because the user is already on the latest version. - `SPUNoUpdateFoundUserInitiatedKey`: A boolean that indicates if a new update was not found when the user intitiated an update check manually. @param updater The updater instance. @param error An error containing information on why a new valid update was not found */ - (void)updaterDidNotFindUpdate:(SPUUpdater *)updater error:(NSError *)error; /** Called when a valid new update is not found. If more information is needed on why an update was not found, use `-[SPUUpdaterDelegate updaterDidNotFindUpdate:error:]` instead. @param updater The updater instance. */ - (void)updaterDidNotFindUpdate:(SPUUpdater *)updater; /** Returns the item in the appcast corresponding to the update that should be installed. Please consider using or migrating to other supported features before adopting this method. Specifically: - If you want to filter out certain tagged updates (like beta updates), consider `-[SPUUpdaterDelegate allowedChannelsForUpdater:]` instead. - If you want to treat certain updates as informational-only, consider supplying @c with a set of affected versions users are updating from. If you're using special logic or extensions in your appcast, implement this to use your own logic for finding a valid update, if any, in the given appcast. Do not base your logic by filtering out items with a minimum or maximum OS version or minimum autoupdate version because Sparkle already has logic for determining whether or not those items should be filtered out. Also do not return a non-top level item from the appcast such as a delta item. Delta items will be ignored. Sparkle picks the delta item from your selection if the appropriate one is available. This method will not be invoked with an appcast that has zero items. Pick the best item from the appcast. If an item is available that has the same version as the application or bundle to update, do not pick an item that is worse than that version. This method may be called multiple times for different selections and filters. This method should be efficient. Return `+[SUAppcastItem emptyAppcastItem]` if no appcast item is valid. Return @c nil if you don't want to be delegated this task and want to let Sparkle handle picking the best valid update. @param appcast The appcast that was downloaded from the remote server. @param updater The updater instance. @return The best valid appcast item. */ - (nullable SUAppcastItem *)bestValidUpdateInAppcast:(SUAppcast *)appcast forUpdater:(SPUUpdater *)updater; /** Returns whether or not the updater should proceed with the new chosen update from the appcast. By default, the updater will always proceed with the best selected update found in an appcast. Override this to override this behavior. If you return @c NO and populate the @c error, the user is not shown this @c updateItem nor is the update downloaded or installed. Note in Swift, this method returns Void and is marked with the throws keyword. If this method doesn't throw an error, the updater will proceed with the update. Otherwise if an error is thrown (we recommend using an NSError), then the will not proceed with the update. @param updater The updater instance. @param updateItem The selected update item to proceed with. @param updateCheck The type of update check that would be performed if proceeded. @param error An error object that must be populated by the delegate if the updater should not proceed with the update. The @c NSLocalizedDescriptionKey user info key should be populated indicating a description of the error. @return @c YES if the updater should proceed with @c updateItem, otherwise @c NO if the updater should not proceed with the update with an @c error populated. */ - (BOOL)updater:(SPUUpdater *)updater shouldProceedWithUpdate:(SUAppcastItem *)updateItem updateCheck:(SPUUpdateCheck)updateCheck error:(NSError * __autoreleasing *)error; /** Called when a user makes a choice to install, dismiss, or skip an update. If the @c choice is `SPUUserUpdateChoiceDismiss` and @c state.stage is `SPUUserUpdateStageDownloaded` the downloaded update is kept around until the next time Sparkle reminds the user of the update. If the @c choice is `SPUUserUpdateChoiceDismiss` and @c state.stage is `SPUUserUpdateStageInstalling` the update is still set to install on application termination. If the @c choice is `SPUUserUpdateChoiceSkip` the user will not be reminded in the future for this update unless they initiate an update check themselves. If @c updateItem.isInformationOnlyUpdate is @c YES the @c choice cannot be `SPUUserUpdateChoiceInstall`. @param updater The updater instance. @param choice The choice (install, dismiss, or skip) the user made for this @c updateItem @param updateItem The appcast item corresponding to the update that the user made a choice on. @param state The current state for the update which includes if the update has already been downloaded or already installing. */ - (void)updater:(SPUUpdater *)updater userDidMakeChoice:(SPUUserUpdateChoice)choice forUpdate:(SUAppcastItem *)updateItem state:(SPUUserUpdateState *)state; /** Returns whether the release notes (if available) should be downloaded after an update is found and shown. This is specifically for the @c element in the appcast item. @param updater The updater instance. @param updateItem The update item to download and show release notes from. @return @c YES to download and show the release notes if available, otherwise @c NO. The default behavior is @c YES. */ - (BOOL)updater:(SPUUpdater *)updater shouldDownloadReleaseNotesForUpdate:(SUAppcastItem *)updateItem; /** Called immediately before downloading the specified update. @param updater The updater instance. @param item The appcast item corresponding to the update that is proposed to be downloaded. @param request The mutable URL request that will be used to download the update. */ - (void)updater:(SPUUpdater *)updater willDownloadUpdate:(SUAppcastItem *)item withRequest:(NSMutableURLRequest *)request; /** Called immediately after successful download of the specified update. @param updater The SUUpdater instance. @param item The appcast item corresponding to the update that has been downloaded. */ - (void)updater:(SPUUpdater *)updater didDownloadUpdate:(SUAppcastItem *)item; /** Called after the specified update failed to download. @param updater The updater instance. @param item The appcast item corresponding to the update that failed to download. @param error The error generated by the failed download. */ - (void)updater:(SPUUpdater *)updater failedToDownloadUpdate:(SUAppcastItem *)item error:(NSError *)error; /** Called when the user cancels an update while it is being downloaded. @param updater The updater instance. */ - (void)userDidCancelDownload:(SPUUpdater *)updater; /** Called immediately before extracting the specified downloaded update. @param updater The SUUpdater instance. @param item The appcast item corresponding to the update that is proposed to be extracted. */ - (void)updater:(SPUUpdater *)updater willExtractUpdate:(SUAppcastItem *)item; /** Called immediately after extracting the specified downloaded update. @param updater The SUUpdater instance. @param item The appcast item corresponding to the update that has been extracted. */ - (void)updater:(SPUUpdater *)updater didExtractUpdate:(SUAppcastItem *)item; /** Called immediately before installing the specified update. @param updater The updater instance. @param item The appcast item corresponding to the update that is proposed to be installed. */ - (void)updater:(SPUUpdater *)updater willInstallUpdate:(SUAppcastItem *)item; /** Returns whether the relaunch should be delayed in order to perform other tasks. This is not called if the user didn't relaunch on the previous update, in that case it will immediately restart. This may also not be called if the application is not going to relaunch after it terminates. @param updater The updater instance. @param item The appcast item corresponding to the update that is proposed to be installed. @param installHandler The install handler that must be completed before continuing with the relaunch. @return @c YES to delay the relaunch until @c installHandler is invoked. */ - (BOOL)updater:(SPUUpdater *)updater shouldPostponeRelaunchForUpdate:(SUAppcastItem *)item untilInvokingBlock:(void (^)(void))installHandler; /** Returns whether the application should be relaunched at all. Some apps **cannot** be relaunched under certain circumstances. This method can be used to explicitly prevent a relaunch. @param updater The updater instance. @return @c YES if the updater should be relaunched, otherwise @c NO if it shouldn't. */ - (BOOL)updaterShouldRelaunchApplication:(SPUUpdater *)updater; /** Called immediately before relaunching. @param updater The updater instance. */ - (void)updaterWillRelaunchApplication:(SPUUpdater *)updater; /** Returns an object that compares version numbers to determine their arithmetic relation to each other. This method allows you to provide a custom version comparator. If you don't implement this method or return @c nil, the standard version comparator will be used. Note that the standard version comparator may be used during installation for preventing a downgrade, even if you provide a custom comparator here. @param updater The updater instance. @return The custom version comparator or @c nil if you don't want to be delegated this task. */ - (nullable id)versionComparatorForUpdater:(SPUUpdater *)updater __deprecated_msg("Custom version comparators are deprecated because they are incompatible with how the system compares different versions of an app."); /** Called when a background update will be scheduled after a delay. Automatic update checks need to be enabled for this to trigger. @param delay The delay in seconds until the next scheduled update will occur. This is an approximation and may vary due to system state. @param updater The updater instance. */ - (void)updater:(SPUUpdater *)updater willScheduleUpdateCheckAfterDelay:(NSTimeInterval)delay; /** Called when no update checks will be scheduled in the future. This may later change if automatic update checks become enabled. @param updater The updater instance. */ - (void)updaterWillNotScheduleUpdateCheck:(SPUUpdater *)updater; /** Returns the decryption password (if any) which is used to extract the update archive DMG. Return @c nil if no password should be used. @param updater The updater instance. @return The password used for decrypting the archive, or @c nil if no password should be used. */ - (nullable NSString *)decryptionPasswordForUpdater:(SPUUpdater *)updater; /** Called when an update is scheduled to be silently installed on quit after downloading the update automatically. If you want to intercept this method without taking control of installing the update, return @c NO. This will let future update cycles to run and allow Sparkle to present the update to the user later if certain conditions are met. For example, critical updates will be presented to the user right away. Other updates may be presented later if the user hasn't terminated the application for a long time (defined by `SUScheduledImpatientCheckInterval`). If you want to take control of installing the update, return @c YES. This stalls the current update cycle and prevents future update cycles from running. When the opportunity arrives, you can invoke `immediateInstallHandler` to install the update and relaunch the application without any UI interaction shown. In either case Sparkle will always attempt to install the update when the app terminates. @param updater The updater instance. @param item The appcast item corresponding to the update that is proposed to be installed. @param immediateInstallHandler The install handler to immediately install the update and relaunch the application. This handler can only be used if @c YES is returned. For Sparkle 2.3 onwards, this handler can be invoked multiple times in case the application cancels the termination request. @return @c YES if you will handle installing the update using `immediateInstallHandler` or @c NO to allow Sparkle's update scheduler to continue running. */ - (BOOL)updater:(SPUUpdater *)updater willInstallUpdateOnQuit:(SUAppcastItem *)item immediateInstallationBlock:(void (^)(void))immediateInstallHandler; /** Called after the update driver aborts due to an error. The update driver runs when checking for updates. This delegate method is called an error occurs during this process. Some special possible values of `error.code` are: - `SUNoUpdateError`: No new update was found. - `SUInstallationCanceledError`: The user canceled installing the update when requested for authorization. @param updater The updater instance. @param error The error that caused the update driver to abort. */ - (void)updater:(SPUUpdater *)updater didAbortWithError:(NSError *)error; /** Called after the update driver finishes. The update driver runs when checking for updates. This delegate method is called when that check is finished. An update may be scheduled to be installed during the update cycle, or no updates may be found, or an available update may be dismissed or skipped (which is the same as no error). If the @c error is @c nil, no error has occurred. Some special possible values of `error.code` are: - `SUNoUpdateError`: No new update was found. - `SUInstallationCanceledError`: The user canceled installing the update when requested for authorization. @param updater The updater instance. @param updateCheck The type of update check was performed. @param error The error that caused the update driver to abort. This is @c nil if the update driver finished normally and there is no error. */ - (void)updater:(SPUUpdater *)updater didFinishUpdateCycleForUpdateCheck:(SPUUpdateCheck)updateCheck error:(nullable NSError *)error; /* Deprecated methods */ - (BOOL)updaterMayCheckForUpdates:(SPUUpdater *)updater __deprecated_msg("Please use -[SPUUpdaterDelegate updater:mayPerformUpdateCheck:error:] instead."); - (void)updater:(SPUUpdater *)updater userDidSkipThisVersion:(SUAppcastItem *)item __deprecated_msg("Please use -[SPUUpdaterDelegate updater:userDidMakeChoice:forUpdate:state:] instead."); @end NS_ASSUME_NONNULL_END ================================================ FILE: Sparkle/SPUUpdaterSettings+Debug.h ================================================ // // SPUUpdaterSettings+Debug.h // Sparkle // // Created on 11/16/25. // Copyright © 2025 Sparkle Project. All rights reserved. // #import "SPUUpdaterSettings.h" /** * Private settings which may be debug gated for the Sparkle Test App under DEBUG */ @interface SPUUpdaterSettings (Debug) /** * The minimum update check interval */ @property (nonatomic, readonly) NSTimeInterval minimumUpdateCheckInterval; /** * The amount of time the system can defer our update check (for improved performance) */ @property (nonatomic, readonly) uint64_t leewayUpdateCheckInterval; /** * The amount of time the app is allowed to be idle for us to consider showing an update prompt right away when the app is active. * * This is for the standard user driver. */ @property (nonatomic, readonly) NSTimeInterval standardUIScheduledUpdateIdleEventLeewayInterval; @end ================================================ FILE: Sparkle/SPUUpdaterSettings.h ================================================ // // SPUUpdaterSettings.h // Sparkle // // Created by Mayur Pawashe on 3/27/16. // Copyright © 2016 Sparkle Project. All rights reserved. // #import #if defined(BUILDING_SPARKLE_SOURCES_EXTERNALLY) // Ignore incorrect warning #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wquoted-include-in-framework-header" #import "SUExport.h" #pragma clang diagnostic pop #else #import #endif NS_ASSUME_NONNULL_BEGIN /** This class can be used for reading and updating updater settings. It retrieves the settings by first looking into the host's user defaults. If the setting is not found in there, then the host's Info.plist file is looked at. For updating updater settings, changes are made in the host's user defaults. */ SU_EXPORT NS_SWIFT_UI_ACTOR @interface SPUUpdaterSettings : NSObject - (instancetype)init NS_UNAVAILABLE; - (instancetype)initWithHostBundle:(NSBundle *)hostBundle; /** * Indicates whether or not automatic update checks are enabled. * * This property is KVO compliant. This property must be called on the main thread. */ @property (nonatomic) BOOL automaticallyChecksForUpdates; /** * The regular update check interval. * * This property is KVO compliant. This property must be called on the main thread. */ @property (nonatomic) NSTimeInterval updateCheckInterval; /** * Indicates whether or not automatically downloading updates is allowed to be turned on by the user. * * This property is determined by checking `automaticallyChecksForUpdates` and `allowsAutomaticUpdatesOption`. * * This property is KVO compliant. This property must be called on the main thread. */ @property (readonly, nonatomic) BOOL allowsAutomaticUpdates; /** * Indicates whether or not automatically downloading updates is enabled by the user or developer. * * Note this does not indicate whether or not automatic downloading of updates is allowable. * See `-allowsAutomaticUpdates` property for that. * * This property is KVO compliant. This property must be called on the main thread. */ @property (nonatomic) BOOL automaticallyDownloadsUpdates; /** * Indicates whether or not the developer allows turning on updates being automatically downloaded and installed. * If this value is nil, the developer has not explicitly specified this option (which is the default). * * Please prefer to use `allowsAutomaticUpdates` instead. */ @property (readonly, nonatomic, nullable) NSNumber *allowsAutomaticUpdatesOption; /** * The impatient update check interval. * * If an update has already been downloaded automatically in the background, Sparkle may not notify users of the update immediately, * and tries to install the update siliently on quit without notifying the user. * * Sparkle uses this long impatient update check interval to decide when to notify the user of the update if they haven't quit the app for a long time. * By default this check interval is set to 604800 seconds (which is 1 week). This interval must be bigger than the `updateCheckInterval`. */ @property (nonatomic, readonly) NSTimeInterval impatientUpdateCheckInterval; /** * Indicates whether or not anonymous system profile information is sent when checking for updates. * * This property is KVO compliant. This property must be called on the main thread. */ @property (nonatomic) BOOL sendsSystemProfile; @end NS_ASSUME_NONNULL_END ================================================ FILE: Sparkle/SPUUpdaterSettings.m ================================================ // // SPUUpdaterSettings.m // Sparkle // // Created by Mayur Pawashe on 3/27/16. // Copyright © 2016 Sparkle Project. All rights reserved. // #import "SPUUpdaterSettings.h" #import "SPUUpdaterSettings+Debug.h" #import "SUHost.h" #import "SUConstants.h" #include "AppKitPrevention.h" static NSString *SUAutomaticallyChecksForUpdatesKeyPath = @"automaticallyChecksForUpdates"; static NSString *SUUpdateCheckIntervalKeyPath = @"updateCheckInterval"; static NSString *SUImpatientUpdateCheckIntervalKeyPath = @"impatientUpdateCheckInterval"; static NSString *SUAutomaticallyDownloadsUpdatesKeyPath = @"automaticallyDownloadsUpdates"; static NSString *SUSendsSystemProfileKeyPath = @"sendsSystemProfile"; static NSString *SUAllowsAutomaticUpdatesOptionKeyPath = @"allowsAutomaticUpdatesOption"; static NSString *SUAllowsAutomaticUpdatesKeyPath = @"allowsAutomaticUpdates"; @implementation SPUUpdaterSettings { SUHost *_host; #if DEBUG BOOL _enableDebugUpdateCheckIntervals; #endif } @synthesize automaticallyChecksForUpdates = _automaticallyChecksForUpdates; @synthesize updateCheckInterval = _updateCheckInterval; @synthesize impatientUpdateCheckInterval = _impatientUpdateCheckInterval; @synthesize automaticallyDownloadsUpdates = _automaticallyDownloadsUpdates; @synthesize sendsSystemProfile = _sendsSystemProfile; @synthesize allowsAutomaticUpdatesOption = _allowsAutomaticUpdatesOption; @synthesize allowsAutomaticUpdates = _allowsAutomaticUpdates; - (instancetype)initWithHostBundle:(NSBundle *)hostBundle { self = [super init]; if (self != nil) { _host = [[SUHost alloc] initWithBundle:hostBundle]; #if DEBUG // This one must be checked first, before checking the other settings, // since the others may rely on this _enableDebugUpdateCheckIntervals = [self currentEnableDebugUpdateCheckIntervals]; #endif _automaticallyChecksForUpdates = [self currentAutomaticallyChecksForUpdates]; _updateCheckInterval = [self currentUpdateCheckInterval]; _impatientUpdateCheckInterval = [self currentImpatientUpdateCheckInterval]; _allowsAutomaticUpdatesOption = [self currentAllowsAutomaticUpdatesOption]; _allowsAutomaticUpdates = [self currentAllowsAutomaticUpdates]; _automaticallyDownloadsUpdates = [self currentAutomaticallyDownloadsUpdates]; _sendsSystemProfile = [self currentSendsSystemProfile]; [NSNotificationCenter.defaultCenter addObserver:self selector:@selector(synchronize:) name:SUUpdateSettingsNeedsSynchronizationNotification object:nil]; __weak __typeof__(self) weakSelf = self; [_host observeChangesFromUserDefaultKeys:[NSSet setWithArray:@[SUEnableAutomaticChecksKey, SUScheduledCheckIntervalKey, SUAutomaticallyUpdateKey, SUSendProfileInfoKey]] changeHandler:^(NSString *keyPath) { __typeof(self) strongSelf = weakSelf; if (strongSelf == nil) { return; } if ([keyPath isEqualToString:SUEnableAutomaticChecksKey]) { [strongSelf processCurrentAutomaticallyChecksForUpdates]; } else if ([keyPath isEqualToString:SUScheduledCheckIntervalKey]) { [strongSelf processUpdateCheckInterval]; } else if ([keyPath isEqualToString:SUAutomaticallyUpdateKey]) { [strongSelf processAutomaticallyDownloadsUpdates]; } else if ([keyPath isEqualToString:SUSendProfileInfoKey]) { [strongSelf processSendsSystemProfile]; } }]; } return self; } - (void)dealloc { [NSNotificationCenter.defaultCenter removeObserver:self name:SUUpdateSettingsNeedsSynchronizationNotification object:_host.bundlePath]; } - (void)processCurrentAutomaticallyChecksForUpdates SPU_OBJC_DIRECT { BOOL currentValue = [self currentAutomaticallyChecksForUpdates]; if (currentValue != _automaticallyChecksForUpdates) { NSString *updatedKeyPath = SUAutomaticallyChecksForUpdatesKeyPath; [self willChangeValueForKey:updatedKeyPath]; _automaticallyChecksForUpdates = currentValue; [self didChangeValueForKey:updatedKeyPath]; [self processAllowsAutomaticUpdates]; [self processAutomaticallyDownloadsUpdates]; } } - (void)processUpdateCheckInterval SPU_OBJC_DIRECT { NSTimeInterval currentValue = [self currentUpdateCheckInterval]; if (fabs(currentValue - _updateCheckInterval) >= 0.001) { NSString *updatedKeyPath = SUUpdateCheckIntervalKeyPath; [self willChangeValueForKey:updatedKeyPath]; _updateCheckInterval = currentValue; [self didChangeValueForKey:updatedKeyPath]; } } - (void)processImpatientUpdateCheckInterval SPU_OBJC_DIRECT { NSTimeInterval currentValue = [self currentImpatientUpdateCheckInterval]; if (fabs(currentValue - _impatientUpdateCheckInterval) >= 0.001) { NSString *updatedKeyPath = SUImpatientUpdateCheckIntervalKeyPath; [self willChangeValueForKey:updatedKeyPath]; _impatientUpdateCheckInterval = currentValue; [self didChangeValueForKey:updatedKeyPath]; } } - (void)processAllowsAutomaticUpdatesOption SPU_OBJC_DIRECT { NSNumber *currentValue = [self currentAllowsAutomaticUpdatesOption]; if (((currentValue != nil) != (_allowsAutomaticUpdatesOption != nil)) || (currentValue.boolValue != _allowsAutomaticUpdatesOption.boolValue)) { NSString *updatedKeyPath = SUAllowsAutomaticUpdatesOptionKeyPath; [self willChangeValueForKey:updatedKeyPath]; _allowsAutomaticUpdatesOption = currentValue; [self didChangeValueForKey:updatedKeyPath]; } } - (void)processAllowsAutomaticUpdates SPU_OBJC_DIRECT { BOOL currentValue = [self currentAllowsAutomaticUpdates]; if (currentValue != _allowsAutomaticUpdates) { NSString *updatedKeyPath = SUAllowsAutomaticUpdatesKeyPath; [self willChangeValueForKey:updatedKeyPath]; _allowsAutomaticUpdates = currentValue; [self didChangeValueForKey:updatedKeyPath]; } } - (void)processAutomaticallyDownloadsUpdates SPU_OBJC_DIRECT { BOOL currentValue = [self currentAutomaticallyDownloadsUpdates]; if (currentValue != _automaticallyDownloadsUpdates) { NSString *updatedKeyPath = SUAutomaticallyDownloadsUpdatesKeyPath; [self willChangeValueForKey:updatedKeyPath]; _automaticallyDownloadsUpdates = currentValue; [self didChangeValueForKey:updatedKeyPath]; } } - (void)processSendsSystemProfile SPU_OBJC_DIRECT { BOOL currentValue = [self currentSendsSystemProfile]; if (currentValue != _sendsSystemProfile) { NSString *updatedKeyPath = SUSendsSystemProfileKeyPath; [self willChangeValueForKey:updatedKeyPath]; _sendsSystemProfile = currentValue; [self didChangeValueForKey:updatedKeyPath]; } } - (void)synchronize:(NSNotification *)notification { NSString *bundlePath = notification.userInfo[SUUpdateBundlePathUserInfoKey]; if (![bundlePath isEqualToString:_host.bundlePath]) { return; } #if DEBUG // This one must be checked first, before checking the other settings, // since the others may rely on this _enableDebugUpdateCheckIntervals = [self currentEnableDebugUpdateCheckIntervals]; #endif [self processCurrentAutomaticallyChecksForUpdates]; [self processUpdateCheckInterval]; [self processImpatientUpdateCheckInterval]; [self processAllowsAutomaticUpdatesOption]; [self processAllowsAutomaticUpdates]; [self processAutomaticallyDownloadsUpdates]; [self processSendsSystemProfile]; } - (BOOL)currentAutomaticallyChecksForUpdates SPU_OBJC_DIRECT { // Don't automatically update when the check interval is 0, to be compatible with 1.1 settings. if ((NSInteger)[self currentUpdateCheckInterval] == 0) { return NO; } return [_host boolForKey:SUEnableAutomaticChecksKey]; } - (void)setAutomaticallyChecksForUpdates:(BOOL)automaticallyCheckForUpdates { [self willChangeValueForKey:SUAutomaticallyChecksForUpdatesKeyPath]; _automaticallyChecksForUpdates = automaticallyCheckForUpdates; [_host setBool:automaticallyCheckForUpdates forUserDefaultsKey:SUEnableAutomaticChecksKey]; [self didChangeValueForKey:SUAutomaticallyChecksForUpdatesKeyPath]; // Hack to support backwards compatibility with older Sparkle versions, which supported // disabling updates by setting the check interval to 0. if (automaticallyCheckForUpdates && (NSInteger)[self currentUpdateCheckInterval] == 0) { [self setUpdateCheckInterval:[self defaultUpdateCheckInterval]]; } else { [NSNotificationCenter.defaultCenter postNotificationName:SUUpdateAutomaticCheckSettingChangedNotification object:nil userInfo:@{SUUpdateBundlePathUserInfoKey: _host.bundlePath}]; } [self processAllowsAutomaticUpdates]; [self processAutomaticallyDownloadsUpdates]; } + (BOOL)automaticallyNotifiesObserversOfAutomaticallyChecksForUpdates { return NO; } - (NSTimeInterval)currentUpdateCheckInterval SPU_OBJC_DIRECT { // Find the stored check interval. User defaults override Info.plist. NSNumber *intervalValue = [_host doubleNumberForKey:SUScheduledCheckIntervalKey]; if (intervalValue == nil) { return [self defaultUpdateCheckInterval]; } return intervalValue.doubleValue; } - (void)setUpdateCheckInterval:(NSTimeInterval)updateCheckInterval { [self willChangeValueForKey:SUUpdateCheckIntervalKeyPath]; _updateCheckInterval = updateCheckInterval; [_host setObject:@(updateCheckInterval) forUserDefaultsKey:SUScheduledCheckIntervalKey]; [self didChangeValueForKey:SUUpdateCheckIntervalKeyPath]; if ((NSInteger)updateCheckInterval == 0) { // For compatibility with 1.1's settings. [self setAutomaticallyChecksForUpdates:NO]; } else { [NSNotificationCenter.defaultCenter postNotificationName:SUUpdateAutomaticCheckSettingChangedNotification object:nil userInfo:@{SUUpdateBundlePathUserInfoKey: _host.bundlePath}]; } } - (NSTimeInterval)currentImpatientUpdateCheckInterval SPU_OBJC_DIRECT { NSNumber *intervalValue = [_host doubleNumberForInfoDictionaryKey:SUScheduledImpatientCheckIntervalKey]; if (intervalValue == nil) { return [self defaultImpatientUpdateCheckInterval]; } return intervalValue.doubleValue; } + (BOOL)automaticallyNotifiesObserversOfUpdateCheckInterval { return NO; } + (BOOL)automaticallyNotifiesObserversOfImpatientUpdateCheckInterval { return NO; } - (NSNumber * _Nullable)currentAllowsAutomaticUpdatesOption SPU_OBJC_DIRECT { NSNumber *developerAllowsAutomaticUpdates = [_host boolNumberForInfoDictionaryKey:SUAllowsAutomaticUpdatesKey]; return developerAllowsAutomaticUpdates; } + (BOOL)automaticallyNotifiesObserversOfAllowsAutomaticUpdatesOption { return NO; } // This depends on currentAllowsAutomaticUpdatesOption and currentAutomaticallyChecksForUpdates and must be processed afterwards - (BOOL)currentAllowsAutomaticUpdates { return (_allowsAutomaticUpdatesOption == nil) ? _automaticallyChecksForUpdates : _allowsAutomaticUpdatesOption.boolValue; } + (BOOL)automaticallyNotifiesObserversOfAllowsAutomaticUpdates { return NO; } // This depends on currentAllowsAutomaticUpdates and must be processed afterwards - (BOOL)currentAutomaticallyDownloadsUpdates SPU_OBJC_DIRECT { return _allowsAutomaticUpdates && [_host boolForKey:SUAutomaticallyUpdateKey]; } - (void)setAutomaticallyDownloadsUpdates:(BOOL)automaticallyDownloadsUpdates { if (![self allowsAutomaticUpdates]) { return; } [self willChangeValueForKey:SUAutomaticallyDownloadsUpdatesKeyPath]; _automaticallyDownloadsUpdates = automaticallyDownloadsUpdates; [_host setBool:automaticallyDownloadsUpdates forUserDefaultsKey:SUAutomaticallyUpdateKey]; [self didChangeValueForKey:SUAutomaticallyDownloadsUpdatesKeyPath]; } + (BOOL)automaticallyNotifiesObserversOfAutomaticallyDownloadsUpdates { return NO; } - (BOOL)currentSendsSystemProfile SPU_OBJC_DIRECT { return [_host boolForKey:SUSendProfileInfoKey]; } - (void)setSendsSystemProfile:(BOOL)sendsSystemProfile { [self willChangeValueForKey:SUSendsSystemProfileKeyPath]; _sendsSystemProfile = sendsSystemProfile; [_host setBool:sendsSystemProfile forUserDefaultsKey:SUSendProfileInfoKey]; [self didChangeValueForKey:SUSendsSystemProfileKeyPath]; } + (BOOL)automaticallyNotifiesObserversOfSendsSystemProfile { return NO; } #if DEBUG // This is only used in DEBUG and is meant for the Sparkle Test App - (BOOL)currentEnableDebugUpdateCheckIntervals { return [_host boolForInfoDictionaryKey:@"_SUEnableDebugUpdateCheckIntervals"]; } #endif - (NSTimeInterval)minimumUpdateCheckInterval { #if DEBUG if (_enableDebugUpdateCheckIntervals) { // 1 minute return 60; } #endif // 1 hour return (60 * 60); } - (uint64_t)leewayUpdateCheckInterval { #if DEBUG if (_enableDebugUpdateCheckIntervals) { // 1 second return 1; } #endif // 15 seconds return 15; } - (NSTimeInterval)defaultUpdateCheckInterval SPU_OBJC_DIRECT { #if DEBUG if (_enableDebugUpdateCheckIntervals) { // 1 minute return 60; } #endif // 1 day return (60 * 60 * 24); } // If the update has already been automatically downloaded, we normally don't want to bug the user about the update // However if the user has gone a very long time without quitting an application, we will notify them - (NSTimeInterval)defaultImpatientUpdateCheckInterval SPU_OBJC_DIRECT { #if DEBUG if (_enableDebugUpdateCheckIntervals) { // 2 minutes return (60 * 2); } #endif // 1 week return (60 * 60 * 24 * 7); } - (NSTimeInterval)standardUIScheduledUpdateIdleEventLeewayInterval { #if DEBUG if (_enableDebugUpdateCheckIntervals) { // 30 seconds return 30.0; } #endif // 5 minutes return (5 * 60.0); } @end ================================================ FILE: Sparkle/SPUUpdaterTimer.h ================================================ // // SPUUpdaterTimer.h // Sparkle // // Created by Mayur Pawashe on 8/12/16. // Copyright © 2016 Sparkle Project. All rights reserved. // #import NS_ASSUME_NONNULL_BEGIN @protocol SPUUpdaterTimerDelegate - (void)updaterTimerDidFire; @end // This notifies the updater for scheduled update checks // This class is used so that an updater instance isn't kept alive by a scheduled update check SPU_OBJC_DIRECT_MEMBERS @interface SPUUpdaterTimer : NSObject - (instancetype)initWithDelegate:(id)delegate; - (void)startAndFireAfterDelay:(NSTimeInterval)delay leewayUpdateCheckInterval:(uint64_t)leewayUpdateCheckInterval; - (void)invalidate; @end NS_ASSUME_NONNULL_END ================================================ FILE: Sparkle/SPUUpdaterTimer.m ================================================ // // SPUUpdaterTimer.m // Sparkle // // Created by Mayur Pawashe on 8/12/16. // Copyright © 2016 Sparkle Project. All rights reserved. // #import "SPUUpdaterTimer.h" #import "SUConstants.h" #include "AppKitPrevention.h" @implementation SPUUpdaterTimer { dispatch_source_t _source; __weak id _delegate; } - (instancetype)initWithDelegate:(id)delegate { self = [super init]; if (self != nil) { _delegate = delegate; } return self; } - (void)startAndFireAfterDelay:(NSTimeInterval)delay leewayUpdateCheckInterval:(uint64_t)leewayUpdateCheckInterval { _source = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_main_queue()); // We use the wall time instead of cpu time for our dispatch timer // So eg if the computer sleeps we want to include that time spent in our timer dispatch_time_t timeToFire = dispatch_walltime(NULL, (int64_t)(delay * NSEC_PER_SEC)); dispatch_source_set_timer(_source, timeToFire, DISPATCH_TIME_FOREVER, leewayUpdateCheckInterval * NSEC_PER_SEC); __weak __typeof__(self) weakSelf = self; dispatch_source_set_event_handler(_source, ^{ __typeof__(self) strongSelf = weakSelf; if (strongSelf != nil) { [strongSelf->_delegate updaterTimerDidFire]; } }); dispatch_resume(_source); } - (void)invalidate { if (_source != nil) { dispatch_source_cancel(_source); _source = nil; } } @end ================================================ FILE: Sparkle/SPUUserAgent+Private.h ================================================ // // SPUUserAgent+Private.h // Sparkle // // Created by Mayur Pawashe on 11/12/21. // Copyright © 2021 Sparkle Project. All rights reserved. // #import #if defined(BUILDING_SPARKLE_SOURCES_EXTERNALLY) // Ignore incorrect warning #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wquoted-include-in-framework-header" #import "SUExport.h" #pragma clang diagnostic pop #else #import #endif NS_ASSUME_NONNULL_BEGIN @class SUHost; SU_EXPORT NSString *SPUMakeUserAgentWithHost(SUHost *responsibleHost, NSString * _Nullable displayNameSuffix); SU_EXPORT NSString *SPUMakeUserAgentWithBundle(NSBundle *responsibleBundle, NSString * _Nullable displayNameSuffix); NS_ASSUME_NONNULL_END ================================================ FILE: Sparkle/SPUUserAgent+Private.m ================================================ // // SPUUserAgent+Private.m // Sparkle // // Created by Mayur Pawashe on 11/12/21. // Copyright © 2021 Sparkle Project. All rights reserved. // #import "SPUUserAgent+Private.h" #import "SUHost.h" NSString *SPUMakeUserAgentWithBundle(NSBundle *responsibleBundle, NSString * _Nullable displayNameSuffix) { SUHost *responsibleHost = [[SUHost alloc] initWithBundle:responsibleBundle]; return SPUMakeUserAgentWithHost(responsibleHost, displayNameSuffix); } NSString *SPUMakeUserAgentWithHost(SUHost *responsibleHost, NSString * _Nullable displayNameSuffix) { NSString *displayVersion = responsibleHost.displayVersion; NSString *userAgent = [NSString stringWithFormat:@"%@%@/%@ Sparkle/%@", responsibleHost.name, (displayNameSuffix != nil ? displayNameSuffix : @""), (displayVersion.length > 0 ? displayVersion : @"?"), @""MARKETING_VERSION]; NSData *cleanedAgent = [userAgent dataUsingEncoding:NSASCIIStringEncoding allowLossyConversion:YES]; NSString *result; if (cleanedAgent != nil) { NSString *cleanedAgentString = [[NSString alloc] initWithData:(NSData * _Nonnull)cleanedAgent encoding:NSASCIIStringEncoding]; if (cleanedAgentString != nil) { result = cleanedAgentString; } else { result = @""; } } else { result = @""; } return result; } ================================================ FILE: Sparkle/SPUUserDriver.h ================================================ // // SPUUserDriver.h // Sparkle // // Created by Mayur Pawashe on 2/14/16. // Copyright © 2016 Sparkle Project. All rights reserved. // #import #if defined(BUILDING_SPARKLE_SOURCES_EXTERNALLY) // Ignore incorrect warning #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wquoted-include-in-framework-header" #import "SPUUserUpdateState.h" #import "SUExport.h" #pragma clang diagnostic pop #else #import #import #endif NS_ASSUME_NONNULL_BEGIN @class SPUUpdatePermissionRequest, SUUpdatePermissionResponse, SUAppcastItem, SPUDownloadData; /** The API in Sparkle for controlling the user interaction. This protocol is used for implementing a user interface for the Sparkle updater. Sparkle's internal drivers tell an object that implements this protocol what actions to take and show to the user. Every method in this protocol can be assumed to be called from the main thread. */ SU_EXPORT NS_SWIFT_UI_ACTOR @protocol SPUUserDriver /** * Show an updater permission request to the user * * Ask the user for their permission regarding update checks. * This is typically only called once per app installation. * * @param request The update permission request. * @param reply A reply with a update permission response. */ - (void)showUpdatePermissionRequest:(SPUUpdatePermissionRequest *)request reply:(void (^)(SUUpdatePermissionResponse *))reply; /** * Show the user initiating an update check * * Respond to the user initiating an update check. Sparkle uses this to show the user a window with an indeterminate progress bar. * * @param cancellation Invoke this cancellation block to cancel the update check before the update check is completed. */ - (void)showUserInitiatedUpdateCheckWithCancellation:(void (^)(void))cancellation; /** * Show the user a new update is found. * * Let the user know a new update is found and ask them what they want to do. * Before this point, `-showUserInitiatedUpdateCheckWithCancellation:` may be called. * * The potential `stage`s on the updater @c state are: * * `SPUUpdateStateNotDownloaded` - Update has not been downloaded yet. * * `SPUUpdateStateDownloaded` - Update has already been downloaded in the background automatically (via `SUAutomaticallyUpdate`) but not started installing yet. * * `SPUUpdateStateInstalling` - Update has been downloaded and already started installing. * * The `userInitiated` property on the @c state indicates if the update was initiated by the user or if it was automatically scheduled in the background. * * Additionally, these properties on the @c appcastItem are of importance: * * @c appcastItem.informationOnlyUpdate indicates if the update is only informational and should not be downloaded. You can direct the user to the infoURL property of the appcastItem in their web browser. Sometimes information only updates are used as a fallback in case a bad update is shipped, so you'll want to support this case. * * @c appcastItem.majorUpgrade indicates if the update is a major or paid upgrade. * * @c appcastItem.criticalUpdate indicates if the update is a critical update. * * @c appcastItem.signingValidationStatus indicates the signing validation status of the appcast, which may be applicable if appcast signing is required. * * A reply of `SPUUserUpdateChoiceInstall` begins or resumes downloading, extracting, or installing the update. * If the state.stage is `SPUUserUpdateStateInstalling`, this may send a quit event to the application and relaunch it immediately (in this state, this behaves as a fast "install and Relaunch"). * If the state.stage is `SPUUpdateStateNotDownloaded` or `SPUUpdateStateDownloaded` the user may be presented an authorization prompt to install the update after `-showDownloadDidStartExtractingUpdate` is called if authorization is required for installation. For example, this may occur if the update on disk is owned by a different user (e.g. root or admin for non-admin users), or if the update is a package install. * Do not use a reply of `SPUUserUpdateChoiceInstall` if @c appcastItem.informationOnlyUpdate is YES. * * A reply of `SPUUserUpdateChoiceDismiss` dismisses the update for the time being. The user may be reminded of the update at a later point. * If the state.stage is `SPUUserUpdateStateDownloaded`, the downloaded update is kept after dismissing until the next time an update is shown to the user. * If the state.stage is `SPUUserUpdateStateInstalling`, the installing update is also preserved after dismissing. In this state however, the update will also still be installed after the application is terminated. * * A reply of `SPUUserUpdateChoiceSkip` skips this particular version and won't notify the user again, unless they initiate an update check themselves. * If @c appcastItem.majorUpgrade is YES, the major update and any future minor updates to that major release are skipped, unless a future minor update specifies a `` requirement. * If the state.stage is `SPUUpdateStateInstalling`, the installation is also canceled when the update is skipped. * * @param appcastItem The Appcast Item containing information that reflects the new update. * @param state The current state of the user update. See above discussion for notable properties. * @param reply The reply which indicates if the update should be installed, dismissed, or skipped. See above discussion for more details. */ - (void)showUpdateFoundWithAppcastItem:(SUAppcastItem *)appcastItem state:(SPUUserUpdateState *)state reply:(void (^)(SPUUserUpdateChoice))reply; /** * Show the user the release notes for the new update * * Display the release notes to the user. This will be called after showing the new update. * This is only applicable if the release notes are linked from the appcast, and are not directly embedded inside of the appcast file. * That is, this may be invoked if the releaseNotesURL from the appcast item is non-nil. * * @param downloadData The data for the release notes that was downloaded from the new update's appcast. */ - (void)showUpdateReleaseNotesWithDownloadData:(SPUDownloadData *)downloadData; /** * Show the user that the new update's release notes could not be downloaded * * This will be called after showing the new update. * This is only applicable if the release notes are linked from the appcast, and are not directly embedded inside of the appcast file. * That is, this may be invoked if the releaseNotesURL from the appcast item is non-nil. * * @param error The error associated with why the new update's release notes could not be downloaded. */ - (void)showUpdateReleaseNotesFailedToDownloadWithError:(NSError *)error; /** * Show the user a new update was not found * * Let the user know a new update was not found after they tried initiating an update check. * Before this point, `-showUserInitiatedUpdateCheckWithCancellation:` may be called. * * There are various reasons a new update is unavailable and can't be installed. * The @c error object is populated with recovery and suggestion strings suitable to be shown in an alert. * * The @c userInfo dictionary on the @c error is also populated with two keys: * * `SPULatestAppcastItemFoundKey`: if available, this may provide the latest SUAppcastItem that was found. * * `SPUNoUpdateFoundReasonKey`: if available, this will provide the `SUNoUpdateFoundReason`. For example the reason could be because * the latest version in the feed requires a newer OS version or could be because the user is already on the latest version. * * @param error The error associated with why a new update was not found. See above discussion for more details. * @param acknowledgement Acknowledge to the updater that no update found error was shown. */ - (void)showUpdateNotFoundWithError:(NSError *)error acknowledgement:(void (^)(void))acknowledgement NS_SWIFT_ASYNC(2); /** * Show the user an update error occurred * * Let the user know that the updater failed with an error. This will not be invoked without the user having been * aware that an update was in progress. * * Before this point, any of the non-error user driver methods may have been invoked. * * @param error The error associated with what update error occurred. * @param acknowledgement Acknowledge to the updater that the error was shown. */ - (void)showUpdaterError:(NSError *)error acknowledgement:(void (^)(void))acknowledgement NS_SWIFT_ASYNC(2); /** * Show the user that downloading the new update initiated * * Let the user know that downloading the new update started. * * @param cancellation Invoke this cancellation block to cancel the download at any point before `-showDownloadDidStartExtractingUpdate` is invoked. */ - (void)showDownloadInitiatedWithCancellation:(void (^)(void))cancellation; /** * Show the user the content length of the new update that will be downloaded * * @param expectedContentLength The expected content length of the new update being downloaded. * An implementor should be able to handle if this value is invalid (more or less than actual content length downloaded). * Additionally, this method may be called more than once for the same download in rare scenarios. */ - (void)showDownloadDidReceiveExpectedContentLength:(uint64_t)expectedContentLength; /** * Show the user that the update download received more data * * This may be an appropriate time to advance a visible progress indicator of the download * @param length The length of the data that was just downloaded */ - (void)showDownloadDidReceiveDataOfLength:(uint64_t)length; /** * Show the user that the update finished downloading and started extracting * * Sparkle uses this to show an indeterminate progress bar. * * Before this point, `showDownloadDidReceiveDataOfLength:` or `showUpdateFoundWithAppcastItem:state:reply:` may be called. * An update can potentially resume at this point after having been automatically downloaded in the background (without the user driver) before. * * After extraction starts, the user may be shown an authorization prompt to install the update if authorization is required for installation. * For example, this may occur if the update on disk is owned by a different user (e.g. root or admin for non-admin users), or if the update is a package install. */ - (void)showDownloadDidStartExtractingUpdate; /** * Show the user that the update is extracting with progress * * Let the user know how far along the update extraction is. * * Before this point, `-showDownloadDidStartExtractingUpdate` is called. * * @param progress The progress of the extraction from a 0.0 to 1.0 scale */ - (void)showExtractionReceivedProgress:(double)progress; /** * Show the user that the update is ready to install & relaunch * * Let the user know that the update is ready to install and relaunch, and ask them whether they want to proceed. * Note if the target application has already terminated, this method may not be invoked. * * A reply of `SPUUserUpdateChoiceInstall` installs the update the new update immediately. The application is relaunched only if it is still running by the time this reply is invoked. If the application terminates on its own, Sparkle will attempt to automatically install the update. * * A reply of `SPUUserUpdateChoiceDismiss` dismisses the update installation for the time being. Note the update may still be installed automatically after the application terminates. * * A reply of `SPUUserUpdateChoiceSkip` cancels the current update that has begun installing and dismisses the update. In this circumstance, the update is canceled but this update version is not skipped in the future. * * Before this point, `-showExtractionReceivedProgress:` or `-showUpdateFoundWithAppcastItem:state:reply:` may be called. * * @param reply The reply which indicates if the update should be installed, dismissed, or skipped. See above discussion for more details. */ - (void)showReadyToInstallAndRelaunch:(void (^)(SPUUserUpdateChoice))reply; /** * Show the user that the update is installing * * Let the user know that the update is currently installing. * * Before this point, `-showReadyToInstallAndRelaunch:` or `-showUpdateFoundWithAppcastItem:state:reply:` will be called. * * @param applicationTerminated Indicates if the application has been terminated already. * If the application hasn't been terminated, a quit event is sent to the running application before installing the update. * If the application or user delays or cancels termination, there may be an indefinite period of time before the application fully quits. * It is up to the implementor whether or not to decide to continue showing installation progress in this case. * * @param retryTerminatingApplication This handler gives a chance for the application to re-try sending a quit event to the running application before installing the update. * The application may cancel or delay termination. This handler gives the user driver another chance to allow the user to try terminating the application again. * If the application does not delay or cancel application termination, there is no need to invoke this handler. This handler may be invoked multiple times. * Note this handler should not be invoked if @c applicationTerminated is already @c YES */ - (void)showInstallingUpdateWithApplicationTerminated:(BOOL)applicationTerminated retryTerminatingApplication:(void (^)(void))retryTerminatingApplication; /** * Show the user that the update installation finished * * Let the user know that the update finished installing. * * This will only be invoked if the updater process is still alive, which is typically not the case if * the updater's lifetime is tied to the application it is updating. This implementation must not try to reference * the old bundle prior to the installation, which will no longer be around. * * Before this point, `-showInstallingUpdateWithApplicationTerminated:retryTerminatingApplication:` will be called. * * @param relaunched Indicates if the update was relaunched. * @param acknowledgement Acknowledge to the updater that the finished installation was shown. */ - (void)showUpdateInstalledAndRelaunched:(BOOL)relaunched acknowledgement:(void (^)(void))acknowledgement NS_SWIFT_ASYNC(2); /** * Dismiss the current update installation * * Stop and tear down everything. * Dismiss all update windows, alerts, progress, etc from the user. * Basically, stop everything that could have been started. Sparkle may invoke this when aborting or finishing an update. */ - (void)dismissUpdateInstallation; @optional /** * Show the user the current presented update or its progress in utmost focus * * The user wishes to check for updates while the user is being shown update progress. * Bring whatever is on screen to frontmost focus (permission request, update information, downloading or extraction status, choice to install update, etc). * Implementing this method is optional. */ - (void)showUpdateInFocus; /* * Below are deprecated methods that have been replaced by better alternatives. * The deprecated methods will be used if the alternatives have not been implemented yet. * In the future support for using these deprecated methods may be removed however. */ // Clients should move to non-deprecated methods // Deprecated methods are only (temporarily) kept around for compatibility reasons - (void)showUpdateNotFoundWithAcknowledgement:(void (^)(void))acknowledgement __deprecated_msg("Implement -showUpdateNotFoundWithError:acknowledgement: instead"); - (void)showUpdateInstallationDidFinishWithAcknowledgement:(void (^)(void))acknowledgement __deprecated_msg("Implement -showUpdateInstalledAndRelaunched:acknowledgement: instead"); - (void)dismissUserInitiatedUpdateCheck __deprecated_msg("Transition to new UI appropriately when a new update is shown, when no update is found, or when an update error occurs."); - (void)showInstallingUpdate __deprecated_msg("Implement -showInstallingUpdateWithApplicationTerminated:retryTerminatingApplication: instead."); - (void)showSendingTerminationSignal __deprecated_msg("Implement -showInstallingUpdateWithApplicationTerminated:retryTerminatingApplication: instead."); - (void)showInstallingUpdateWithApplicationTerminated:(BOOL)applicationTerminated __deprecated_msg("Implement -showInstallingUpdateWithApplicationTerminated:retryTerminatingApplication: instead.");; @end NS_ASSUME_NONNULL_END ================================================ FILE: Sparkle/SPUUserInitiatedUpdateDriver.h ================================================ // // SPUUserInitiatedUpdateDriver.h // Sparkle // // Created by Mayur Pawashe on 3/18/16. // Copyright © 2016 Sparkle Project. All rights reserved. // #import #import "SPUUpdateDriver.h" NS_ASSUME_NONNULL_BEGIN @class SUHost; @protocol SPUUpdaterDelegate, SPUUserDriver; SPU_OBJC_DIRECT_MEMBERS @interface SPUUserInitiatedUpdateDriver : NSObject - (instancetype)initWithHost:(SUHost *)host applicationBundle:(NSBundle *)applicationBundle updater:(id)updater userDriver:(id )userDriver updaterDelegate:(nullable id )updaterDelegate; @end NS_ASSUME_NONNULL_END ================================================ FILE: Sparkle/SPUUserInitiatedUpdateDriver.m ================================================ // // SPUUserInitiatedUpdateDriver.m // Sparkle // // Created by Mayur Pawashe on 3/18/16. // Copyright © 2016 Sparkle Project. All rights reserved. // #import "SPUUserInitiatedUpdateDriver.h" #import "SPUUIBasedUpdateDriver.h" #import "SPUUserDriver.h" #include "AppKitPrevention.h" @interface SPUUserInitiatedUpdateDriver () @end @implementation SPUUserInitiatedUpdateDriver { SPUUIBasedUpdateDriver *_uiDriver; id _userDriver; void (^_updateDidShowHandler)(void); BOOL _showingUserInitiatedProgress; BOOL _showingUpdate; BOOL _aborted; } @synthesize showingUpdate = _showingUpdate; - (instancetype)initWithHost:(SUHost *)host applicationBundle:(NSBundle *)applicationBundle updater:(id)updater userDriver:(id )userDriver updaterDelegate:(nullable id )updaterDelegate { self = [super init]; if (self != nil) { _uiDriver = [[SPUUIBasedUpdateDriver alloc] initWithHost:host applicationBundle:applicationBundle updater:updater userDriver:userDriver userInitiated:YES updaterDelegate:updaterDelegate delegate:self]; _userDriver = userDriver; } return self; } - (void)setCompletionHandler:(SPUUpdateDriverCompletion)completionBlock { [_uiDriver setCompletionHandler:completionBlock]; } - (void)setUpdateShownHandler:(void (^)(void))handler { _updateDidShowHandler = [handler copy]; } - (void)setUpdateWillInstallHandler:(void (^)(void))updateWillInstallHandler { [_uiDriver setUpdateWillInstallHandler:updateWillInstallHandler]; } - (void)checkForUpdatesAtAppcastURL:(NSURL *)appcastURL withUserAgent:(NSString *)userAgent httpHeaders:(NSDictionary * _Nullable)httpHeaders { _showingUserInitiatedProgress = YES; if (_updateDidShowHandler != nil) { _updateDidShowHandler(); _updateDidShowHandler = nil; } [_userDriver showUserInitiatedUpdateCheckWithCancellation:^{ dispatch_async(dispatch_get_main_queue(), ^{ if (self->_showingUserInitiatedProgress) { [self abortUpdate]; } }); }]; [_uiDriver checkForUpdatesAtAppcastURL:appcastURL withUserAgent:userAgent httpHeaders:httpHeaders inBackground:NO]; } - (void)resumeInstallingUpdate { [_uiDriver resumeInstallingUpdate]; } - (void)resumeUpdate:(id)resumableUpdate { [_uiDriver resumeUpdate:resumableUpdate]; } - (void)uiDriverDidShowUpdate { // When a new update check has not been initiated and an update has been resumed, // update the driver to indicate we are showing an update to the user _showingUpdate = YES; if (_updateDidShowHandler != nil) { _updateDidShowHandler(); _updateDidShowHandler = nil; } } - (void)basicDriverIsRequestingAbortUpdateWithError:(nullable NSError *)error { [self abortUpdateWithError:error]; } - (void)coreDriverIsRequestingAbortUpdateWithError:(nullable NSError *)error { [self abortUpdateWithError:error]; } - (void)uiDriverIsRequestingAbortUpdateWithError:(nullable NSError *)error { [self abortUpdateWithError:error]; } - (void)basicDriverDidFinishLoadingAppcast { if (_showingUserInitiatedProgress) { _showingUserInitiatedProgress = NO; #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdeprecated-declarations" if ([_userDriver respondsToSelector:@selector(dismissUserInitiatedUpdateCheck)]) { [_userDriver dismissUserInitiatedUpdateCheck]; } #pragma clang diagnostic pop } } - (void)abortUpdate { [self abortUpdateWithError:nil]; } - (void)abortUpdateWithError:(nullable NSError *)error { if (_showingUserInitiatedProgress) { #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdeprecated-declarations" if ([_userDriver respondsToSelector:@selector(dismissUserInitiatedUpdateCheck)]) { [_userDriver dismissUserInitiatedUpdateCheck]; } #pragma clang diagnostic pop _showingUserInitiatedProgress = NO; } _aborted = YES; [_uiDriver abortUpdateWithError:error showErrorToUser:YES]; } @end ================================================ FILE: Sparkle/SPUUserUpdateState+Private.h ================================================ // // SPUUserUpdateState+Private.h // Sparkle // // Created by Mayur Pawashe on 5/9/21. // Copyright © 2021 Sparkle Project. All rights reserved. // #ifndef SPUUserUpdateState_Private_h #define SPUUserUpdateState_Private_h #import "SPUUserUpdateState.h" NS_ASSUME_NONNULL_BEGIN @interface SPUUserUpdateState (Private) - (instancetype)initWithStage:(SPUUserUpdateStage)stage userInitiated:(BOOL)userInitiated; @end NS_ASSUME_NONNULL_END #endif /* SPUUserUpdateState_Private_h */ ================================================ FILE: Sparkle/SPUUserUpdateState.h ================================================ // // SPUUserUpdateState.h // Sparkle // // Created by Mayur Pawashe on 2/29/16. // Copyright © 2016 Sparkle Project. All rights reserved. // #ifndef SPUUserUpdateState_h #define SPUUserUpdateState_h #import #if defined(BUILDING_SPARKLE_SOURCES_EXTERNALLY) // Ignore incorrect warning #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wquoted-include-in-framework-header" #import "SUExport.h" #pragma clang diagnostic pop #else #import #endif NS_ASSUME_NONNULL_BEGIN /** A choice made by the user when prompted with a new update. */ typedef NS_ENUM(NSInteger, SPUUserUpdateChoice) { /** Dismisses the update and skips being notified of it in the future. */ SPUUserUpdateChoiceSkip, /** Downloads (if needed) and installs the update. */ SPUUserUpdateChoiceInstall, /** Dismisses the update until Sparkle reminds the user of it at a later time. */ SPUUserUpdateChoiceDismiss, }; /** Describes the current stage an update is undergoing. */ typedef NS_ENUM(NSInteger, SPUUserUpdateStage) { /** The update has not been downloaded. */ SPUUserUpdateStageNotDownloaded, /** The update has already been downloaded but not begun installing. */ SPUUserUpdateStageDownloaded, /** The update has already been downloaded and began installing in the background. */ SPUUserUpdateStageInstalling }; /** This represents the user's current update state. */ SU_EXPORT NS_SWIFT_SENDABLE @interface SPUUserUpdateState : NSObject - (instancetype)init NS_UNAVAILABLE; /** The current update stage. This stage indicates if data has been already downloaded or not, or if an update is currently being installed. */ @property (nonatomic, readonly) SPUUserUpdateStage stage; /** Indicates whether or not the update check was initiated by the user. */ @property (nonatomic, readonly) BOOL userInitiated; @end NS_ASSUME_NONNULL_END #endif /* SPUUserUpdateState_h */ ================================================ FILE: Sparkle/SPUUserUpdateState.m ================================================ // // SPUUserUpdateState.m // Sparkle // // Created by Mayur Pawashe on 5/9/21. // Copyright © 2021 Sparkle Project. All rights reserved. // #import "SPUUserUpdateState.h" #import "SPUUserUpdateState+Private.h" #include "AppKitPrevention.h" #define SPUUserUpdateStateStageKey @"SPUUserUpdateStateStage" #define SPUUserUpdateStateUserInitiatedKey @"SPUUserUpdateStateUserInitiated" #define SPUUserUpdateStateMajorUpgradeKey @"SPUUserUpdateStateMajorUpgrade" #define SPUUserUpdateStateCriticalUpdateKey @"SPUUserUpdateStateCriticalUpdate" @implementation SPUUserUpdateState @synthesize stage = _stage; @synthesize userInitiated = _userInitiated; - (instancetype)initWithStage:(SPUUserUpdateStage)stage userInitiated:(BOOL)userInitiated { self = [super init]; if (self != nil) { _stage = stage; _userInitiated = userInitiated; } return self; } - (void)encodeWithCoder:(NSCoder *)encoder { [encoder encodeInteger:_stage forKey:SPUUserUpdateStateStageKey]; [encoder encodeBool:_userInitiated forKey:SPUUserUpdateStateUserInitiatedKey]; } - (instancetype)initWithCoder:(NSCoder *)decoder { SPUUserUpdateStage stage = (SPUUserUpdateStage)[decoder decodeIntegerForKey:SPUUserUpdateStateStageKey]; BOOL userInitiated = [decoder decodeBoolForKey:SPUUserUpdateStateUserInitiatedKey]; return [self initWithStage:stage userInitiated:userInitiated]; } + (BOOL)supportsSecureCoding { return YES; } @end ================================================ FILE: Sparkle/SPUVerifierInformation.h ================================================ // // SPUVerifierInformation.h // Autoupdate // // Copyright © 2023 Sparkle Project. All rights reserved. // #import NS_ASSUME_NONNULL_BEGIN #ifndef BUILDING_SPARKLE_TESTS #define SPUVerifierInformationDefinitionAttribute SPU_OBJC_DIRECT_MEMBERS #else #define SPUVerifierInformationDefinitionAttribute __attribute__((objc_runtime_name("SPUTestVerifierInformation"))) #endif SPUVerifierInformationDefinitionAttribute @interface SPUVerifierInformation : NSObject - (instancetype)initWithExpectedVersion:(NSString * _Nullable)expectedVersion expectedContentLength:(uint64_t)expectedContentLength; @property (nonatomic, readonly, copy, nullable) NSString *expectedVersion; @property (nonatomic, readonly) uint64_t expectedContentLength; @property (nonatomic, copy, nullable) NSString *actualVersion; @property (nonatomic) uint64_t actualContentLength; @end NS_ASSUME_NONNULL_END ================================================ FILE: Sparkle/SPUVerifierInformation.m ================================================ // // SPUVerifierInformation.m // Autoupdate // // Copyright © 2023 Sparkle Project. All rights reserved. // #import "SPUVerifierInformation.h" @implementation SPUVerifierInformation @synthesize expectedVersion = _expectedVersion; @synthesize expectedContentLength = _expectedContentLength; @synthesize actualVersion = _actualVersion; @synthesize actualContentLength = _actualContentLength; - (instancetype)initWithExpectedVersion:(NSString *)expectedVersion expectedContentLength:(uint64_t)expectedContentLength { self = [super init]; if (self != nil) { _expectedVersion = [expectedVersion copy]; _expectedContentLength = expectedContentLength; } return self; } @end ================================================ FILE: Sparkle/SPUXPCServiceInfo.h ================================================ // // SPUXPCServiceInfo.h // Sparkle // // Created by Mayur Pawashe on 4/17/16. // Copyright © 2016 Sparkle Project. All rights reserved. // #import NS_ASSUME_NONNULL_BEGIN BOOL SPUXPCServiceIsEnabled(NSString *enabledKey); BOOL SPUHelperHasExecutablePermission(NSString *component, NSString * _Nullable __autoreleasing * _Nullable failureReason); BOOL SPUXPCServiceHasExecutablePermission(NSString *serviceName, NSString * _Nullable __autoreleasing * _Nullable failureReason); NS_ASSUME_NONNULL_END ================================================ FILE: Sparkle/SPUXPCServiceInfo.m ================================================ // // SUXPCServiceInfo.m // Sparkle // // Created by Mayur Pawashe on 4/17/16. // Copyright © 2016 Sparkle Project. All rights reserved. // #import "SPUXPCServiceInfo.h" #import "SUErrors.h" #import "SUConstants.h" #import "SUHost.h" #include "AppKitPrevention.h" BOOL SPUXPCServiceIsEnabled(NSString *enabledKey) { NSBundle *mainBundle = [NSBundle mainBundle]; SUHost *mainBundleHost = [[SUHost alloc] initWithBundle:mainBundle]; return [mainBundleHost boolForInfoDictionaryKey:enabledKey]; } BOOL SPUHelperHasExecutablePermission(NSString *component, NSString * _Nullable __autoreleasing * _Nullable failureReason) { NSBundle *sparkleBundle = [NSBundle bundleWithIdentifier:SUBundleIdentifier]; NSURL *helperURL = [[sparkleBundle.bundleURL URLByAppendingPathComponent:component isDirectory:NO] URLByResolvingSymlinksInPath]; NSString *helperPath = helperURL.path; NSError *attributesError = nil; NSDictionary *attributes = [NSFileManager.defaultManager attributesOfItemAtPath:helperPath error:&attributesError]; if (attributes == nil) { if (failureReason != NULL) { *failureReason = [NSString stringWithFormat:@"Failed to fetch info from file '%@' -- does this helper exist? %@", helperPath, attributesError.localizedDescription]; } return NO; } NSNumber *posixPermissions = attributes[NSFilePosixPermissions]; if (posixPermissions != nil) { mode_t mode = posixPermissions.unsignedShortValue; if (((mode & S_IXUSR) == 0 || (mode & S_IXGRP) == 0 || (mode & S_IXOTH) == 0)) { if (failureReason != NULL) { *failureReason = [NSString stringWithFormat:@"The file '%@' may not have executable permissions -- were they lost during a bad file copy? Please ensure file permissions and symbolic links for Sparkle framework are preserved.", helperPath]; } return NO; } } return YES; } BOOL SPUXPCServiceHasExecutablePermission(NSString *serviceName, NSString * _Nullable __autoreleasing * _Nullable failureReason) { NSString *componentName = [NSString stringWithFormat:@"XPCServices/%@.xpc/Contents/MacOS/%@", serviceName, serviceName]; return SPUHelperHasExecutablePermission(componentName, failureReason); } ================================================ FILE: Sparkle/SUAppcast+Private.h ================================================ // // SUAppcast+Private.h // Sparkle // // Created by Mayur Pawashe on 4/30/21. // Copyright © 2021 Sparkle Project. All rights reserved. // #import #ifdef BUILDING_SPARKLE_SOURCES_EXTERNALLY // Ignore incorrect warning #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wquoted-include-in-framework-header" #import "SUAppcast.h" #import "SPUAppcastSigningValidationStatus.h" #pragma clang diagnostic pop #else #import #import #endif NS_ASSUME_NONNULL_BEGIN @class SPUAppcastItemStateResolver; @interface SUAppcast (Private) - (nullable instancetype)initWithXMLData:(NSData *)xmlData relativeToURL:(NSURL * _Nullable)relativeURL stateResolver:(SPUAppcastItemStateResolver *)stateResolver signingValidationStatus:(SPUAppcastSigningValidationStatus)signingValidationStatus error:(NSError * __autoreleasing *)error; - (SUAppcast *)copyByFilteringItems:(BOOL (^)(SUAppcastItem *))filterBlock; @end NS_ASSUME_NONNULL_END ================================================ FILE: Sparkle/SUAppcast.h ================================================ // // SUAppcast.h // Sparkle // // Created by Andy Matuschak on 3/12/06. // Copyright 2006 Andy Matuschak. All rights reserved. // #ifndef SUAPPCAST_H #define SUAPPCAST_H #import #if defined(BUILDING_SPARKLE_SOURCES_EXTERNALLY) // Ignore incorrect warning #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wquoted-include-in-framework-header" #import "SUExport.h" #import "SPUAppcastSigningValidationStatus.h" #pragma clang diagnostic pop #else #import #import #endif NS_ASSUME_NONNULL_BEGIN @class SUAppcastItem; /** The appcast representing a collection of `SUAppcastItem` items in the feed. */ SU_EXPORT NS_SWIFT_SENDABLE @interface SUAppcast : NSObject - (instancetype)init NS_UNAVAILABLE; /** The collection of update items. These `SUAppcastItem` items are in the same order as specified in the appcast XML feed and are thus not sorted by version. */ @property (readonly, nonatomic, copy) NSArray *items; /** The appcast signing validation status. Please see documentation of @c SPUAppcastSigningValidationStatus values for more information. */ @property (nonatomic, readonly) SPUAppcastSigningValidationStatus signingValidationStatus; @end NS_ASSUME_NONNULL_END #endif ================================================ FILE: Sparkle/SUAppcast.m ================================================ // // SUAppcast.m // Sparkle // // Created by Andy Matuschak on 3/12/06. // Copyright 2006 Andy Matuschak. All rights reserved. // #import "SUExport.h" #import "SUAppcast.h" #import "SUAppcast+Private.h" #import "SPUAppcastItemState.h" #import "SUAppcastItem.h" #import "SUAppcastItem+Private.h" #import "SUVersionComparisonProtocol.h" #import "SUConstants.h" #import "SULog.h" #import "SUErrors.h" #import "SULocalizations.h" #include "AppKitPrevention.h" @implementation SUAppcast @synthesize items = _items; @synthesize signingValidationStatus = _signingValidationStatus; - (nullable instancetype)initWithXMLData:(NSData *)xmlData relativeToURL:(NSURL * _Nullable)relativeURL stateResolver:(SPUAppcastItemStateResolver *)stateResolver signingValidationStatus:(SPUAppcastSigningValidationStatus)signingValidationStatus error:(NSError * __autoreleasing *)error { self = [super init]; if (self != nil) { _signingValidationStatus = signingValidationStatus; _items = [self parseAppcastItemsFromXMLData:xmlData relativeToURL:relativeURL stateResolver:stateResolver signingValidationStatus:signingValidationStatus error:error]; if (_items == nil) { return nil; } } return self; } - (NSDictionary *)attributesOfNode:(NSXMLElement *)node SPU_OBJC_DIRECT { NSEnumerator *attributeEnum = [[node attributes] objectEnumerator]; NSMutableDictionary *dictionary = [NSMutableDictionary dictionary]; for (NSXMLNode *attribute in attributeEnum) { NSString *attrName = [self sparkleNamespacedNameOfNode:attribute]; if (!attrName) { continue; } NSString *attributeStringValue = [attribute stringValue]; if (attributeStringValue != nil) { [dictionary setObject:attributeStringValue forKey:attrName]; } } return dictionary; } -(NSString *)sparkleNamespacedNameOfNode:(NSXMLNode *)node SPU_OBJC_DIRECT { // XML namespace prefix is semantically meaningless, so compare namespace URI // NS URI isn't used to fetch anything, and must match exactly, so we look for http:// not https:// if ([[node URI] isEqualToString:@"http://www.andymatuschak.org/xml-namespaces/sparkle"]) { NSString *localName = [node localName]; assert(localName); return [@"sparkle:" stringByAppendingString:localName]; } else { return [node name]; // Backwards compatibility } } -(NSArray *)parseAppcastItemsFromXMLData:(NSData *)appcastData relativeToURL:(NSURL * _Nullable)appcastURL stateResolver:(SPUAppcastItemStateResolver *)stateResolver signingValidationStatus:(SPUAppcastSigningValidationStatus)signingValidationStatus error:(NSError *__autoreleasing*)errorp SPU_OBJC_DIRECT { if (errorp) { *errorp = nil; } if (!appcastData) { return nil; } NSXMLNodeOptions options = NSXMLNodeLoadExternalEntitiesNever; // Prevent inclusion from file:// NSXMLDocument *document = [[NSXMLDocument alloc] initWithData:appcastData options:options error:errorp]; if (nil == document) { return nil; } NSArray *xmlItems = [document nodesForXPath:@"/rss/channel/item" error:errorp]; if (nil == xmlItems) { return nil; } NSMutableArray *appcastItems = [NSMutableArray array]; NSEnumerator *nodeEnum = [xmlItems objectEnumerator]; NSXMLNode *node; while((node = [nodeEnum nextObject])) { NSMutableDictionary *nodesDict = [NSMutableDictionary dictionary]; NSMutableDictionary *dict = [NSMutableDictionary dictionary]; // First, we'll "index" all the first-level children of this appcast item so we can pick them out by language later. if ([[node children] count]) { node = [node childAtIndex:0]; while (nil != node) { NSString *name = [self sparkleNamespacedNameOfNode:node]; if (name) { NSMutableArray *nodes = [nodesDict objectForKey:name]; if (nodes == nil) { nodes = [NSMutableArray array]; [nodesDict setObject:nodes forKey:name]; } [nodes addObject:node]; } node = [node nextSibling]; } } for (NSString *name in nodesDict) { node = [self bestNodeInNodes:[nodesDict objectForKey:name] name:name]; if ([name isEqualToString:SURSSElementEnclosure] || [name isEqualToString:SUAppcastElementCriticalUpdate]) { // These are flattened as a separate dictionary for some reason NSDictionary *innerDict = [self attributesOfNode:(NSXMLElement *)node]; [dict setObject:innerDict forKey:name]; } else if ([name isEqualToString:SURSSElementPubDate]) { // We don't want to parse and create a NSDate instance - // that's a risk we can avoid. We don't use the date anywhere other // than it being accessible from SUAppcastItem NSString *dateString = node.stringValue; if (dateString) { [dict setObject:dateString forKey:name]; } } else if ([name isEqualToString:SURSSElementDescription]) { NSString *description = node.stringValue; if (description != nil) { NSDictionary *attributes = [self attributesOfNode:(NSXMLElement *)node]; NSString *descriptionFormat = attributes[SUAppcastAttributeFormat]; NSMutableDictionary *descriptionDict = [NSMutableDictionary dictionary]; [descriptionDict setObject:description forKey:@"content"]; if (descriptionFormat != nil) { [descriptionDict setObject:descriptionFormat forKey:@"format"]; } [dict setObject:descriptionDict forKey:SURSSElementDescription]; } } else if ([name isEqualToString:SUAppcastElementReleaseNotesLink]) { NSString *releaseNotesLink = node.stringValue; if (releaseNotesLink != nil) { NSDictionary *attributes = [self attributesOfNode:(NSXMLElement *)node]; NSMutableDictionary *linkDict = [NSMutableDictionary dictionary]; [linkDict setObject:releaseNotesLink forKey:@"content"]; NSString *edSignature = attributes[SUAppcastAttributeEDSignature]; if (edSignature != nil) { [linkDict setObject:edSignature forKey:SUAppcastAttributeEDSignature]; } NSString *length = attributes[SUAppcastAttributeLength]; if (length != nil) { [linkDict setObject:length forKey:SUAppcastAttributeLength]; } [dict setObject:linkDict forKey:SUAppcastElementReleaseNotesLink]; } } else if ([name isEqualToString:SUAppcastElementDeltas]) { NSMutableArray *deltas = [NSMutableArray array]; NSEnumerator *childEnum = [[node children] objectEnumerator]; for (NSXMLNode *child in childEnum) { if ([[child name] isEqualToString:SURSSElementEnclosure]) { [deltas addObject:[self attributesOfNode:(NSXMLElement *)child]]; } } [dict setObject:deltas forKey:name]; } else if ([name isEqualToString:SUAppcastElementTags]) { NSMutableArray *names = [NSMutableArray array]; NSEnumerator *childEnum = [[node children] objectEnumerator]; for (NSXMLNode *child in childEnum) { NSString *childName = child.name; if (childName) { [names addObject:childName]; } } [dict setObject:names forKey:name]; } else if ([name isEqualToString:SUAppcastElementInformationalUpdate]) { NSMutableSet *informationalUpdateVersions = [NSMutableSet set]; NSEnumerator *childEnum = [[node children] objectEnumerator]; for (NSXMLNode *child in childEnum) { if ([child.name isEqualToString:SUAppcastElementVersion]) { NSString *version = child.stringValue; if (version != nil) { [informationalUpdateVersions addObject:version]; } } else if ([child.name isEqualToString:SUAppcastElementBelowVersion]) { NSString *version = child.stringValue; if (version != nil) { // Denote version is used as an upper bound by using '<' [informationalUpdateVersions addObject:[NSString stringWithFormat:@"<%@", version]]; } } } [dict setObject:[informationalUpdateVersions copy] forKey:name]; } else if (name != nil) { // add all other values as strings NSString *theValue = [[node stringValue] stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]]; if (theValue != nil) { [dict setObject:theValue forKey:name]; } } } NSString *errString; SUAppcastItem *anItem = [[SUAppcastItem alloc] initWithDictionary:dict relativeToURL:appcastURL stateResolver:stateResolver signingValidationStatus:signingValidationStatus failureReason:&errString]; if (anItem != nil) { [appcastItems addObject:anItem]; } else { // An info-only update item could fail to be created when signing validation fails // Let's just resume creating the other appcast items in that case. if (signingValidationStatus != SPUAppcastSigningValidationStatusFailed) { SULog(SULogLevelError, @"Sparkle Updater: Failed to parse appcast item: %@.\nAppcast dictionary was: %@", errString, dict); if (errorp) *errorp = [NSError errorWithDomain:SUSparkleErrorDomain code:SUAppcastParseError userInfo:@{NSLocalizedDescriptionKey: errString}]; return nil; } } } return appcastItems; } - (NSXMLNode *)bestNodeInNodes:(NSArray *)nodes name:(NSString *)name SPU_OBJC_DIRECT { // We use this method to pick out the localized version of a node when one's available. if ([nodes count] == 1) return [nodes objectAtIndex:0]; else if ([nodes count] == 0) return nil; // Now that we reached here, we are dealing with multiple nodes NSMutableArray *languages = [NSMutableArray array]; for (NSXMLElement *node in nodes) { NSString *nodeLanguage = [[node attributeForName:SUXMLLanguage] stringValue]; NSString *language; if (nodeLanguage.length == 0) { language = @"en"; SULog(SULogLevelError, @"Error: Multiple nodes for %@ element are present and one of them does not have %@ attribute specified. Defaulting to %@=\"en\" but not all versions of Sparkle handle an implicit set language. Please specify the %@ attribute explicitly for all %@ elements.", name, SUXMLLanguage, SUXMLLanguage, SUXMLLanguage, name); } else { language = nodeLanguage; } [languages addObject:language]; } NSString *preferredLanguage = [[NSBundle preferredLocalizationsFromArray:languages] objectAtIndex:0]; if (preferredLanguage == nil) { SULog(SULogLevelError, @"Error: Failed to obtain preferred localizations from %@ for node %@.", languages, name); return [nodes objectAtIndex:0]; } NSUInteger preferredLanguageIndex = [languages indexOfObject:preferredLanguage]; if (preferredLanguageIndex == NSNotFound) { SULog(SULogLevelError, @"Error: Failed to find preferred language index for %@ for node %@.", preferredLanguage, name); return [nodes objectAtIndex:0]; } return [nodes objectAtIndex:preferredLanguageIndex]; } - (SUAppcast *)copyByFilteringItems:(BOOL (^)(SUAppcastItem *))filterBlock { SUAppcast *other = [SUAppcast new]; NSMutableArray *newItems = [NSMutableArray new]; for (SUAppcastItem *item in _items) { if (filterBlock(item)) { [newItems addObject:item]; } } other->_items = newItems; other->_signingValidationStatus = _signingValidationStatus; return other; } @end ================================================ FILE: Sparkle/SUAppcastDriver.h ================================================ // // SUAppcastDriver.h // Sparkle // // Created by Mayur Pawashe on 3/17/16. // Copyright © 2016 Sparkle Project. All rights reserved. // #import NS_ASSUME_NONNULL_BEGIN @class SUAppcastItem, SUHost, SUAppcast; @protocol SPUUpdaterDelegate; @protocol SUAppcastDriverDelegate - (void)didFailToFetchAppcastWithError:(NSError *)error; - (void)didFinishLoadingAppcast:(SUAppcast *)appcast; - (void)didFindValidUpdateWithAppcastItem:(SUAppcastItem *)appcastItem secondaryAppcastItem:(SUAppcastItem * _Nullable)secondaryAppcastItem; - (void)didNotFindUpdateWithLatestAppcastItem:(nullable SUAppcastItem *)latestAppcastItem hostToLatestAppcastItemComparisonResult:(NSComparisonResult)hostToLatestAppcastItemComparisonResult background:(BOOL)background; @end #ifndef BUILDING_SPARKLE_TESTS #define SUAppcastDriverDefinitionAttribute SPU_OBJC_DIRECT_MEMBERS #else #define SUAppcastDriverDefinitionAttribute __attribute__((objc_runtime_name("SUTestAppcastDriver"))) #endif SUAppcastDriverDefinitionAttribute @interface SUAppcastDriver : NSObject - (instancetype)initWithHost:(SUHost *)host updater:(id)updater updaterDelegate:(nullable id )updaterDelegate delegate:(nullable id )delegate; - (void)loadAppcastFromURL:(NSURL *)appcastURL userAgent:(NSString *)userAgent httpHeaders:(NSDictionary * _Nullable)httpHeaders inBackground:(BOOL)background; - (void)cleanup:(void (^)(void))completionHandler; @end NS_ASSUME_NONNULL_END ================================================ FILE: Sparkle/SUAppcastDriver.m ================================================ // // SUAppcastDriver.m // Sparkle // // Created by Mayur Pawashe on 3/17/16. // Copyright © 2016 Sparkle Project. All rights reserved. // #import "SUAppcastDriver.h" #import "SUAppcast.h" #import "SUAppcast+Private.h" #import "SUAppcastItem.h" #import "SUAppcastItem+Private.h" #import "SUVersionComparisonProtocol.h" #import "SUStandardVersionComparator.h" #import "SPUUpdaterDelegate.h" #import "SUHost.h" #import "SPUSkippedUpdate.h" #import "SUConstants.h" #import "SUPhasedUpdateGroupInfo.h" #import "SULog.h" #import "SPUDownloadDriver.h" #import "SPUDownloadData.h" #import "SULocalizations.h" #import "SUErrors.h" #import "SPUAppcastItemStateResolver.h" #import "SPUAppcastItemStateResolver+Private.h" #import "SPUAppcastItemState.h" #import "SUSignatureVerifier.h" #import "SPUExtractSignedFeed.h" #import "SUSignatures.h" #import "SPUVerifierInformation.h" #include "AppKitPrevention.h" #define SUInitialFailedFeedSigningValidationDateKey @"SUInitialFailedFeedSigningValidationDate" #define DEFAULT_APPCAST_FAILURE_EXPIRATION_INTERVAL 1728000 // 20 days @interface SUAppcastDriver () @end @implementation SUAppcastDriver { SUHost *_host; SPUDownloadDriver *_downloadDriver; __weak id _updater; __weak id _updaterDelegate; __weak id _delegate; } - (instancetype)initWithHost:(SUHost *)host updater:(id)updater updaterDelegate:(id )updaterDelegate delegate:(id )delegate { self = [super init]; if (self != nil) { _host = host; _updater = updater; _updaterDelegate = updaterDelegate; _delegate = delegate; } return self; } - (void)loadAppcastFromURL:(NSURL *)appcastURL userAgent:(NSString *)userAgent httpHeaders:(NSDictionary * _Nullable)httpHeaders inBackground:(BOOL)background { assert(NSThread.isMainThread); NSMutableDictionary *requestHTTPHeaders = [NSMutableDictionary dictionary]; if (httpHeaders != nil) { [requestHTTPHeaders addEntriesFromDictionary:(NSDictionary * _Nonnull)httpHeaders]; } requestHTTPHeaders[@"Accept"] = @"application/rss+xml,*/*;q=0.1"; _downloadDriver = [[SPUDownloadDriver alloc] initWithRequestURL:appcastURL host:_host userAgent:userAgent httpHeaders:requestHTTPHeaders inBackground:background delegate:self]; [_downloadDriver downloadFile]; } - (void)downloadDriverDidDownloadData:(SPUDownloadData *)downloadData { SPUAppcastItemStateResolver *stateResolver = [[SPUAppcastItemStateResolver alloc] initWithHostVersion:_host.version applicationVersionComparator:[self versionComparator] standardVersionComparator:[SUStandardVersionComparator defaultComparator]]; NSData *downloadedAppcastData = downloadData.data; id delegate = _delegate; NSDate *currentDate = [NSDate date]; // Verify feed if appcast signing is required SPUAppcastSigningValidationStatus appcastSigningValidationStatus; NSData *verifiedAppcastData; NSError *verifyAppcastDataFailureError = nil; if (_host.requiresSignedAppcast) { // Extract appcast content without signing information and prepare verification information SUSignatureVerifier *signatureVerifier = [[SUSignatureVerifier alloc] initWithPublicKeys:_host.publicKeys]; NSString *edSignatureBase64 = nil; uint64_t contentLength = 0; verifiedAppcastData = SPUExtractAppcastContent(downloadedAppcastData, &edSignatureBase64, &contentLength); SUSignatures *signatures = [[SUSignatures alloc] initWithEd:edSignatureBase64 #if SPARKLE_BUILD_LEGACY_DSA_SUPPORT dsa:nil #endif ]; SPUVerifierInformation *verifierInformation = [[SPUVerifierInformation alloc] initWithExpectedVersion:nil expectedContentLength:contentLength]; verifierInformation.actualContentLength = verifiedAppcastData.length; // If signature verification fails, we will record the initial date it failed // If a significant period of time passes with verification still failing, we may operate in a 'safe' mode as fallback // and allow an app to be updated through key rotation // If main bundle and host bundle differ, use the main bundle (updater) with a host bundle identifier to record the validation failure date // This ensures external updaters record this date separately from an app updating itself. SUHost *hostForFailedSigningValidationDate; NSString *initialFailedFeedSigningValidationDateKey; NSBundle *mainBundle = NSBundle.mainBundle; if ([mainBundle isEqual:_host.bundle]) { hostForFailedSigningValidationDate = _host; initialFailedFeedSigningValidationDateKey = SUInitialFailedFeedSigningValidationDateKey; } else { hostForFailedSigningValidationDate = [[SUHost alloc] initWithBundle:mainBundle]; NSString *hostBundleIdentifier = _host.bundle.bundleIdentifier; initialFailedFeedSigningValidationDateKey = (hostBundleIdentifier != nil) ? [SUInitialFailedFeedSigningValidationDateKey"_" stringByAppendingString:hostBundleIdentifier] : nil; } NSError *verifyAppcastDataInnerError = nil; if (![signatureVerifier verifyData:verifiedAppcastData signatures:signatures fileKind:@"appcast" verifierInformation:verifierInformation error:&verifyAppcastDataInnerError]) { // Feed validation failed. Proceed to check if we can operate in safe mode if feed failure expiration interval has passed. NSNumber *failureExpirationIntervalSetting = [_host doubleNumberForInfoDictionaryKey:SUSignedFeedFailureExpirationIntervalKey]; NSTimeInterval failureExpirationInterval = (failureExpirationIntervalSetting == nil) ? DEFAULT_APPCAST_FAILURE_EXPIRATION_INTERVAL : failureExpirationIntervalSetting.doubleValue; BOOL canRecoverFromSigningValidationFailure; if (initialFailedFeedSigningValidationDateKey == nil || fpclassify(failureExpirationInterval) == FP_ZERO) { canRecoverFromSigningValidationFailure = NO; } else { NSDate *firstFailedSigningValidationDate = [hostForFailedSigningValidationDate objectForUserDefaultsKey:initialFailedFeedSigningValidationDateKey ofClass:NSDate.class]; if (firstFailedSigningValidationDate == nil) { // Record first signing validation date [hostForFailedSigningValidationDate setObject:currentDate forUserDefaultsKey:initialFailedFeedSigningValidationDateKey]; canRecoverFromSigningValidationFailure = NO; } else { canRecoverFromSigningValidationFailure = ([currentDate timeIntervalSinceDate:firstFailedSigningValidationDate] >= failureExpirationInterval); } } NSMutableDictionary *userInfo = [NSMutableDictionary dictionaryWithObject:SULocalizedStringFromTableInBundle(@"The update feed is improperly signed and could not be validated. Please try again later or contact the app developer.", SPARKLE_TABLE, SUSparkleBundle(), nil) forKey:NSLocalizedDescriptionKey]; if (verifyAppcastDataInnerError != nil) { [userInfo setObject:verifyAppcastDataInnerError forKey:NSUnderlyingErrorKey]; } verifyAppcastDataFailureError = [NSError errorWithDomain:SUSparkleErrorDomain code:SUAppcastParseError userInfo:userInfo]; // If we can recover from signing validation failure for now, still hold onto failureVerifyAppcastDataError // because we will use it later on if there's no actual new update to report if (!canRecoverFromSigningValidationFailure) { [delegate didFailToFetchAppcastWithError:verifyAppcastDataFailureError]; return; } appcastSigningValidationStatus = SPUAppcastSigningValidationStatusFailed; } else { // Feed validation passes if (initialFailedFeedSigningValidationDateKey != nil) { [hostForFailedSigningValidationDate setObject:nil forUserDefaultsKey:initialFailedFeedSigningValidationDateKey]; } appcastSigningValidationStatus = SPUAppcastSigningValidationStatusSucceeded; } } else { verifiedAppcastData = downloadedAppcastData; appcastSigningValidationStatus = SPUAppcastSigningValidationStatusSkipped; } NSError *appcastError = nil; SUAppcast *loadedAppcast = [[SUAppcast alloc] initWithXMLData:verifiedAppcastData relativeToURL:downloadData.URL stateResolver:stateResolver signingValidationStatus:appcastSigningValidationStatus error:&appcastError]; if (loadedAppcast == nil) { NSMutableDictionary *userInfo = [NSMutableDictionary dictionaryWithObject:SULocalizedStringFromTableInBundle(@"An error occurred while parsing the update feed.", SPARKLE_TABLE, SUSparkleBundle(), nil) forKey:NSLocalizedDescriptionKey]; if (appcastError != nil) { [userInfo setObject:appcastError forKey:NSUnderlyingErrorKey]; } [delegate didFailToFetchAppcastWithError:[NSError errorWithDomain:SUSparkleErrorDomain code:SUAppcastParseError userInfo:userInfo]]; return; } // Now process the loaded appcast and find a suitable update item [delegate didFinishLoadingAppcast:loadedAppcast]; id updater = _updater; if (updater != nil) { NSDictionary *userInfo = @{ SUUpdaterAppcastNotificationKey: loadedAppcast }; [[NSNotificationCenter defaultCenter] postNotificationName:SUUpdaterDidFinishLoadingAppCastNotification object:updater userInfo:userInfo]; } NSSet *allowedChannels; id updaterDelegate = _updaterDelegate; if (updater != nil && [updaterDelegate respondsToSelector:@selector(allowedChannelsForUpdater:)]) { allowedChannels = [updaterDelegate allowedChannelsForUpdater:updater]; if (allowedChannels == nil) { SULog(SULogLevelError, @"Error: -allowedChannelsForUpdater: cannot return nil. Treating this as an empty set."); allowedChannels = [NSSet set]; } } else { allowedChannels = [NSSet set]; } SUAppcast *macOSAppcast = [SUAppcastDriver filterAppcast:loadedAppcast forMacOSAndAllowedChannels:allowedChannels]; id applicationVersionComparator = [self versionComparator]; BOOL background = _downloadDriver.inBackground; NSNumber *phasedUpdateGroup = background ? @([SUPhasedUpdateGroupInfo updateGroupForHost:_host]) : nil; SPUSkippedUpdate *skippedUpdate = background ? [SPUSkippedUpdate skippedUpdateForHost:_host] : nil; // First filter out min/max OS version and see if there's an update that passes // the minimum autoupdate version. We filter out updates that fail the minimum // autoupdate version test because we have a preference over minor updates that can be // downloaded and installed with less disturbance SUAppcast *passesMinimumAutoupdateAppcast = [SUAppcastDriver filterSupportedAppcast:macOSAppcast phasedUpdateGroup:phasedUpdateGroup skippedUpdate:skippedUpdate currentDate:currentDate hostVersion:_host.version versionComparator:applicationVersionComparator testMinimumSystemRequirements:YES testMinimumAutoupdateVersion:YES]; SUAppcastItem *secondaryItemPassesMinimumAutoupdate = nil; SUAppcastItem *primaryItemPassesMinimumAutoupdate = [self retrieveBestAppcastItemFromAppcast:passesMinimumAutoupdateAppcast versionComparator:applicationVersionComparator secondaryUpdate:&secondaryItemPassesMinimumAutoupdate]; // If we weren't able to find a valid update, try to find an update that // doesn't pass the minimum autoupdate version SUAppcastItem *finalPrimaryItem; SUAppcastItem *finalSecondaryItem = nil; if (![self isItemNewer:primaryItemPassesMinimumAutoupdate]) { SUAppcast *failsMinimumAutoupdateAppcast = [SUAppcastDriver filterSupportedAppcast:macOSAppcast phasedUpdateGroup:phasedUpdateGroup skippedUpdate:skippedUpdate currentDate:currentDate hostVersion:_host.version versionComparator:applicationVersionComparator testMinimumSystemRequirements:YES testMinimumAutoupdateVersion:NO]; finalPrimaryItem = [self retrieveBestAppcastItemFromAppcast:failsMinimumAutoupdateAppcast versionComparator:applicationVersionComparator secondaryUpdate:&finalSecondaryItem]; } else { finalPrimaryItem = primaryItemPassesMinimumAutoupdate; finalSecondaryItem = secondaryItemPassesMinimumAutoupdate; } // Check if we found a new suitable update if ([self isItemNewer:finalPrimaryItem]) { [delegate didFindValidUpdateWithAppcastItem:finalPrimaryItem secondaryAppcastItem:finalSecondaryItem]; return; } // If we're trying to recover from a failed signing validation of the feed, show an error rather than // showing why there isn't a new update, which we should be cautious about if (appcastSigningValidationStatus == SPUAppcastSigningValidationStatusFailed && verifyAppcastDataFailureError != nil) { [delegate didFailToFetchAppcastWithError:verifyAppcastDataFailureError]; return; } // Find the latest appcast item that we can report to the user and updater delegates // This may include updates that fail due to OS version requirements. // This excludes newer backgrounded updates that fail because they are skipped or not in current phased rollout group SUAppcast *notFoundAppcast = [SUAppcastDriver filterSupportedAppcast:macOSAppcast phasedUpdateGroup:phasedUpdateGroup skippedUpdate:skippedUpdate currentDate:currentDate hostVersion:_host.version versionComparator:applicationVersionComparator testMinimumSystemRequirements:NO testMinimumAutoupdateVersion:NO]; SUAppcastItem *notFoundPrimaryItem = [self retrieveBestAppcastItemFromAppcast:notFoundAppcast versionComparator:applicationVersionComparator secondaryUpdate:nil]; NSComparisonResult hostToLatestAppcastItemComparisonResult; if (notFoundPrimaryItem != nil) { hostToLatestAppcastItemComparisonResult = [applicationVersionComparator compareVersion:_host.version toVersion:notFoundPrimaryItem.versionString]; } else { hostToLatestAppcastItemComparisonResult = NSOrderedSame; } [delegate didNotFindUpdateWithLatestAppcastItem:notFoundPrimaryItem hostToLatestAppcastItemComparisonResult:hostToLatestAppcastItemComparisonResult background:background]; } - (void)downloadDriverDidFailToDownloadFileWithError:(nonnull NSError *)error { SULog(SULogLevelError, @"Encountered download feed error: %@", error); NSMutableDictionary *userInfo = [NSMutableDictionary dictionaryWithDictionary:@{NSLocalizedDescriptionKey:SULocalizedStringFromTableInBundle(@"An error occurred in retrieving update information. Please try again later.", SPARKLE_TABLE, SUSparkleBundle(), nil)}]; if (error != nil) { [userInfo setObject:error forKey:NSUnderlyingErrorKey]; } [_delegate didFailToFetchAppcastWithError:[NSError errorWithDomain:SUSparkleErrorDomain code:SUDownloadError userInfo:userInfo]]; } - (SUAppcastItem * _Nullable)preferredUpdateForRegularAppcastItem:(SUAppcastItem * _Nullable)regularItem secondaryUpdate:(SUAppcastItem * __autoreleasing _Nullable *)secondaryUpdate SPU_OBJC_DIRECT { SUAppcastItem *deltaItem = (regularItem != nil) ? [[self class] deltaUpdateFromAppcastItem:regularItem hostVersion:_host.version] : nil; BOOL supportsDeltaItem; if (deltaItem == nil) { supportsDeltaItem = NO; } else { // Delta updates are not supported when bundles are transferred over to some file systems like fat32 and exfat systems // This is because they do not preserve permissions completely, which we require for diff'ing. // We shouldn't download delta updates in cases where we can detect they aren't supported // More accurately we will detect if the host bundle's permission bits have been 'tainted' // which is more reliable than checking the underlying file system. // To do this, we will check the Sparkle executable's permission bits, which is produced from us // We will also check if the executable file or the localization files have been stripped from Sparkle.framework NSFileManager *fileManager = NSFileManager.defaultManager; NSBundle *hostBundle = _host.bundle; NSString *sparkleExecutablePath; NSString *sparkleResourcesPath; if ([hostBundle isEqual:NSBundle.mainBundle]) { NSBundle *sparkleBundle = [NSBundle bundleForClass:[self class]]; sparkleExecutablePath = sparkleBundle.executableURL.URLByResolvingSymlinksInPath.path; sparkleResourcesPath = sparkleBundle.resourcePath; } else { // If we are not updating ourselves, make a good guess to the Sparkle executable location NSURL *frameworksURL = hostBundle.privateFrameworksURL; NSURL *sparkleSymlinkURL = [frameworksURL URLByAppendingPathComponent:@"Sparkle.framework/Sparkle"]; NSString *candidateExecutablePath = sparkleSymlinkURL.URLByResolvingSymlinksInPath.path; if ([fileManager fileExistsAtPath:candidateExecutablePath]) { sparkleExecutablePath = candidateExecutablePath; sparkleResourcesPath = [candidateExecutablePath.stringByDeletingLastPathComponent stringByAppendingPathComponent:@"Resources"]; } else { sparkleExecutablePath = nil; sparkleResourcesPath = nil; } } if (sparkleExecutablePath != nil) { NSError *attributesError = nil; NSDictionary *attributes = [fileManager attributesOfItemAtPath:sparkleExecutablePath error:&attributesError]; BOOL sparkleExecutableIsOK; if (attributes != nil) { // Skip delta updates if permissions are not 0755 NSNumber *posixPermissions = attributes[NSFilePosixPermissions]; if (posixPermissions != nil && posixPermissions.shortValue != 0755) { sparkleExecutableIsOK = NO; // Irregular permissions on the Sparkle executable could mean the app was transferred on a file system we don't support // (which doesn't track all permissions) SULog(SULogLevelDefault, @"Encountered irregular POSIX permissions 0%o for Sparkle executable, which is not 0755. Skipping delta updates..", posixPermissions.shortValue); } else { // Test if Sparkle's executable file on disk has expected file size for applying this delta update // If the file size has been reduced, the user could have stripped an architecture out if (deltaItem.deltaFromSparkleExecutableSize != nil) { NSNumber *fileSize = attributes[NSFileSize]; if (fileSize != nil && ![deltaItem.deltaFromSparkleExecutableSize isEqualToNumber:fileSize]) { sparkleExecutableIsOK = NO; SULog(SULogLevelDefault, @"Expected file size (%llu) of Sparkle's executable does not match actual file size (%llu). Skipping delta update.", deltaItem.deltaFromSparkleExecutableSize.unsignedLongLongValue, fileSize.unsignedLongLongValue); } else { sparkleExecutableIsOK = YES; } } else { sparkleExecutableIsOK = YES; } } } else { sparkleExecutableIsOK = YES; SULog(SULogLevelError, @"Error: Failed to retrieve attributes from Sparkle executable: %@", attributesError.localizedDescription); } // Test if Sparkle's expected localization files on disk are still present for applying this delta update // If there are missing localization files, the user could have stripped them out // No need to test this though if !sparkleExecutableIsOK BOOL sparkleResourcesAreOK; if (sparkleExecutableIsOK && sparkleResourcesPath != nil && deltaItem.deltaFromSparkleLocales != nil) { BOOL foundAllExpectedLocales = YES; for (NSString *locale in deltaItem.deltaFromSparkleLocales) { NSString *localeProjectPath = [[sparkleResourcesPath stringByAppendingPathComponent:locale] stringByAppendingPathExtension:@"lproj"]; if (![fileManager fileExistsAtPath:localeProjectPath]) { foundAllExpectedLocales = NO; SULog(SULogLevelDefault, @"Expected project locale (%@) is missing in Sparkle.framework. Skipping delta update.", locale); break; } } sparkleResourcesAreOK = foundAllExpectedLocales; } else { sparkleResourcesAreOK = YES; } supportsDeltaItem = sparkleExecutableIsOK && sparkleResourcesAreOK; } else { supportsDeltaItem = YES; SULog(SULogLevelError, @"Error: Failed to unexpectably retrieve Sparkle executable URL from %@", hostBundle.bundlePath); } } if (supportsDeltaItem) { if (secondaryUpdate != NULL) { *secondaryUpdate = regularItem; } return deltaItem; } else { if (secondaryUpdate != NULL) { *secondaryUpdate = nil; } return regularItem; } } - (SUAppcastItem *)retrieveBestAppcastItemFromAppcast:(SUAppcast *)appcast versionComparator:(id)versionComparator secondaryUpdate:(SUAppcastItem * __autoreleasing _Nullable *)secondaryAppcastItem SPU_OBJC_DIRECT { // Find the best valid update in the appcast by asking the delegate // Don't ask the delegate if the appcast has no items though id updaterDelegate = _updaterDelegate; id updater = _updater; SUAppcastItem *regularItemFromDelegate; BOOL delegateOptedOutOfSelection; if (appcast.items.count > 0 && updater != nil && [updaterDelegate respondsToSelector:@selector((bestValidUpdateInAppcast:forUpdater:))]) { SUAppcastItem *candidateItem = [updaterDelegate bestValidUpdateInAppcast:appcast forUpdater:updater]; if (candidateItem == SUAppcastItem.emptyAppcastItem) { regularItemFromDelegate = nil; delegateOptedOutOfSelection = YES; } else if (candidateItem == nil) { regularItemFromDelegate = nil; delegateOptedOutOfSelection = NO; } else { if (candidateItem.deltaUpdate) { // Client would have to go out of their way to examine the .deltaUpdates to return one // We need them to give us a regular update item back instead.. SULog(SULogLevelError, @"Error: -bestValidUpdateInAppcast:forUpdater: cannot return a delta update item"); regularItemFromDelegate = nil; } else { regularItemFromDelegate = candidateItem; } delegateOptedOutOfSelection = NO; } } else { regularItemFromDelegate = nil; delegateOptedOutOfSelection = NO; } // Take care of finding best appcast item ourselves if delegate does not SUAppcastItem *regularItem; if (regularItemFromDelegate == nil && !delegateOptedOutOfSelection) { regularItem = [SUAppcastDriver bestItemFromAppcastItems:appcast.items comparator:versionComparator]; } else { regularItem = regularItemFromDelegate; } // Retrieve the preferred primary and secondary update items // In the case of a delta update, the preferred primary item will be the delta update, // and the secondary item will be the regular update. return [self preferredUpdateForRegularAppcastItem:regularItem secondaryUpdate:secondaryAppcastItem]; } // Note: This method is used by unit tests + (SUAppcast *)filterAppcast:(SUAppcast *)appcast forMacOSAndAllowedChannels:(NSSet *)allowedChannels #ifndef BUILDING_SPARKLE_TESTS SPU_OBJC_DIRECT #endif { return [appcast copyByFilteringItems:^(SUAppcastItem *item) { // We will never care about other OS's BOOL macOSUpdate = [item isMacOsUpdate]; if (!macOSUpdate) { return NO; } // Delta updates cannot be top-level entries BOOL isDeltaUpdate = [item isDeltaUpdate]; if (isDeltaUpdate) { return NO; } // We should never be interested in the update if it doesn't pass the minimum update version BOOL passesUpdateVersion = item.minimumUpdateVersionIsOK; if (!passesUpdateVersion) { return NO; } NSString *channel = item.channel; if (channel == nil) { // Item is on the default channel return YES; } return [allowedChannels containsObject:channel]; }]; } // Note: This method is used by unit tests + (SUAppcast *)filterSupportedAppcast:(SUAppcast *)appcast phasedUpdateGroup:(NSNumber * _Nullable)phasedUpdateGroup skippedUpdate:(SPUSkippedUpdate * _Nullable)skippedUpdate currentDate:(NSDate *)currentDate hostVersion:(NSString *)hostVersion versionComparator:(id)versionComparator testMinimumSystemRequirements:(BOOL)testMinimumSystemRequirements testMinimumAutoupdateVersion:(BOOL)testMinimumAutoupdateVersion #ifndef BUILDING_SPARKLE_TESTS SPU_OBJC_DIRECT #endif { BOOL hostPassesSkippedMajorVersion = [SPUAppcastItemStateResolver isMinimumAutoupdateVersionOK:skippedUpdate.majorVersion hostVersion:hostVersion versionComparator:versionComparator]; return [appcast copyByFilteringItems:^(SUAppcastItem *item) { BOOL passesOSVersion = (!testMinimumSystemRequirements || (item.minimumOperatingSystemVersionIsOK && item.maximumOperatingSystemVersionIsOK)); BOOL passesHardwareRequirements = (!testMinimumSystemRequirements || item.arm64HardwareRequirementIsOK); BOOL passesPhasedRollout = [self itemIsReadyForPhasedRollout:item phasedUpdateGroup:phasedUpdateGroup currentDate:currentDate hostVersion:hostVersion versionComparator:versionComparator]; BOOL passesMinimumAutoupdateVersion = (!testMinimumAutoupdateVersion || !item.majorUpgrade); BOOL passesSkippedUpdates = (versionComparator == nil || hostVersion == nil || ![self item:item containsSkippedUpdate:skippedUpdate hostPassesSkippedMajorVersion:hostPassesSkippedMajorVersion versionComparator:versionComparator]); return (BOOL)(passesOSVersion && passesHardwareRequirements && passesPhasedRollout && passesMinimumAutoupdateVersion && passesSkippedUpdates); }]; } + (SUAppcastItem * _Nullable)deltaUpdateFromAppcastItem:(SUAppcastItem *)appcastItem hostVersion:(NSString *)hostVersion { return appcastItem.deltaUpdates[hostVersion]; } + (SUAppcastItem * _Nullable)bestItemFromAppcastItems:(NSArray *)appcastItems comparator:(id)comparator { SUAppcastItem *item = nil; for(SUAppcastItem *candidate in appcastItems) { // Note if two items are equal, we must select the first matching one if (!item || [comparator compareVersion:item.versionString toVersion:candidate.versionString] == NSOrderedAscending) { item = candidate; } } return item; } // Note: this method is used by unit tests // This method should not do *any* filtering, only version comparing + (SUAppcastItem *)bestItemFromAppcastItems:(NSArray *)appcastItems getDeltaItem:(SUAppcastItem * __autoreleasing *)deltaItem withHostVersion:(NSString *)hostVersion comparator:(id)comparator #ifndef BUILDING_SPARKLE_TESTS SPU_OBJC_DIRECT #endif { SUAppcastItem *item = [self bestItemFromAppcastItems:appcastItems comparator:comparator]; if (item != nil && deltaItem != NULL) { *deltaItem = [self deltaUpdateFromAppcastItem:item hostVersion:hostVersion]; } return item; } - (id)versionComparator SPU_OBJC_DIRECT { id comparator = nil; #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdeprecated-declarations" // Give the delegate a chance to provide a custom version comparator id updaterDelegate = _updaterDelegate; if ([updaterDelegate respondsToSelector:@selector((versionComparatorForUpdater:))]) { id updater = _updater; if (updater != nil) { comparator = [updaterDelegate versionComparatorForUpdater:updater]; } } #pragma clang diagnostic pop // If we don't get a comparator from the delegate, use the default comparator if (comparator == nil) { comparator = [SUStandardVersionComparator defaultComparator]; } return comparator; } - (BOOL)isItemNewer:(SUAppcastItem *)ui SPU_OBJC_DIRECT { return ui != nil && [[self versionComparator] compareVersion:_host.version toVersion:ui.versionString] == NSOrderedAscending; } + (BOOL)item:(SUAppcastItem *)ui containsSkippedUpdate:(SPUSkippedUpdate * _Nullable)skippedUpdate hostPassesSkippedMajorVersion:(BOOL)hostPassesSkippedMajorVersion versionComparator:(id)versionComparator SPU_OBJC_DIRECT { NSString *skippedMajorVersion = skippedUpdate.majorVersion; NSString *skippedMajorSubreleaseVersion = skippedUpdate.majorSubreleaseVersion; if (!hostPassesSkippedMajorVersion && skippedMajorVersion != nil && ui.minimumAutoupdateVersion != nil && [versionComparator compareVersion:skippedMajorVersion toVersion:(NSString * _Nonnull)ui.minimumAutoupdateVersion] != NSOrderedAscending && (ui.ignoreSkippedUpgradesBelowVersion == nil || (skippedMajorSubreleaseVersion != nil && [versionComparator compareVersion:skippedMajorSubreleaseVersion toVersion:(NSString * _Nonnull)ui.ignoreSkippedUpgradesBelowVersion] != NSOrderedAscending))) { // If skipped major version is >= than the item's minimumAutoupdateVersion, we can skip the item. // But if there is an ignoreSkippedUpgradesBelowVersion, we can only skip the item if the last skipped subrelease // version is >= than that version provided by the item return YES; } NSString *skippedMinorVersion = skippedUpdate.minorVersion; if (skippedMinorVersion != nil && [versionComparator compareVersion:skippedMinorVersion toVersion:ui.versionString] != NSOrderedAscending) { // Item is on a less or equal version than a minor version we've skipped // So we skip this item return YES; } return NO; } + (BOOL)itemIsReadyForPhasedRollout:(SUAppcastItem *)ui phasedUpdateGroup:(NSNumber * _Nullable)phasedUpdateGroup currentDate:(NSDate *)currentDate hostVersion:(NSString *)hostVersion versionComparator:(id)versionComparator SPU_OBJC_DIRECT { if (phasedUpdateGroup == nil || ui.criticalUpdate) { return YES; } NSNumber *phasedRolloutIntervalObject = [ui phasedRolloutInterval]; if (phasedRolloutIntervalObject == nil) { return YES; } NSDate* itemReleaseDate = ui.date; if (itemReleaseDate == nil) { return YES; } NSTimeInterval timeSinceRelease = [currentDate timeIntervalSinceDate:itemReleaseDate]; NSTimeInterval phasedRolloutInterval = [phasedRolloutIntervalObject doubleValue]; NSTimeInterval timeToWaitForGroup = phasedRolloutInterval * (NSTimeInterval)(phasedUpdateGroup.unsignedIntegerValue); if (timeSinceRelease >= timeToWaitForGroup) { return YES; } return NO; } - (void)cleanup:(void (^)(void))completionHandler { if (_downloadDriver == nil) { completionHandler(); } else { [_downloadDriver cleanup:completionHandler]; } } @end ================================================ FILE: Sparkle/SUAppcastItem+Private.h ================================================ // // SUAppcastItem+Private.h // Sparkle // // Created by Mayur Pawashe on 4/30/21. // Copyright © 2021 Sparkle Project. All rights reserved. // #ifndef SUAppcastItem_Private_h #define SUAppcastItem_Private_h #import #import NS_ASSUME_NONNULL_BEGIN // Available in SPUAppcastItemStateResolver.h (a private exposed header) @class SPUAppcastItemStateResolver; @class SUSignatures; @interface SUAppcastItem (Private) /** Initializes with data from a dictionary provided by the RSS class and state resolver This initializer method is intended to be marked "private" and discouraged from public usage. This method is available however. Talk to us to describe your use case and if you need to construct appcast items yourself. */ - (nullable instancetype)initWithDictionary:(NSDictionary *)dict relativeToURL:(NSURL * _Nullable)appcastURL stateResolver:(SPUAppcastItemStateResolver *)stateResolver signingValidationStatus:(SPUAppcastSigningValidationStatus)signingValidationStatus failureReason:(NSString * _Nullable __autoreleasing *_Nullable)error; /** The EdDSA and DSA signatures of the update along with their statuses. */ @property (readonly, nonatomic, nullable) SUSignatures *signatures; /** The EdDSA signature of the external release notes along with its status. */ @property (readonly, nonatomic, nullable) SUSignatures *releaseNotesSignatures; /** The expected content length of the release notes file. */ @property (readonly, nonatomic) uint64_t releaseNotesContentLength; @end NS_ASSUME_NONNULL_END #endif /* SUAppcastItem_Private_h */ ================================================ FILE: Sparkle/SUAppcastItem.h ================================================ // // SUAppcastItem.h // Sparkle // // Created by Andy Matuschak on 3/12/06. // Copyright 2006 Andy Matuschak. All rights reserved. // #ifndef SUAPPCASTITEM_H #define SUAPPCASTITEM_H #import #ifdef BUILDING_SPARKLE_SOURCES_EXTERNALLY // Ignore incorrect warning #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wquoted-include-in-framework-header" #import "SUExport.h" #import "SPUAppcastSigningValidationStatus.h" #pragma clang diagnostic pop #else #import #import #endif NS_ASSUME_NONNULL_BEGIN /** The appcast item describing an update in the application's appcast feed. An appcast item represents a single update item in the `SUAppcast` contained within the @c element. Every appcast item must have a `versionString`, and either a `fileURL` or an `infoURL`. All the remaining properties describing an update to the application are optional. Extended documentation and examples on using appcast item features are available at: https://sparkle-project.org/documentation/publishing/ */ SU_EXPORT NS_SWIFT_SENDABLE @interface SUAppcastItem : NSObject /** The version of the update item. Sparkle uses this property to compare update items and determine the best available update item in the `SUAppcast`. This corresponds to the application update's @c CFBundleVersion This is extracted from the @c element, or the @c sparkle:version attribute from the @c element. */ @property (nonatomic, copy, readonly) NSString *versionString; /** The human-readable display version of the update item if provided. This is the version string shown to the user when they are notified of a new update. This corresponds to the application update's @c CFBundleShortVersionString This is extracted from the @c element, or the @c sparkle:shortVersionString attribute from the @c element. If no short version string is available, this falls back to the update's `versionString`. */ @property (nonatomic, copy, readonly) NSString *displayVersionString; /** The file URL to the update item if provided. This download contains the actual update Sparkle will attempt to install. In cases where a download cannot be provided, an `infoURL` must be provided instead. A file URL should have an accompanying `contentLength` provided. This is extracted from the @c url attribute in the @c element. */ @property (nonatomic, readonly, nullable) NSURL *fileURL; /** The content length of the download in bytes. This property is used as a fallback when the server doesn't report the content length of the download. In that case, it is used to report progress of the downloading update to the user. A warning is outputted if this property is not equal the server's expected content length (if provided). This is extracted from the @c length attribute in the @c element. It should be specified if a `fileURL` is provided. */ @property (nonatomic, readonly) uint64_t contentLength; /** The info URL to the update item if provided. This informational link is used to direct the user to learn more about an update they cannot download/install directly from within the application. The link should point to the product's web page. The informational link will be used if `informationOnlyUpdate` is @c YES This is extracted from the @c element. */ @property (nonatomic, readonly, nullable) NSURL *infoURL; /** Indicates whether or not the update item is only informational and has no download. If `infoURL` is not present, this is @c NO If `fileURL` is not present, this is @c YES Otherwise this is determined based on the contents extracted from the @c element. */ @property (nonatomic, getter=isInformationOnlyUpdate, readonly) BOOL informationOnlyUpdate; /** The title of the appcast item if provided. This is extracted from the @c element. */ @property (nonatomic, copy, readonly, nullable) NSString *title; /** The date string of the appcast item if provided. The `date` property is constructed from this property and expects this string to comply with the following date format: `E, dd MMM yyyy HH:mm:ss Z` This is extracted from the @c <pubDate> element. */ @property (nonatomic, copy, readonly, nullable) NSString *dateString; /** The date constructed from the `dateString` property if provided. Sparkle by itself only uses this property for phased group rollouts specified via `phasedRolloutInterval`, but clients may query this property too. This date is constructed using the @c en_US locale. */ @property (nonatomic, copy, readonly, nullable) NSDate *date; /** The release notes URL of the appcast item if provided. This external link points to an HTML file that Sparkle downloads and renders to show the user a new or old update item's changelog. An alternative to using an external release notes link is providing an embedded `itemDescription`. This is extracted from the @c <sparkle:releaseNotesLink> element. */ @property (nonatomic, readonly, nullable) NSURL *releaseNotesURL; /** The description of the appcast item if provided. A description may be provided for inline/embedded release notes for new updates using @c <![CDATA[...]]> This is an alternative to providing a `releaseNotesURL`. This is extracted from the @c <description> element. */ @property (nonatomic, copy, readonly, nullable) NSString *itemDescription; /** The format of the `itemDescription` for inline/embedded release notes if provided. This may be: - @c html - @c plain-text This is extracted from the @c sparkle:descriptionFormat attribute in the @c <description> element. If the format is not provided in the @c <description> element of the appcast item, then this property may default to `html`. If the @c <description> element of the appcast item is not available, this property is `nil`. */ @property (nonatomic, readonly, nullable) NSString *itemDescriptionFormat; /** The full release notes URL of the appcast item if provided. The link should point to the product's full changelog. Sparkle's standard user interface offers to show these full release notes when a user checks for a new update and no new update is available. This is extracted from the @c <sparkle:fullReleaseNotesLink> element. */ @property (nonatomic, readonly, nullable) NSURL *fullReleaseNotesURL; /** The required minimum system operating version string for this update if provided. This version string should contain three period-separated components. Example: @c 10.13.0 Use `minimumOperatingSystemVersionIsOK` property to test if the current running system passes this requirement. This is extracted from the @c <sparkle:minimumSystemVersion> element. */ @property (nonatomic, copy, readonly, nullable) NSString *minimumSystemVersion; /** Indicates whether or not the current running system passes the `minimumSystemVersion` requirement. */ @property (nonatomic, readonly) BOOL minimumOperatingSystemVersionIsOK; /** This update will be ignored if the application's version precedes this update's minimum version. This application's version corresponds to the bundle's @c CFBundleVersion The minimum update version is extracted from the @c <sparkle:minimumUpdateVersion> element. Use `minimumUpdateVersionIsOK` property to test if the current bundle passes this requirement. Old applications must be using Sparkle 2.9 or later, otherwise this property will be ignored. */ @property (nonatomic, copy, readonly, nullable) NSString *minimumUpdateVersion; /** Indicates whether or not the current bundle passes the `minimumUpdateVersion` requirement. */ @property (nonatomic, readonly) BOOL minimumUpdateVersionIsOK; /** The required maximum system operating version string for this update if provided. A maximum system operating version requirement should only be made in unusual scenarios. This version string should contain three period-separated components. Example: @c 10.14.0 Use `maximumOperatingSystemVersionIsOK` property to test if the current running system passes this requirement. This is extracted from the @c <sparkle:maximumSystemVersion> element. */ @property (nonatomic, copy, readonly, nullable) NSString *maximumSystemVersion; /** Indicates whether or not the current running system passes the `maximumSystemVersion` requirement. */ @property (nonatomic, readonly) BOOL maximumOperatingSystemVersionIsOK; /** The required hardware requirements for this update if provided. Example: @c arm64 Use `arm64HardwareRequirementIsOK` property to test if the current running system passes the @c arm64 requirement. This is extracted from the @c <sparkle:hardwareRequirements> element, which is a comma-delimited list of hardware requirements. */ @property (nonatomic, copy, readonly) NSSet<NSString *> *hardwareRequirements; /** Indicates whether or not the current running system passes the arm64 requirement if specified in the `hardwareRequirements` requirement. */ @property (nonatomic, readonly) BOOL arm64HardwareRequirementIsOK; /** The channel the update item is on if provided. An update item may specify a custom channel name (such as @c beta) that can only be found by updaters that filter for that channel. If no channel is provided, the update item is assumed to be on the default channel. This is extracted from the @c <sparkle:channel> element. Old applications must be using Sparkle 2 or later to interpret the channel element and to ignore unmatched channels. */ @property (nonatomic, readonly, nullable) NSString *channel; /** The installation type of the update at `fileURL` This may be: - @c application - indicates this is a regular application update. - @c package - indicates this is a package installer update. This is extracted from the @c sparkle:installationType attribute in the @c <enclosure> element. If no installation type is provided in the enclosure, the installation type is inferred from the `fileURL` file extension instead. If the file extension is @c pkg or @c mpkg, the installation type is @c package otherwise it is @c application Hence, the installation type in the enclosure element only needs to be specified for package based updates distributed inside of a @c zip or other archive format. Old applications must be using Sparkle 1.26 or later to support downloading bare package updates (`pkg` or `mpkg`) that are not additionally archived inside of a @c zip or other archive format. */ @property (nonatomic, copy, readonly) NSString *installationType; /** The appcast signing validation status that this appcast item came from. Please see documentation of @c SPUAppcastSigningValidationStatus values for more information. */ @property (readonly, nonatomic) SPUAppcastSigningValidationStatus signingValidationStatus; /** The phased rollout interval of the update item in seconds if provided. This is the interval between when different groups of users are notified of a new update. For this property to be used by Sparkle, the published `date` on the update item must be present as well. After each interval after the update item's `date`, a new group of users become eligible for being notified of the new update. This is extracted from the @c <sparkle:phasedRolloutInterval> element. Old applications must be using Sparkle 1.25 or later to support phased rollout intervals, otherwise they may assume updates are immediately available. */ @property (nonatomic, copy, readonly, nullable) NSNumber* phasedRolloutInterval; /** The minimum bundle version string this update requires for automatically downloading and installing updates if provided. If an application's bundle version meets this version requirement, it can install the new update item in the background automatically. Otherwise if the requirement is not met, the user is always prompted to install the update. In this case, the update is assumed to be a `majorUpgrade`. If the update is a `majorUpgrade` and the update is skipped by the user, other future update alerts with the same `minimumAutoupdateVersion` will also be skipped automatically unless an update specifies `ignoreSkippedUpgradesBelowVersion`. This version string corresponds to the application's @c CFBundleVersion This is extracted from the @c <sparkle:minimumAutoupdateVersion> element. */ @property (nonatomic, copy, readonly, nullable) NSString *minimumAutoupdateVersion; /** Indicates whether or not the update item is a major upgrade. An update is a major upgrade if the application's bundle version doesn't meet the `minimumAutoupdateVersion` requirement. */ @property (nonatomic, getter=isMajorUpgrade, readonly) BOOL majorUpgrade; /** Previously skipped upgrades by the user will be ignored if they skipped an update whose version precedes this version. This can only be applied if the update is a `majorUpgrade`. This version string corresponds to the application's @c CFBundleVersion This is extracted from the @c <sparkle:ignoreSkippedUpgradesBelowVersion> element. Old applications must be using Sparkle 2.1 or later, otherwise this property will be ignored. */ @property (nonatomic, readonly, nullable) NSString *ignoreSkippedUpgradesBelowVersion; /** Indicates whether or not the update item is critical. Critical updates are shown to the user more promptly. Sparkle's standard user interface also does not allow them to be skipped. This is determined and extracted from a top-level @c <sparkle:criticalUpdate> element or a @c sparkle:criticalUpdate element inside of a @c sparkle:tags element. Old applications must be using Sparkle 2 or later to support the top-level @c <sparkle:criticalUpdate> element. */ @property (nonatomic, getter=isCriticalUpdate, readonly) BOOL criticalUpdate; /** Specifies the operating system the download update is available for if provided. If this property is not provided, then the supported operating system is assumed to be macOS. Known potential values for this string are @c macos and @c windows Sparkle on Mac ignores update items that are for other operating systems. This is only useful for sharing appcasts between Sparkle on Mac and Sparkle on other operating systems. Use `macOsUpdate` property to test if this update item is for macOS. This is extracted from the @c sparkle:os attribute in the @c <enclosure> element. */ @property (nonatomic, copy, readonly, nullable) NSString *osString; /** Indicates whether or not this update item is for macOS. This is determined from the `osString` property. */ @property (nonatomic, getter=isMacOsUpdate, readonly) BOOL macOsUpdate; /** The delta updates for this update item. Sparkle uses these to download and apply a smaller update based on the version the user is updating from. The key is based on the @c sparkle:version of the update. The value is an update item that will have `deltaUpdate` be @c YES Clients typically should not need to examine the contents of the delta updates. This is extracted from the @c <sparkle:deltas> element. */ @property (nonatomic, copy, readonly, nullable) NSDictionary<NSString *, SUAppcastItem *> *deltaUpdates; /** The expected size of the Sparkle executable file before applying this delta update. This attribute is used to test if the delta item can still be applied. If Sparkle's executable file has changed (e.g. from having an architecture stripped), then the delta item cannot be applied. This is extracted from the @c sparkle:deltaFromSparkleExecutableSize attribute from the @c <enclosure> element of a @c sparkle:deltas item. This attribute is optional for delta update items. */ @property (nonatomic, nonatomic, readonly, nullable) NSNumber *deltaFromSparkleExecutableSize; /** An expected set of Sparkle's locales present on disk before applying this delta update. This attribute is used to test if the delta item can still be applied. If Sparkle's list of locales present on disk (.lproj directories) do not contain any items from this set, (e.g. from having localization files stripped) then the delta item cannot be applied. This set does not need to be a complete list of locales. Sparkle may even decide to not process all them. 1-10 should be a decent amount. This is extracted from the @c sparkle:deltaFromSparkleLocales attribute from the @c <enclosure> element of a @c sparkle:deltas item. The locales extracted from this attribute are delimited by a comma (e.g. "en,ca,fr,hr,hu"). This attribute is optional for delta update items. */ @property (nonatomic, nonatomic, readonly, nullable) NSSet<NSString *> *deltaFromSparkleLocales; /** Indicates whether or not the update item is a delta update. An update item is a delta update if it is in the `deltaUpdates` of another update item. */ @property (nonatomic, getter=isDeltaUpdate, readonly) BOOL deltaUpdate; /** The dictionary representing the entire appcast item. This is useful for querying custom extensions or elements from the appcast item. */ @property (nonatomic, readonly, copy) NSDictionary *propertiesDictionary; - (instancetype)init NS_UNAVAILABLE; /** An empty appcast item. This may be used as a potential return value in `-[SPUUpdaterDelegate bestValidUpdateInAppcast:forUpdater:]` */ + (instancetype)emptyAppcastItem; // Deprecated initializers - (nullable instancetype)initWithDictionary:(NSDictionary *)dict __deprecated_msg("Properties that depend on the system or application version are not supported when used with this initializer. The designated initializer is available in SUAppcastItem+Private.h. Please first explore other APIs or contact us to describe your use case."); - (nullable instancetype)initWithDictionary:(NSDictionary *)dict failureReason:(NSString * _Nullable __autoreleasing *_Nullable)error __deprecated_msg("Properties that depend on the system or application version are not supported when used with this initializer. The designated initializer is available in SUAppcastItem+Private.h. Please first explore other APIs or contact us to describe your use case."); - (nullable instancetype)initWithDictionary:(NSDictionary *)dict relativeToURL:(NSURL * _Nullable)appcastURL failureReason:(NSString * _Nullable __autoreleasing *_Nullable)error __deprecated_msg("Properties that depend on the system or application version are not supported when used with this initializer. The designated initializer is available in SUAppcastItem+Private.h. Please first explore other APIs or contact us to describe your use case."); @end NS_ASSUME_NONNULL_END #endif ================================================ FILE: Sparkle/SUAppcastItem.m ================================================ // // SUAppcastItem.m // Sparkle // // Created by Andy Matuschak on 3/12/06. // Copyright 2006 Andy Matuschak. All rights reserved. // #import "SUAppcastItem.h" #import "SUVersionComparisonProtocol.h" #import "SULog.h" #import "SUConstants.h" #import "SUSignatures.h" #import "SPUInstallationType.h" #import "SPUAppcastItemState.h" #import "SPUAppcastItemStateResolver.h" #import "SPUAppcastItemStateResolver+Private.h" #include "AppKitPrevention.h" #define DELTA_EXPECTED_LOCALES_LIMIT 15 static NSString *SUAppcastItemDeltaUpdatesKey = @"deltaUpdates"; static NSString *SUAppcastItemDisplayVersionStringKey = @"displayVersionString"; static NSString *SUAppcastItemSignaturesKey = @"signatures"; static NSString *SUAppcastItemReleaseNotesSignaturesKey = @"releaseNotesSignatures"; static NSString *SUAppcastItemFileURLKey = @"fileURL"; static NSString *SUAppcastItemInfoURLKey = @"infoURL"; static NSString *SUAppcastItemContentLengthKey = @"contentLength"; static NSString *SUAppcastItemLinkLengthKey = @"linkLength"; static NSString *SUAppcastItemDescriptionKey = @"itemDescription"; static NSString *SUAppcastItemDescriptionFormatKey = @"itemDescriptionFormat"; static NSString *SUAppcastItemMaximumSystemVersionKey = @"maximumSystemVersion"; static NSString *SUAppcastItemMinimumSystemVersionKey = @"minimumSystemVersion"; static NSString *SUAppcastElementHardwareRequirementsKey = @"hardwareRequirements"; static NSString *SUAppcastItemReleaseNotesURLKey = @"releaseNotesURL"; static NSString *SUAppcastItemFullReleaseNotesURLKey = @"fullReleaseNotesURL"; static NSString *SUAppcastItemTitleKey = @"title"; static NSString *SUAppcastItemVersionStringKey = @"versionString"; static NSString *SUAppcastItemPropertiesKey = @"propertiesDictionary"; static NSString *SUAppcastItemInstallationTypeKey = @"SUAppcastItemInstallationType"; static NSString *SUAppcastItemStateKey = @"SUAppcastItemState"; static NSString *SUAppcastItemDeltaFromSparkleExecutableSizeKey = @"SUAppcastItemDeltaFromSparkleExecutableSize"; static NSString *SUAppcastItemDeltaFromSparkleLocalesKey = @"SUAppcastItemDeltaFromSparkleLocales"; static NSString *SUAppcastItemSigningValidationStatusKey = @"SUAppcastItemSigningValidationStatus"; @interface SUAppcastItem () @property (readonly, nonatomic, nullable) SUSignatures *signatures; @property (readonly, nonatomic, nullable) SUSignatures *releaseNotesSignatures; @property (readonly, nonatomic) uint64_t releaseNotesContentLength; @end @implementation SUAppcastItem { // Auxiliary appcast item state that needs to be evaluated based on the host state // This may be nil if the client creates an SUAppcastItem with a deprecated initializer // In that case we will need to fallback to safe behavior SPUAppcastItemState *_state; // Indicates if we have any critical information. Used as a fallback if state is nil BOOL _hasCriticalInformation; // Indicates the versions we update from that are informational-only NSSet<NSString *> *_informationalUpdateVersions; } @synthesize dateString = _dateString; @synthesize deltaUpdates = _deltaUpdates; @synthesize displayVersionString = _displayVersionString; @synthesize signatures = _signatures; @synthesize releaseNotesSignatures = _releaseNotesSignatures; @synthesize releaseNotesContentLength = _releaseNotesContentLength; @synthesize fileURL = _fileURL; @synthesize contentLength = _contentLength; @synthesize infoURL = _infoURL; @synthesize itemDescription = _itemDescription; @synthesize itemDescriptionFormat = _itemDescriptionFormat; @synthesize maximumSystemVersion = _maximumSystemVersion; @synthesize minimumSystemVersion = _minimumSystemVersion; @synthesize hardwareRequirements = _hardwareRequirements; @synthesize releaseNotesURL = _releaseNotesURL; @synthesize fullReleaseNotesURL = _fullReleaseNotesURL; @synthesize title = _title; @synthesize versionString = _versionString; @synthesize osString = _osString; @synthesize propertiesDictionary = _propertiesDictionary; @synthesize installationType = _installationType; @synthesize minimumAutoupdateVersion = _minimumAutoupdateVersion; @synthesize ignoreSkippedUpgradesBelowVersion = _ignoreSkippedUpgradesBelowVersion; @synthesize minimumUpdateVersion = _minimumUpdateVersion; @synthesize phasedRolloutInterval = _phasedRolloutInterval; @synthesize channel = _channel; @synthesize deltaFromSparkleExecutableSize = _deltaFromSparkleExecutableSize; @synthesize deltaFromSparkleLocales = _deltaFromSparkleLocales; @synthesize signingValidationStatus = _signingValidationStatus; + (BOOL)supportsSecureCoding { return YES; } - (instancetype)initWithCoder:(NSCoder *)decoder { self = [super init]; if (self != nil) { _deltaUpdates = [decoder decodeObjectOfClasses:[NSSet setWithArray:@[[NSDictionary class], [SUAppcastItem class], [NSString class]]] forKey:SUAppcastItemDeltaUpdatesKey]; _deltaFromSparkleExecutableSize = [decoder decodeObjectOfClass:[NSNumber class] forKey:SUAppcastItemDeltaFromSparkleExecutableSizeKey]; _deltaFromSparkleLocales = [decoder decodeObjectOfClasses:[NSSet setWithArray:@[[NSSet class], [NSString class]]] forKey:SUAppcastItemDeltaFromSparkleLocalesKey]; _displayVersionString = [(NSString *)[decoder decodeObjectOfClass:[NSString class] forKey:SUAppcastItemDisplayVersionStringKey] copy]; _signatures = (SUSignatures *)[decoder decodeObjectOfClass:[SUSignatures class] forKey:SUAppcastItemSignaturesKey]; _releaseNotesSignatures = (SUSignatures *)[decoder decodeObjectOfClass:[SUSignatures class] forKey:SUAppcastItemReleaseNotesSignaturesKey]; _fileURL = [decoder decodeObjectOfClass:[NSURL class] forKey:SUAppcastItemFileURLKey]; _infoURL = [decoder decodeObjectOfClass:[NSURL class] forKey:SUAppcastItemInfoURLKey]; if (_fileURL == nil && _infoURL == nil) { return nil; } _contentLength = (uint64_t)[decoder decodeInt64ForKey:SUAppcastItemContentLengthKey]; _releaseNotesContentLength = (uint64_t)[decoder decodeInt64ForKey:SUAppcastItemLinkLengthKey]; NSString *installationType = [decoder decodeObjectOfClass:[NSString class] forKey:SUAppcastItemInstallationTypeKey]; if (!SPUValidInstallationType(installationType)) { return nil; } SPUAppcastItemState *state = [decoder decodeObjectOfClass:[SPUAppcastItemState class] forKey:SUAppcastItemStateKey]; _state = state; _installationType = [installationType copy]; _itemDescription = [(NSString *)[decoder decodeObjectOfClass:[NSString class] forKey:SUAppcastItemDescriptionKey] copy]; _itemDescriptionFormat = [(NSString *)[decoder decodeObjectOfClass:[NSString class] forKey:SUAppcastItemDescriptionFormatKey] copy]; _maximumSystemVersion = [(NSString *)[decoder decodeObjectOfClass:[NSString class] forKey:SUAppcastItemMaximumSystemVersionKey] copy]; _minimumSystemVersion = [(NSString *)[decoder decodeObjectOfClass:[NSString class] forKey:SUAppcastItemMinimumSystemVersionKey] copy]; _minimumAutoupdateVersion = [(NSString *)[decoder decodeObjectOfClass:[NSString class] forKey:SUAppcastElementMinimumAutoupdateVersion] copy]; NSSet<NSString *> *hardwareRequirements = [(NSSet<NSString *> *)[decoder decodeObjectOfClasses:[NSSet setWithArray:@[[NSString class], [NSSet class]]] forKey:SUAppcastElementHardwareRequirementsKey] copy]; _hardwareRequirements = (hardwareRequirements != nil) ? hardwareRequirements : [NSSet set]; _minimumUpdateVersion = [(NSString *)[decoder decodeObjectOfClass:[NSString class] forKey:SUAppcastElementMinimumUpdateVersion] copy]; _ignoreSkippedUpgradesBelowVersion = [(NSString *)[decoder decodeObjectOfClass:[NSString class] forKey:SUAppcastElementIgnoreSkippedUpgradesBelowVersion] copy]; _releaseNotesURL = [decoder decodeObjectOfClass:[NSURL class] forKey:SUAppcastItemReleaseNotesURLKey]; _fullReleaseNotesURL = [decoder decodeObjectOfClass:[NSURL class] forKey:SUAppcastItemFullReleaseNotesURLKey]; _title = [(NSString *)[decoder decodeObjectOfClass:[NSString class] forKey:SUAppcastItemTitleKey] copy]; NSString *versionString = [(NSString *)[decoder decodeObjectOfClass:[NSString class] forKey:SUAppcastItemVersionStringKey] copy]; if (versionString == nil) { return nil; } _versionString = versionString; _osString = [(NSString *)[decoder decodeObjectOfClass:[NSString class] forKey:SUAppcastAttributeOsType] copy]; NSDictionary *propertiesDictionary = [decoder decodeObjectOfClasses:[NSSet setWithArray:@[[NSDictionary class], [NSString class], [NSDate class], [NSArray class]]] forKey:SUAppcastItemPropertiesKey]; if (propertiesDictionary == nil) { return nil; } _propertiesDictionary = propertiesDictionary; _phasedRolloutInterval = [decoder decodeObjectOfClass:[NSNumber class] forKey:SUAppcastElementPhasedRolloutInterval]; _channel = [(NSString *)[decoder decodeObjectOfClass:[NSString class] forKey:SUAppcastElementChannel] copy]; NSInteger decodedSigningValidationStatus = [decoder decodeIntegerForKey:SUAppcastItemSigningValidationStatusKey]; switch (decodedSigningValidationStatus) { case SPUAppcastSigningValidationStatusSkipped: case SPUAppcastSigningValidationStatusSucceeded: case SPUAppcastSigningValidationStatusFailed: _signingValidationStatus = (SPUAppcastSigningValidationStatus)decodedSigningValidationStatus; break; default: // This shouldn't be reached, skipped == 0 matches an old encoder that doesn't encode this enum. return nil; } } return self; } - (void)encodeWithCoder:(NSCoder *)encoder { if (_deltaUpdates != nil) { [encoder encodeObject:_deltaUpdates forKey:SUAppcastItemDeltaUpdatesKey]; } if (_deltaFromSparkleExecutableSize != nil) { [encoder encodeObject:_deltaFromSparkleExecutableSize forKey:SUAppcastItemDeltaFromSparkleExecutableSizeKey]; } if (_deltaFromSparkleLocales != nil) { [encoder encodeObject:_deltaFromSparkleLocales forKey:SUAppcastItemDeltaFromSparkleLocalesKey]; } if (_displayVersionString != nil) { [encoder encodeObject:_displayVersionString forKey:SUAppcastItemDisplayVersionStringKey]; } if (_signatures != nil) { [encoder encodeObject:_signatures forKey:SUAppcastItemSignaturesKey]; } if (_releaseNotesSignatures != nil) { [encoder encodeObject:_releaseNotesSignatures forKey:SUAppcastItemReleaseNotesSignaturesKey]; } if (_fileURL != nil) { [encoder encodeObject:_fileURL forKey:SUAppcastItemFileURLKey]; } if (_infoURL != nil) { [encoder encodeObject:_infoURL forKey:SUAppcastItemInfoURLKey]; } [encoder encodeInt64:(int64_t)_contentLength forKey:SUAppcastItemContentLengthKey]; [encoder encodeInt64:(int64_t)_releaseNotesContentLength forKey:SUAppcastItemLinkLengthKey]; if (_itemDescription != nil) { [encoder encodeObject:_itemDescription forKey:SUAppcastItemDescriptionKey]; } if (_itemDescriptionFormat != nil) { [encoder encodeObject:_itemDescriptionFormat forKey:SUAppcastItemDescriptionFormatKey]; } if (_maximumSystemVersion != nil) { [encoder encodeObject:_maximumSystemVersion forKey:SUAppcastItemMaximumSystemVersionKey]; } if (_minimumSystemVersion != nil) { [encoder encodeObject:_minimumSystemVersion forKey:SUAppcastItemMinimumSystemVersionKey]; } if (_minimumAutoupdateVersion != nil) { [encoder encodeObject:_minimumAutoupdateVersion forKey:SUAppcastElementMinimumAutoupdateVersion]; } if (_hardwareRequirements != nil) { [encoder encodeObject:_hardwareRequirements forKey:SUAppcastElementHardwareRequirementsKey]; } if (_ignoreSkippedUpgradesBelowVersion != nil) { [encoder encodeObject:_ignoreSkippedUpgradesBelowVersion forKey:SUAppcastElementIgnoreSkippedUpgradesBelowVersion]; } if (_minimumUpdateVersion != nil) { [encoder encodeObject:_minimumUpdateVersion forKey:SUAppcastElementMinimumUpdateVersion]; } if (_state != nil) { [encoder encodeObject:_state forKey:SUAppcastItemStateKey]; } if (_releaseNotesURL != nil) { [encoder encodeObject:_releaseNotesURL forKey:SUAppcastItemReleaseNotesURLKey]; } if (_fullReleaseNotesURL != nil) { [encoder encodeObject:_fullReleaseNotesURL forKey:SUAppcastItemFullReleaseNotesURLKey]; } if (_title != nil) { [encoder encodeObject:_title forKey:SUAppcastItemTitleKey]; } if (_versionString != nil) { [encoder encodeObject:_versionString forKey:SUAppcastItemVersionStringKey]; } if (_osString != nil) { [encoder encodeObject:_osString forKey:SUAppcastAttributeOsType]; } if (_propertiesDictionary != nil) { [encoder encodeObject:_propertiesDictionary forKey:SUAppcastItemPropertiesKey]; } if (_installationType != nil) { [encoder encodeObject:_installationType forKey:SUAppcastItemInstallationTypeKey]; } if (_phasedRolloutInterval != nil) { [encoder encodeObject:_phasedRolloutInterval forKey:SUAppcastElementPhasedRolloutInterval]; } if (_channel != nil) { [encoder encodeObject:_channel forKey:SUAppcastElementChannel]; } [encoder encodeInteger:_signingValidationStatus forKey:SUAppcastItemSigningValidationStatusKey]; } - (BOOL)isDeltaUpdate { NSDictionary *rssElementEnclosure = [_propertiesDictionary objectForKey:SURSSElementEnclosure]; return [rssElementEnclosure objectForKey:SUAppcastAttributeDeltaFrom] != nil; } - (BOOL)isCriticalUpdate { if (_state != nil) { return _state.criticalUpdate; } else { return _hasCriticalInformation; } } - (BOOL)isMajorUpgrade { if (_state != nil) { return _state.majorUpgrade; } else { return NO; } } - (BOOL)minimumOperatingSystemVersionIsOK { if (_state != nil) { return _state.minimumOperatingSystemVersionIsOK; } else { return YES; } } - (BOOL)maximumOperatingSystemVersionIsOK { if (_state != nil) { return _state.maximumOperatingSystemVersionIsOK; } else { return YES; } } - (BOOL)minimumUpdateVersionIsOK { if (_state != nil) { return _state.minimumUpdateVersionIsOK; } else { return YES; } } - (BOOL)arm64HardwareRequirementIsOK { if (_state != nil) { return _state.arm64HardwareRequirementIsOK; } else { return YES; } } - (BOOL)isMacOsUpdate { return _osString == nil || [_osString isEqualToString:SUAppcastAttributeValueMacOS]; } - (NSDate *)date { NSString *dateString = _dateString; if (dateString == nil) { return nil; } NSDateFormatter* dateFormatter = [[NSDateFormatter alloc] init]; dateFormatter.locale = [[NSLocale alloc] initWithLocaleIdentifier:@"en_US"]; dateFormatter.dateFormat = @"E, dd MMM yyyy HH:mm:ss Z"; return [dateFormatter dateFromString:dateString]; } - (BOOL)isInformationOnlyUpdate { if (_state != nil) { return _state.informationalUpdate; } else { return (_informationalUpdateVersions != nil && _informationalUpdateVersions.count == 0); } } + (instancetype)emptyAppcastItem { static SUAppcastItem *emptyAppcastItem; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ emptyAppcastItem = [[SUAppcastItem alloc] init]; }); return emptyAppcastItem; } // Initializer used for making delta items - (nullable instancetype)initWithDictionary:(NSDictionary *)dict relativeToURL:(NSURL * _Nullable)appcastURL state:(SPUAppcastItemState * _Nullable)state signingValidationStatus:(SPUAppcastSigningValidationStatus)signingValidationStatus SPU_OBJC_DIRECT { return [self initWithDictionary:dict relativeToURL:appcastURL stateResolver:nil resolvedState:state signingValidationStatus:signingValidationStatus failureReason:nil]; } // Exported public initializer - (nullable instancetype)initWithDictionary:(NSDictionary *)dict relativeToURL:(NSURL * _Nullable)appcastURL stateResolver:(SPUAppcastItemStateResolver *)stateResolver signingValidationStatus:(SPUAppcastSigningValidationStatus)signingValidationStatus failureReason:(NSString *__autoreleasing *)error { return [self initWithDictionary:dict relativeToURL:appcastURL stateResolver:stateResolver resolvedState:nil signingValidationStatus:signingValidationStatus failureReason:error]; } // Deprecated - (nullable instancetype)initWithDictionary:(NSDictionary *)dict { return [self initWithDictionary:dict relativeToURL:nil stateResolver:nil resolvedState:nil signingValidationStatus:SPUAppcastSigningValidationStatusSkipped failureReason:nil]; } // Deprecated - (nullable instancetype)initWithDictionary:(NSDictionary *)dict failureReason:(NSString *__autoreleasing *)error { return [self initWithDictionary:dict relativeToURL:nil stateResolver:nil resolvedState:nil signingValidationStatus:SPUAppcastSigningValidationStatusSkipped failureReason:error]; } // Deprecated - (nullable instancetype)initWithDictionary:(NSDictionary *)dict relativeToURL:(NSURL * _Nullable)appcastURL failureReason:(NSString *__autoreleasing *)error { return [self initWithDictionary:dict relativeToURL:appcastURL stateResolver:nil resolvedState:nil signingValidationStatus:SPUAppcastSigningValidationStatusSkipped failureReason:error]; } // When the feed fails signing validation as fallback, sanitize the version strings // so they don't easily contain long believable messages telling the user to do something #define MAX_NUMBER_OF_CHARACTERS_IN_UNTRUSTED_VERSION_STRING_AFTER_FIRST_LETTER 10 static NSString *SPUSanitizeUntrustedVersionString(NSString *versionString, NSString *versionStringElement) { NSCharacterSet *lettersCharacterSet = [NSCharacterSet letterCharacterSet]; NSRange firstLetterRange = [versionString rangeOfCharacterFromSet:lettersCharacterSet]; if (firstLetterRange.location == NSNotFound) { // No alphabetic characters return versionString; } NSUInteger maxVersionStringLength = MIN(firstLetterRange.location + MAX_NUMBER_OF_CHARACTERS_IN_UNTRUSTED_VERSION_STRING_AFTER_FIRST_LETTER, versionString.length); NSRange allowedRange = NSMakeRange(0, maxVersionStringLength); NSString *allowedVersionString = [versionString substringWithRange:allowedRange]; if (![versionString isEqualToString:allowedVersionString]) { SULog(SULogLevelError, @"Error: Sanitized appcast item %@ from '%@' to '%@' because appcast signing validation failed and version could be untrusted", versionStringElement, versionString, allowedVersionString); } return allowedVersionString; } - (nullable instancetype)initWithDictionary:(NSDictionary *)dict relativeToURL:(NSURL * _Nullable)appcastURL stateResolver:(SPUAppcastItemStateResolver * _Nullable)stateResolver resolvedState:(SPUAppcastItemState * _Nullable)resolvedState signingValidationStatus:(SPUAppcastSigningValidationStatus)signingValidationStatus failureReason:(NSString *__autoreleasing *)error { self = [super init]; if (self) { _signingValidationStatus = signingValidationStatus; _title = [(NSString *)[dict objectForKey:SURSSElementTitle] copy]; NSDictionary *enclosure = [dict objectForKey:SURSSElementEnclosure]; // Try to find a version string. // Finding the new version number from the RSS feed is a little bit hacky. There are a few ways: // 1. A "sparkle:version" attribute on the enclosure tag, an extension from the RSS spec. // 2. If there isn't a version attribute, see if there is a version element (this is now the recommended path). // 3. If there isn't a version element, Sparkle will parse the path in the enclosure, expecting // that it will look like this: http://something.com/YourApp_0.5.zip. It'll read whatever's between the last // underscore and the last period as the version number. So name your packages like this: APPNAME_VERSION.extension. // The big caveat with this is that you can't have underscores in your version strings, as that'll confuse Sparkle. // Feel free to change the separator string to a hyphen or something more suited to your needs if you like. NSString *newVersion = [enclosure objectForKey:SUAppcastAttributeVersion]; if (newVersion == nil) { // Get version from the item newVersion = [dict objectForKey:SUAppcastElementVersion]; } if (newVersion == nil) { // No sparkle:version element/attribute anywhere? SULog(SULogLevelError, @"warning: Item '%@' is missing '<%@>' element. Version comparison may be unreliable. Please always specify %@", _title, SUAppcastElementVersion, SUAppcastElementVersion); // Grabbing the version from the URL is not properly documented or encouraged. // Not supporting it for appcast signing if (signingValidationStatus == SPUAppcastSigningValidationStatusSkipped) { // Separate the url by underscores and take the last component, as that'll be closest to the end, // then we remove the extension. Hopefully, this will be the version. NSArray<NSString *> *fileComponents = [(NSString *)[enclosure objectForKey:SURSSAttributeURL] componentsSeparatedByString:@"_"]; if ([fileComponents count] > 1) { newVersion = [[fileComponents lastObject] stringByDeletingPathExtension]; } } } if (newVersion == nil) { if (error) { *error = [NSString stringWithFormat:@"Feed item lacks %@ element, and version couldn't be deduced.", SUAppcastElementVersion]; } return nil; } if (signingValidationStatus == SPUAppcastSigningValidationStatusFailed) { newVersion = SPUSanitizeUntrustedVersionString(newVersion, SUAppcastElementVersion); } _propertiesDictionary = [[NSDictionary alloc] initWithDictionary:dict]; _dateString = [(NSString *)[dict objectForKey:SURSSElementPubDate] copy]; // Description is not to be trusted if appcast wasn't signed correctly id itemDescription = [dict objectForKey:SURSSElementDescription]; if (signingValidationStatus != SPUAppcastSigningValidationStatusFailed && itemDescription != nil) { if ([(NSObject *)itemDescription isKindOfClass:[NSDictionary class]]) { NSString *descriptionContent = itemDescription[@"content"]; NSString *itemDescriptionString; if ([descriptionContent isKindOfClass:[NSString class]]) { itemDescriptionString = [descriptionContent copy]; } else { itemDescriptionString = nil; } id descriptionFormat = itemDescription[@"format"]; NSString *descriptionFormatString; if ([(NSObject *)descriptionFormat isKindOfClass:[NSString class]]) { descriptionFormatString = [(NSString *)descriptionFormat lowercaseString]; } else { descriptionFormatString = nil; } _itemDescription = itemDescriptionString; if (itemDescriptionString != nil) { if (descriptionFormatString != nil) { if (![descriptionFormatString isEqualToString:@"plain-text"] && ![descriptionFormatString isEqualToString:@"markdown"] && ![descriptionFormatString isEqualToString:@"html"]) { SULog(SULogLevelError, @"warning: Item '%@' has unknown format '%@' in '<%@>'. Ignoring and using 'html' instead.", _title, descriptionFormatString, SUAppcastItemDescriptionKey); _itemDescriptionFormat = @"html"; } else { _itemDescriptionFormat = descriptionFormatString; } } else { _itemDescriptionFormat = @"html"; } } else { _itemDescriptionFormat = nil; } } else if ([(NSObject *)itemDescription isKindOfClass:[NSString class]]) { // Legacy path _itemDescription = [(NSString *)itemDescription copy]; _itemDescriptionFormat = @"html"; } } else { _itemDescription = nil; _itemDescriptionFormat = nil; } NSURL *infoURL = nil; NSString *infoLinkURLString = [dict objectForKey:SURSSElementLink]; if (infoLinkURLString != nil) { if (![infoLinkURLString isKindOfClass:[NSString class]]) { SULog(SULogLevelError, @"%@ -%@ Info URL is not of valid type.", NSStringFromClass([self class]), NSStringFromSelector(_cmd)); } else { NSURL *processedInfoURL; if (appcastURL != nil) { processedInfoURL = [NSURL URLWithString:infoLinkURLString relativeToURL:appcastURL]; } else { processedInfoURL = [NSURL URLWithString:infoLinkURLString]; } if ([processedInfoURL.scheme caseInsensitiveCompare:@"http"] == NSOrderedSame || [processedInfoURL.scheme caseInsensitiveCompare:@"https"] == NSOrderedSame) { infoURL = processedInfoURL; } else { SULog(SULogLevelError, @"%@ -%@ Info URL must have a http or https URL scheme.", NSStringFromClass([self class]), NSStringFromSelector(_cmd)); } } } // Need an info URL or an enclosure URL. Former to show "More Info" // page, latter to download & install: if (enclosure == nil && infoURL == nil) { if (error) { *error = @"No enclosure in feed item"; } return nil; } // Further below in this method validation is done for info-only updates if appcast signing validation failed if (infoURL != nil) { // If enclosure doesn't exist, the update must be an informational update // Otherwise check presence of informational update element _informationalUpdateVersions = (enclosure != nil) ? [dict objectForKey:SUAppcastElementInformationalUpdate] : [NSSet set]; } else { // Not an informational update _informationalUpdateVersions = nil; } NSString *enclosureURLString = [enclosure objectForKey:SURSSAttributeURL]; if (enclosureURLString == nil && infoURL == nil) { if (error) { *error = @"Feed item's enclosure lacks URL"; } return nil; } if (enclosureURLString) { NSString *enclosureLengthString = [enclosure objectForKey:SURSSAttributeLength]; long long contentLength = 0; if (enclosureLengthString != nil) { contentLength = [enclosureLengthString longLongValue]; } _contentLength = (contentLength > 0) ? (uint64_t)contentLength : 0; } if (enclosureURLString) { // Sparkle used to always URL-encode, so for backwards compatibility spaces in URLs must be forgiven. NSString *fileURLString = [enclosureURLString stringByReplacingOccurrencesOfString:@" " withString:@"%20"]; NSURL *fileURL; if (appcastURL != nil) { fileURL = [NSURL URLWithString:fileURLString relativeToURL:appcastURL]; } else { fileURL = [NSURL URLWithString:fileURLString]; } if ([fileURL.scheme caseInsensitiveCompare:@"http"] == NSOrderedSame || [fileURL.scheme caseInsensitiveCompare:@"https"] == NSOrderedSame) { _fileURL = fileURL; } else { if (error) { *error = @"File URLs must have a http or https URL scheme"; } return nil; } } if (enclosure) { _signatures = [[SUSignatures alloc] initWithEd:[enclosure objectForKey:SUAppcastAttributeEDSignature] #if SPARKLE_BUILD_LEGACY_DSA_SUPPORT dsa:[enclosure objectForKey:SUAppcastAttributeDSASignature] #endif ]; _osString = [enclosure objectForKey:SUAppcastAttributeOsType]; } _versionString = [(NSString *)newVersion copy]; _minimumSystemVersion = [(NSString *)[dict objectForKey:SUAppcastElementMinimumSystemVersion] copy]; _maximumSystemVersion = [(NSString *)[dict objectForKey:SUAppcastElementMaximumSystemVersion] copy]; _minimumAutoupdateVersion = [(NSString *)[dict objectForKey:SUAppcastElementMinimumAutoupdateVersion] copy]; { NSString *hardwareRequirementsString = [(NSString *)[dict objectForKey:SUAppcastElementHardwareRequirements] copy]; if (hardwareRequirementsString != nil) { NSMutableCharacterSet *characterSet = [NSMutableCharacterSet whitespaceCharacterSet]; [characterSet addCharactersInString:@","]; NSArray<NSString *> *hardwareRequirementsArray = [hardwareRequirementsString componentsSeparatedByCharactersInSet:characterSet]; NSMutableSet<NSString *> *hardwareRequirements = [[NSMutableSet alloc] init]; for (NSString *hardwareRequirement in hardwareRequirementsArray) { if (hardwareRequirement.length > 0) { [hardwareRequirements addObject:hardwareRequirement.lowercaseString]; } } _hardwareRequirements = [hardwareRequirements copy]; } else { _hardwareRequirements = [NSSet set]; } } _minimumUpdateVersion = [(NSString *)[dict objectForKey:SUAppcastElementMinimumUpdateVersion] copy]; _ignoreSkippedUpgradesBelowVersion = [(NSString *)[dict objectForKey:SUAppcastElementIgnoreSkippedUpgradesBelowVersion] copy]; NSString *channel = [dict objectForKey:SUAppcastElementChannel]; if (channel != nil) { if (channel.length == 0) { SULog(SULogLevelError, @"warning: Item with version '%@' has zero-length channel; this will be ignored.", newVersion); _channel = nil; } else { // Reject characters in the channel name that may cause parsing problems in tools later NSMutableCharacterSet *allowedCharacterSet = [NSMutableCharacterSet alphanumericCharacterSet]; [allowedCharacterSet addCharactersInString:@"_.-"]; if ([channel rangeOfCharacterFromSet:allowedCharacterSet.invertedSet].location != NSNotFound) { SULog(SULogLevelError, @"warning: Item with version '%@' has channel with invalid name. This channel will be ignored. Only [a-zA-Z0-9._-] is allowed.", newVersion); _channel = nil; } else { _channel = [channel copy]; } } } // Grab critical update information // Critical update information is not to be trusted if appcast wasn't signed correctly NSDictionary * _Nullable criticalUpdateDictionary; if (signingValidationStatus == SPUAppcastSigningValidationStatusFailed) { criticalUpdateDictionary = nil; } else { NSDictionary * _Nullable criticalUpdateDictionaryFromAppcast = (NSDictionary *)[dict objectForKey:SUAppcastElementCriticalUpdate]; NSArray *tags = [dict objectForKey:SUAppcastElementTags]; if (criticalUpdateDictionaryFromAppcast != nil) { criticalUpdateDictionary = criticalUpdateDictionaryFromAppcast; } else if ([tags isKindOfClass:[NSArray class]] && [tags containsObject:SUAppcastElementCriticalUpdate]) { // Legacy path where critical update used to be a tag without a specified version criticalUpdateDictionary = @{}; } else { // No critical info present criticalUpdateDictionary = nil; } } _hasCriticalInformation = (criticalUpdateDictionary != nil); if (stateResolver != nil) { _state = [(SPUAppcastItemStateResolver * _Nonnull)stateResolver resolveStateWithInformationalUpdateVersions:_informationalUpdateVersions minimumUpdateVersion:_minimumUpdateVersion minimumOperatingSystemVersion:_minimumSystemVersion maximumOperatingSystemVersion:_maximumSystemVersion minimumAutoupdateVersion:_minimumAutoupdateVersion criticalUpdateDictionary:criticalUpdateDictionary hardwareRequirements:_hardwareRequirements]; } else { // Note state still may be nil if a deprecated initializer is used _state = resolvedState; } // Note this needs to be checked after creating _state and _informationalUpdateVersions if (signingValidationStatus == SPUAppcastSigningValidationStatusFailed && [self isInformationOnlyUpdate]) { if (error != nil) { *error = @"Informational update is rejected because signing validation on feed failed"; } return nil; } // Even when the update is not an informational only update, the link may be referenced elsewhere // If signing validation on appcast failed, the link is not to be trusted anywhere _infoURL = (signingValidationStatus != SPUAppcastSigningValidationStatusFailed) ? infoURL : nil; NSString* rolloutIntervalString = [(NSString *)[dict objectForKey:SUAppcastElementPhasedRolloutInterval] copy]; if (rolloutIntervalString != nil) { _phasedRolloutInterval = @(rolloutIntervalString.integerValue); } NSString *shortVersionString = [enclosure objectForKey:SUAppcastAttributeShortVersionString]; if (nil == shortVersionString) { shortVersionString = [dict objectForKey:SUAppcastElementShortVersionString]; // fall back on the <item> } if (shortVersionString != nil && signingValidationStatus == SPUAppcastSigningValidationStatusFailed) { shortVersionString = SPUSanitizeUntrustedVersionString(shortVersionString, SUAppcastElementShortVersionString); } if (shortVersionString) { _displayVersionString = [shortVersionString copy]; } else { _displayVersionString = [_versionString copy]; } NSString *chosenInstallationType; #if SPARKLE_BUILD_PACKAGE_SUPPORT NSString *attributeInstallationType = [enclosure objectForKey:SUAppcastAttributeInstallationType]; if (attributeInstallationType == nil) { // If we have a bare package, assume installation type is guided package // Otherwise assume we have a normal application inside an archive if ([_fileURL.pathExtension isEqualToString:@"pkg"] || [_fileURL.pathExtension isEqualToString:@"mpkg"]) { chosenInstallationType = SPUInstallationTypeGuidedPackage; } else { chosenInstallationType = SPUInstallationTypeApplication; } } else if (!SPUValidInstallationType(attributeInstallationType)) { if (error != NULL) { *error = [NSString stringWithFormat:@"Feed item's enclosure lacks valid %@ (found %@)", SUAppcastAttributeInstallationType, attributeInstallationType]; } return nil; } else { chosenInstallationType = attributeInstallationType; } #else chosenInstallationType = SPUInstallationTypeApplication; #endif _installationType = [chosenInstallationType copy]; NSString *enclosureDeltaSparkleExecutableSize = [enclosure objectForKey:SUAppcastAttributeDeltaFromSparkleExecutableSize]; if (enclosureDeltaSparkleExecutableSize != nil) { long long sparkleExecutableSize = [enclosureDeltaSparkleExecutableSize longLongValue]; if (sparkleExecutableSize > 0) { _deltaFromSparkleExecutableSize = @(sparkleExecutableSize); } } NSString *enclosureDeltaSparkleLocales = [enclosure objectForKey:SUAppcastAttributeDeltaFromSparkleLocales]; if (enclosureDeltaSparkleLocales != nil) { NSMutableSet<NSString *> *expectedLocales = [NSMutableSet set]; NSArray<NSString *> *locales = [enclosureDeltaSparkleLocales componentsSeparatedByString:@","]; NSUInteger localeIndex = 0; for (NSString *locale in locales) { if (locale.length != 0 && ![locale containsString:@"."] && ![locale containsString:@"/"]) { [expectedLocales addObject:locale]; localeIndex++; // Place an upper limit on the number of locales we process if (localeIndex >= DELTA_EXPECTED_LOCALES_LIMIT) { break; } } else { SULog(SULogLevelError, @"Ignoring expected delta locale '%@' because it contains a period or slash or is empty", locale); } } _deltaFromSparkleLocales = [expectedLocales copy]; } // Find the appropriate release notes URL. // Release notes is not to be trusted if signing validation on appcast failed NSDictionary *releaseNotesLinkDictionary = [dict objectForKey:SUAppcastElementReleaseNotesLink]; if (signingValidationStatus != SPUAppcastSigningValidationStatusFailed && releaseNotesLinkDictionary != nil) { NSString *releaseNotesString = [releaseNotesLinkDictionary objectForKey:@"content"]; if (releaseNotesString != nil) { NSURL *url; if (appcastURL != nil) { url = [NSURL URLWithString:releaseNotesString relativeToURL:appcastURL]; } else { url = [NSURL URLWithString:releaseNotesString]; } if ([url.scheme caseInsensitiveCompare:@"http"] == NSOrderedSame || [url.scheme caseInsensitiveCompare:@"https"] == NSOrderedSame) { _releaseNotesURL = url; } else { SULog(SULogLevelError, @"Release notes must have a http or https URL scheme."); _releaseNotesURL = nil; } } else if ([_itemDescription hasPrefix:@"http://"] || [_itemDescription hasPrefix:@"https://"]) { // if the description starts with http:// or https:// use that. _releaseNotesURL = [NSURL URLWithString:(NSString * _Nonnull)_itemDescription]; } else { _releaseNotesURL = nil; } _releaseNotesSignatures = [[SUSignatures alloc] initWithEd:[releaseNotesLinkDictionary objectForKey:SUAppcastAttributeEDSignature] #if SPARKLE_BUILD_LEGACY_DSA_SUPPORT dsa:nil #endif ]; long long releaseNotesLength = [(NSString *)[releaseNotesLinkDictionary objectForKey:@"length"] longLongValue]; _releaseNotesContentLength = (releaseNotesLength > 0) ? (uint64_t)releaseNotesLength : 0; } // Get full release notes URL if informed. // Full release notes is not to be trusted if signing validation on appcast failed NSString *fullReleaseNotesString = [dict objectForKey:SUAppcastElementFullReleaseNotesLink]; if (signingValidationStatus != SPUAppcastSigningValidationStatusFailed && fullReleaseNotesString != nil) { NSURL *url; if (appcastURL != nil) { url = [NSURL URLWithString:fullReleaseNotesString relativeToURL:appcastURL]; } else { url = [NSURL URLWithString:fullReleaseNotesString]; } if ([url.scheme caseInsensitiveCompare:@"http"] == NSOrderedSame || [url.scheme caseInsensitiveCompare:@"https"] == NSOrderedSame) { _fullReleaseNotesURL = url; } else { SULog(SULogLevelError, @"Full release notes must have a http or https URL scheme."); _fullReleaseNotesURL = nil; } } else { _fullReleaseNotesURL = nil; } NSArray *deltaDictionaries = [dict objectForKey:SUAppcastElementDeltas]; if (deltaDictionaries) { NSMutableDictionary *deltas = [NSMutableDictionary dictionary]; for (NSDictionary *deltaDictionary in deltaDictionaries) { NSString *deltaFrom = [deltaDictionary objectForKey:SUAppcastAttributeDeltaFrom]; if (!deltaFrom) continue; NSMutableDictionary *fakeAppCastDict = [dict mutableCopy]; [fakeAppCastDict removeObjectForKey:SUAppcastElementDeltas]; [fakeAppCastDict setObject:deltaDictionary forKey:SURSSElementEnclosure]; SUAppcastItem *deltaItem = [[SUAppcastItem alloc] initWithDictionary:fakeAppCastDict relativeToURL:appcastURL state:_state signingValidationStatus:_signingValidationStatus]; if (deltaItem != nil) { [deltas setObject:deltaItem forKey:deltaFrom]; } } _deltaUpdates = deltas; } } return self; } @end ================================================ FILE: Sparkle/SUApplicationInfo.h ================================================ // // SUApplicationInfo.h // Sparkle // // Created by Mayur Pawashe on 2/28/16. // Copyright © 2016 Sparkle Project. All rights reserved. // #if SPARKLE_BUILD_UI_BITS || !BUILDING_SPARKLE #import <Foundation/Foundation.h> NS_ASSUME_NONNULL_BEGIN @class SUHost, NSImage, NSApplication; SPU_OBJC_DIRECT_MEMBERS @interface SUApplicationInfo : NSObject + (BOOL)isBackgroundApplication:(NSApplication *)application; + (NSImage *)bestIconForHost:(SUHost *)host; @end NS_ASSUME_NONNULL_END #endif ================================================ FILE: Sparkle/SUApplicationInfo.m ================================================ // // SUApplicationInfo.m // Sparkle // // Created by Mayur Pawashe on 2/28/16. // Copyright © 2016 Sparkle Project. All rights reserved. // #if SPARKLE_BUILD_UI_BITS || !BUILDING_SPARKLE #import "SUApplicationInfo.h" #import "SUHost.h" #import <AppKit/AppKit.h> @implementation SUApplicationInfo + (BOOL)isBackgroundApplication:(NSApplication *)application { return (application.activationPolicy == NSApplicationActivationPolicyAccessory); } + (NSImage *)bestIconForHost:(SUHost *)host { BOOL isMainBundle = [host.bundle isEqualTo:[NSBundle mainBundle]]; // First try NSImageNameApplicationIcon. This image can be dynamically updated if the user's system icon settings change. NSImage *icon = isMainBundle ? [NSImage imageNamed:NSImageNameApplicationIcon] : nil; // Next try asking NSWorkspace for icon of the bundle if (icon == nil) { icon = [[NSWorkspace sharedWorkspace] iconForFile:host.bundlePath]; } return icon; } @end #endif ================================================ FILE: Sparkle/SUConstants.h ================================================ // // SUConstants.h // Sparkle // // Created by Andy Matuschak on 3/16/06. // Copyright 2006 Andy Matuschak. All rights reserved. // #ifndef SUCONSTANTS_H #define SUCONSTANTS_H #import <Foundation/Foundation.h> // ----------------------------------------------------------------------------- // Misc: // ----------------------------------------------------------------------------- extern NSString *const SUBundleIdentifier; extern NSString *const SUAppcastAttributeValueMacOS; // ----------------------------------------------------------------------------- // Notifications: // ----------------------------------------------------------------------------- extern NSString *const SUUpdateAutomaticCheckSettingChangedNotification; extern NSString *const SUUpdateSettingsNeedsSynchronizationNotification; extern NSString *const SUUpdateBundlePathUserInfoKey; // ----------------------------------------------------------------------------- // PList keys:: // ----------------------------------------------------------------------------- extern NSString *const SUFeedURLKey; extern NSString *const SUHasLaunchedBeforeKey; extern NSString *const SURelaunchHostBundleKey; extern NSString *const SUShowReleaseNotesKey; extern NSString *const SUSkippedMinorVersionKey; extern NSString *const SUSkippedMajorVersionKey; extern NSString *const SUSkippedMajorSubreleaseVersionKey; extern NSString *const SUScheduledCheckIntervalKey; extern NSString *const SUScheduledImpatientCheckIntervalKey; extern NSString *const SULastCheckTimeKey; extern NSString *const SUSignedFeedFailureExpirationIntervalKey; extern NSString *const SUPublicDSAKeyKey; extern NSString *const SUPublicDSAKeyFileKey; extern NSString *const SUPublicEDKeyKey; extern NSString *const SURequireSignedFeedKey; extern NSString *const SUVerifyUpdateBeforeExtractionKey; extern NSString *const SUAutomaticallyUpdateKey; extern NSString *const SUAllowsAutomaticUpdatesKey; extern NSString *const SUEnableAutomaticChecksKey; extern NSString *const SUEnableInstallerLauncherServiceKey; extern NSString *const SUEnableDownloaderServiceKey; extern NSString *const SUEnableInstallerConnectionServiceKey; extern NSString *const SUEnableInstallerStatusServiceKey; extern NSString *const SUEnableSystemProfilingKey; extern NSString *const SUSendProfileInfoKey; extern NSString *const SUUpdateGroupIdentifierKey; extern NSString *const SULastProfileSubmitDateKey; extern NSString *const SUPromptUserOnFirstLaunchKey; extern NSString *const SUDefaultsDomainKey; extern NSString *const SUEnableJavaScriptKey; extern NSString *const SUAllowedURLSchemesKey; // ----------------------------------------------------------------------------- // Appcast keys:: // ----------------------------------------------------------------------------- extern NSString *const SUAppcastAttributeDeltaFrom; extern NSString *const SUAppcastAttributeDeltaFromSparkleExecutableSize; extern NSString *const SUAppcastAttributeDeltaFromSparkleLocales; extern NSString *const SUAppcastAttributeDSASignature; extern NSString *const SUAppcastAttributeEDSignature; extern NSString *const SUAppcastAttributeShortVersionString; extern NSString *const SUAppcastAttributeVersion; extern NSString *const SUAppcastAttributeOsType; extern NSString *const SUAppcastAttributeInstallationType; extern NSString *const SUAppcastAttributeFormat; extern NSString *const SUAppcastAttributeLength; extern NSString *const SUAppcastElementVersion; extern NSString *const SUAppcastElementShortVersionString; extern NSString *const SUAppcastElementCriticalUpdate; extern NSString *const SUAppcastElementDeltas; extern NSString *const SUAppcastElementMinimumAutoupdateVersion; extern NSString *const SUAppcastElementMinimumSystemVersion; extern NSString *const SUAppcastElementMaximumSystemVersion; extern NSString *const SUAppcastElementMinimumUpdateVersion; extern NSString *const SUAppcastElementHardwareRequirements; extern NSString *const SUAppcastElementHardwareRequirementARM64; extern NSString *const SUAppcastElementReleaseNotesLink; extern NSString *const SUAppcastElementFullReleaseNotesLink; extern NSString *const SUAppcastElementTags; extern NSString *const SUAppcastElementPhasedRolloutInterval; extern NSString *const SUAppcastElementInformationalUpdate; extern NSString *const SUAppcastElementChannel; extern NSString *const SUAppcastElementBelowVersion; extern NSString *const SUAppcastElementIgnoreSkippedUpgradesBelowVersion; extern NSString *const SURSSAttributeURL; extern NSString *const SURSSAttributeLength; extern NSString *const SURSSElementDescription; extern NSString *const SURSSElementEnclosure; extern NSString *const SURSSElementLink; extern NSString *const SURSSElementPubDate; extern NSString *const SURSSElementTitle; extern NSString *const SUXMLLanguage; #endif ================================================ FILE: Sparkle/SUConstants.m ================================================ // // SUConstants.m // Sparkle // // Created by Andy Matuschak on 3/16/06. // Copyright 2006 Andy Matuschak. All rights reserved. // #import "SUConstants.h" #import "SUErrors.h" #include "AppKitPrevention.h" NSString *const SUBundleIdentifier = @SPARKLE_BUNDLE_IDENTIFIER; NSString *const SUAppcastAttributeValueMacOS = @"macos"; NSString *const SUUpdateAutomaticCheckSettingChangedNotification = @"SUUpdateAutomaticCheckSettingChanged"; NSString *const SUUpdateSettingsNeedsSynchronizationNotification = @"SUUpdateSettingsNeedsSynchronization"; NSString *const SUUpdateBundlePathUserInfoKey = @"SUBundlePath"; NSString *const SUFeedURLKey = @"SUFeedURL"; NSString *const SUHasLaunchedBeforeKey = @"SUHasLaunchedBefore"; NSString *const SURelaunchHostBundleKey = @"SURelaunchHostBundle"; NSString *const SUShowReleaseNotesKey = @"SUShowReleaseNotes"; NSString *const SUSkippedMinorVersionKey = @"SUSkippedVersion"; NSString *const SUSkippedMajorVersionKey = @"SUSkippedMajorVersion"; NSString *const SUSkippedMajorSubreleaseVersionKey = @"SUSkippedMajorSubreleaseVersion"; NSString *const SUScheduledCheckIntervalKey = @"SUScheduledCheckInterval"; NSString *const SUScheduledImpatientCheckIntervalKey = @"SUScheduledImpatientCheckInterval"; NSString *const SULastCheckTimeKey = @"SULastCheckTime"; NSString *const SUSignedFeedFailureExpirationIntervalKey = @"SUSignedFeedFailureExpirationInterval"; NSString *const SUPublicDSAKeyKey = @"SUPublicDSAKey"; NSString *const SUPublicDSAKeyFileKey = @"SUPublicDSAKeyFile"; NSString *const SUPublicEDKeyKey = @"SUPublicEDKey"; NSString *const SURequireSignedFeedKey = @"SURequireSignedFeed"; NSString *const SUVerifyUpdateBeforeExtractionKey = @"SUVerifyUpdateBeforeExtraction"; NSString *const SUAutomaticallyUpdateKey = @"SUAutomaticallyUpdate"; NSString *const SUAllowsAutomaticUpdatesKey = @"SUAllowsAutomaticUpdates"; NSString *const SUEnableSystemProfilingKey = @"SUEnableSystemProfiling"; NSString *const SUEnableAutomaticChecksKey = @"SUEnableAutomaticChecks"; NSString *const SUEnableInstallerLauncherServiceKey = @"SUEnableInstallerLauncherService"; NSString *const SUEnableDownloaderServiceKey = @"SUEnableDownloaderService"; NSString *const SUEnableInstallerConnectionServiceKey = @"SUEnableInstallerConnectionService"; NSString *const SUEnableInstallerStatusServiceKey = @"SUEnableInstallerStatusService"; NSString *const SUSendProfileInfoKey = @"SUSendProfileInfo"; NSString *const SUUpdateGroupIdentifierKey = @"SUUpdateGroupIdentifier"; NSString *const SULastProfileSubmitDateKey = @"SULastProfileSubmissionDate"; NSString *const SUPromptUserOnFirstLaunchKey = @"SUPromptUserOnFirstLaunch"; NSString *const SUEnableJavaScriptKey = @"SUEnableJavaScript"; NSString *const SUAllowedURLSchemesKey = @"SUAllowedURLSchemes"; NSString *const SUDefaultsDomainKey = @"SUDefaultsDomain"; NSString *const SUSparkleErrorDomain = @"SUSparkleErrorDomain"; NSString *const SPUNoUpdateFoundReasonKey = @"SUNoUpdateFoundReason"; NSString *const SPUNoUpdateFoundUserInitiatedKey = @"SPUNoUpdateUserInitiated"; NSString *const SPULatestAppcastItemFoundKey = @"SULatestAppcastItemFound"; NSString *const SUAppcastAttributeDeltaFrom = @"sparkle:deltaFrom"; NSString *const SUAppcastAttributeDeltaFromSparkleExecutableSize = @"sparkle:deltaFromSparkleExecutableSize"; NSString *const SUAppcastAttributeDeltaFromSparkleLocales = @"sparkle:deltaFromSparkleLocales"; #if SPARKLE_BUILD_LEGACY_DSA_SUPPORT || GENERATE_APPCAST_BUILD_LEGACY_DSA_SUPPORT NSString *const SUAppcastAttributeDSASignature = @"sparkle:dsaSignature"; #endif NSString *const SUAppcastAttributeEDSignature = @"sparkle:edSignature"; NSString *const SUAppcastAttributeShortVersionString = @"sparkle:shortVersionString"; NSString *const SUAppcastAttributeVersion = @"sparkle:version"; NSString *const SUAppcastAttributeOsType = @"sparkle:os"; NSString *const SUAppcastAttributeInstallationType = @"sparkle:installationType"; NSString *const SUAppcastAttributeFormat = @"sparkle:format"; NSString *const SUAppcastAttributeLength = @"sparkle:length"; NSString *const SUAppcastElementVersion = SUAppcastAttributeVersion; NSString *const SUAppcastElementShortVersionString = SUAppcastAttributeShortVersionString; NSString *const SUAppcastElementCriticalUpdate = @"sparkle:criticalUpdate"; NSString *const SUAppcastElementDeltas = @"sparkle:deltas"; NSString *const SUAppcastElementMinimumAutoupdateVersion = @"sparkle:minimumAutoupdateVersion"; NSString *const SUAppcastElementMinimumSystemVersion = @"sparkle:minimumSystemVersion"; NSString *const SUAppcastElementMaximumSystemVersion = @"sparkle:maximumSystemVersion"; NSString *const SUAppcastElementMinimumUpdateVersion = @"sparkle:minimumUpdateVersion"; NSString *const SUAppcastElementHardwareRequirements = @"sparkle:hardwareRequirements"; NSString *const SUAppcastElementHardwareRequirementARM64 = @"arm64"; NSString *const SUAppcastElementReleaseNotesLink = @"sparkle:releaseNotesLink"; NSString *const SUAppcastElementFullReleaseNotesLink = @"sparkle:fullReleaseNotesLink"; NSString *const SUAppcastElementTags = @"sparkle:tags"; NSString *const SUAppcastElementPhasedRolloutInterval = @"sparkle:phasedRolloutInterval"; NSString *const SUAppcastElementInformationalUpdate = @"sparkle:informationalUpdate"; NSString *const SUAppcastElementChannel = @"sparkle:channel"; NSString *const SUAppcastElementBelowVersion = @"sparkle:belowVersion"; NSString *const SUAppcastElementIgnoreSkippedUpgradesBelowVersion = @"sparkle:ignoreSkippedUpgradesBelowVersion"; NSString *const SURSSAttributeURL = @"url"; NSString *const SURSSAttributeLength = @"length"; NSString *const SURSSElementDescription = @"description"; NSString *const SURSSElementEnclosure = @"enclosure"; NSString *const SURSSElementLink = @"link"; NSString *const SURSSElementPubDate = @"pubDate"; NSString *const SURSSElementTitle = @"title"; NSString *const SUXMLLanguage = @"xml:lang"; ================================================ FILE: Sparkle/SUErrors.h ================================================ // // 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 <Foundation/Foundation.h> #if defined(BUILDING_SPARKLE_SOURCES_EXTERNALLY) // Ignore incorrect warning #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wquoted-include-in-framework-header" #import "SUExport.h" #pragma clang diagnostic pop #else #import <Sparkle/SUExport.h> #endif /** * Error domain used by Sparkle */ SU_EXPORT extern NSString *const SUSparkleErrorDomain; typedef NS_ENUM(OSStatus, SUError) { // Configuration phase errors SUNoPublicDSAFoundError = 0001, SUInsufficientSigningError = 0002, SUInsecureFeedURLError = 0003, SUInvalidFeedURLError = 0004, SUInvalidUpdaterError = 0005, SUInvalidHostBundleIdentifierError = 0006, SUInvalidHostVersionError = 0007, // Appcast phase errors. SUAppcastParseError = 1000, SUNoUpdateError = 1001, SUAppcastError = 1002, SURunningFromDiskImageError = 1003, SUResumeAppcastError = 1004, SURunningTranslocated = 1005, SUWebKitTerminationError = 1006, SUReleaseNotesError = 1007, // Download phase errors. SUTemporaryDirectoryError = 2000, SUDownloadError = 2001, // Extraction phase errors. SUUnarchivingError = 3000, SUSignatureError = 3001, SUValidationError = 3002, // Installation phase errors. SUFileCopyFailure = 4000, SUAuthenticationFailure = 4001, SUMissingUpdateError = 4002, SUMissingInstallerToolError = 4003, SURelaunchError = 4004, SUInstallationError = 4005, SUDowngradeError = 4006, SUInstallationCanceledError = 4007, SUInstallationAuthorizeLaterError = 4008, SUNotValidUpdateError = 4009, SUAgentInvalidationError = 4010, //SUInstallationRootInteractiveError = 4011, SUInstallationWriteNoPermissionError = 4012, // API misuse errors. SUIncorrectAPIUsageError = 5000 }; /** The reason why a new update is not available. */ typedef NS_ENUM(OSStatus, SPUNoUpdateFoundReason) { /** A new update is unavailable for an unknown reason. */ SPUNoUpdateFoundReasonUnknown, /** A new update is unavailable because the user is on the latest known version in the appcast feed. */ SPUNoUpdateFoundReasonOnLatestVersion, /** A new update is unavailable because the user is on a version newer than the latest known version in the appcast feed. */ SPUNoUpdateFoundReasonOnNewerThanLatestVersion, /** A new update is unavailable because the user's operating system version is too old for the update. */ SPUNoUpdateFoundReasonSystemIsTooOld, /** A new update is unavailable because the user's operating system version is too new for the update. */ SPUNoUpdateFoundReasonSystemIsTooNew, /** A new update is unavailable because the user's system is an Intel Mac that doesn't support ARM64. */ SPUNoUpdateFoundReasonHardwareDoesNotSupportARM64, }; SU_EXPORT extern NSString *const SPUNoUpdateFoundReasonKey; SU_EXPORT extern NSString *const SPULatestAppcastItemFoundKey; SU_EXPORT extern NSString *const SPUNoUpdateFoundUserInitiatedKey; #endif ================================================ FILE: Sparkle/SUExport.h ================================================ // // SUExport.h // Sparkle // // Created by Jake Petroules on 2014-08-23. // Copyright (c) 2014 Sparkle Project. All rights reserved. // #ifndef SUEXPORT_H #define SUEXPORT_H #ifdef BUILDING_SPARKLE #define SU_EXPORT __attribute__((visibility("default"))) #else #define SU_EXPORT #endif #endif ================================================ FILE: Sparkle/SUFileManager.h ================================================ // // SUFileManager.h // Sparkle // // Created by Mayur Pawashe on 7/18/15. // Copyright (c) 2015 zgcoder. All rights reserved. // #import <Foundation/Foundation.h> #import "SUExport.h" NS_ASSUME_NONNULL_BEGIN #ifndef BUILDING_SPARKLE_TESTS #define SUFileManagerDefinitionAttribute SPU_OBJC_DIRECT_MEMBERS #else #define SUFileManagerDefinitionAttribute __attribute__((objc_runtime_name("SUTestFileManager"))) #endif /** * A class used for performing file operations more suitable than NSFileManager for performing installation work. * All operations on this class may be used on thread other than the main thread. * This class provides just basic file operations and stays away from including much application-level logic. */ SUFileManagerDefinitionAttribute @interface SUFileManager : NSObject /** * Creates a temporary directory on the same volume as a provided URL * @param appropriateURL A URL to a directory that resides on the volume that the temporary directory will be created on. In the uncommon case, the temporary directory may be created inside this directory. * @param error If an error occurs, upon returns contains an NSError object that describes the problem. If you are not interested in possible errors, you may pass in NULL. * @return A URL pointing to the newly created temporary directory, or nil with a populated error object if an error occurs. * * When moving an item from a source to a destination, it is desirable to create a temporary intermediate destination on the same volume as the destination to ensure * that the item will be moved, and not copied, from the intermediate point to the final destination. This ensures file atomicity. */ - (NSURL * _Nullable)makeTemporaryDirectoryAppropriateForDirectoryURL:(NSURL *)appropriateURL error:(NSError * __autoreleasing *)error; /** * Creates a directory at the target URL * @param targetURL A URL pointing to the directory to create. The item at this URL must not exist, and the parent directory of this URL must already exist. * @param error If an error occurs, upon returns contains an NSError object that describes the problem. If you are not interested in possible errors, you may pass in NULL. * @return YES if the item was created successfully, otherwise NO along with a populated error object * * This is an atomic operation. */ - (BOOL)makeDirectoryAtURL:(NSURL *)targetURL error:(NSError **)error; /** * Moves an item from a source to a destination * @param sourceURL A URL pointing to the item to move. The item at this URL must exist. * @param destinationURL A URL pointing to the destination the item will be moved at. An item must not already exist at this URL. * @param error If an error occurs, upon returns contains an NSError object that describes the problem. If you are not interested in possible errors, you may pass in NULL. * @return YES if the item was moved successfully, otherwise NO along with a populated error object * * If sourceURL and destinationURL reside on the same volume, this operation will be an atomic move operation. * Otherwise this will be equivalent to a copy & remove which will be a nonatomic operation. */ - (BOOL)moveItemAtURL:(NSURL *)sourceURL toURL:(NSURL *)destinationURL error:(NSError **)error; /** * Swaps an original item with a new item atomically. * @param originalItemURL A URL pointing to the original item to replace. The item at this URL must exist. * @param newItemURL A URL pointing to the new item that will replace the original item. * @param error If an error occurs, upon returns contains an NSError object that describes the problem. If you are not interested in possible errors, you may pass in NULL. * @return YES if the original item was replaced with the new item successfully, otherwise NO along with a populated error object * * originalItemURL and newItemURL must reside on the same volume. If the operation succeeds, this will be be an atomic operation. * Otherwise on failure you may need to re-try using move operations. This operation will fail on non-apfs volumes or volumes that don't support rename swapping. * Both originalItemURL and newItemURL must exist. */ - (BOOL)swapItemAtURL:(NSURL *)originalItemURL withItemAtURL:(NSURL *)newItemURL error:(NSError **)error; /** * Checks if two URLs are on the same volume. * @param url1 A URL pointing to the first item * @param url2 A URL pointing to the second item * @return YES if both URLs are on the same volume, otherwise NO * * If any volume retrieval error occurs during the process, this method assumes both items are on the same volume (which is the common case). */ - (BOOL)itemAtURL:(NSURL *)url1 isOnSameVolumeItemAsURL:(NSURL *)url2; /** * Copies an item from a source to a destination * @param sourceURL A URL pointing to the item to move. The item at this URL must exist. * @param destinationURL A URL pointing to the destination the item will be moved at. An item must not already exist at this URL. * @param error If an error occurs, upon returns contains an NSError object that describes the problem. If you are not interested in possible errors, you may pass in NULL. * @return YES if the item was copied successfully, otherwise NO along with a populated error object * * This is not an atomic operation. */ - (BOOL)copyItemAtURL:(NSURL *)sourceURL toURL:(NSURL *)destinationURL error:(NSError **)error; /** * Removes an item at a URL * @param url A URL pointing to the item to remove. The item at this URL must exist. * @param error If an error occurs, upon returns contains an NSError object that describes the problem. If you are not interested in possible errors, you may pass in NULL. * @return YES if the item was removed successfully, otherwise NO along with a populated error object * * This is not an atomic operation. */ - (BOOL)removeItemAtURL:(NSURL *)url error:(NSError **)error; /** * Changes the owner and group IDs of an item at a specified target URL to match another URL * @param targetURL A URL pointing to the target item whose owner and group IDs to alter. This will be applied recursively if the item is a directory. The item at this URL must exist. * @param matchURL A URL pointing to the item whose owner and group IDs will be used for changing on the targetURL. The item at this URL must exist. * @param error If an error occurs, upon returns contains an NSError object that describes the problem. If you are not interested in possible errors, you may pass in NULL. * @return YES if the target item's owner and group IDs have changed to match the origin's ones, otherwise NO along with a populated error object * * If the owner and group IDs match on the root items of targetURL and matchURL, this method stops and assumes that nothing needs to be done. * Otherwise this method recursively changes the IDs if the target is a directory. If an item in the directory is encountered that is unable to be changed, * then this method stops and returns NO. * While this method will try to change the group ID, being unable to change the group ID does not result in a failure if the owner ID can be changed or matched. * * This is not an atomic operation. */ - (BOOL)changeOwnerAndGroupOfItemAtRootURL:(NSURL *)targetURL toMatchURL:(NSURL *)matchURL error:(NSError **)error; /** Changes the owner and group ID of an item at a specified target URL @param targetURL A URL pointing to the target item whose owner and group IDs to alter. The item at this URL must exist. @param ownerID The new owner ID to set on the item. @param groupID The new group ID to set on the item. @param error If an error occurs, upon returns contains an NSError object that describes the problem. If you are not interested in possible errors, you may pass in NULL. @return YES if the target item's owner and group IDs have changed, otherwise NO along with a populated error object. Unlike -changeOwnerAndGroupOfItemAtRootURL:toMatchURL:error: this method does not recursively try to change the owner and group IDs if the target item is a directory. */ - (BOOL)changeOwnerAndGroupOfItemAtURL:(NSURL *)targetURL ownerID:(uid_t)ownerID groupID:(gid_t)groupID error:(NSError * __autoreleasing *)error; /** * Updates the modification and access time of an item at a specified target URL to the current time * @param targetURL A URL pointing to the target item whose modification and access time to update. The item at this URL must exist. * @param error If an error occurs, upon returns contains an NSError object that describes the problem. If you are not interested in possible errors, you may pass in NULL. * @return YES if the target item's modification and access times have been updated, otherwise NO along with a populated error object * * This method updates the modification and access time of an item to the current time, ideal for letting the system know we installed a new file or * application. * * This is not an atomic operation. */ - (BOOL)updateModificationAndAccessTimeOfItemAtURL:(NSURL *)targetURL error:(NSError **)error; /** * Updates the access time of an item at a specified root URL to the current time * @param targetURL A URL pointing to the target item whose access time to update to the current time. * This will be applied recursively if the item is a directory. The item at this URL must exist. * @param error If an error occurs, upon returns contains an NSError object that describes the problem. If you are not interested in possible errors, you may pass in NULL. * @return YES if the target item's access times have been updated, otherwise NO along with a populated error object * * This method updates the access time of an item to the current time, ideal for letting the system know not to remove a file or directory when placing it * at a temporary directory. * * This is not an atomic operation. */ - (BOOL)updateAccessTimeOfItemAtRootURL:(NSURL *)targetURL error:(NSError * __autoreleasing *)error; /** * Releases Apple's quarantine extended attribute from the item at the specified root URL * @param rootURL A URL pointing to the item to release from Apple's quarantine. This will be applied recursively if the item is a directory. The item at this URL must exist. * @param error If an error occurs, upon returns contains an NSError object that describes the problem. If you are not interested in possible errors, you may pass in NULL. * @return YES if all the items at the target could be released from quarantine, otherwise NO if any items couldn't along with a populated error object * * This method removes quarantine attributes from an item, ideally an application, so that when the user launches a new application themselves, they * don't have to witness the system dialog alerting them that they downloaded an application from the internet and asking if they want to continue. * Note that this may not exactly mimic the system behavior when a user opens an application for the first time (i.e, the xattr isn't deleted), * but this should be sufficient enough for our purposes. * * This method may return NO even if some items do get released from quarantine if the target URL is pointing to a directory. * Thus if an item cannot be released from quarantine, this method still continues on to the next enumerated item. * * This is not an atomic operation. */ - (BOOL)releaseItemFromQuarantineAtRootURL:(NSURL *)rootURL error:(NSError **)error; @end NS_ASSUME_NONNULL_END ================================================ FILE: Sparkle/SUFileManager.m ================================================ // // SUFileManager.m // Sparkle // // Created by Mayur Pawashe on 7/18/15. // Copyright (c) 2015 zgcoder. All rights reserved. // #import "SUFileManager.h" #import "SUErrors.h" #include <sys/xattr.h> #include <sys/errno.h> #include <sys/time.h> #include <sys/stat.h> #include "AppKitPrevention.h" extern int renamex_np(const char *, const char *, unsigned int) __attribute__((weak_import)); static char SUAppleQuarantineIdentifier[] = "com.apple.quarantine"; @implementation SUFileManager { NSFileManager *_fileManager; } - (instancetype)init { self = [super init]; if (self != nil) { _fileManager = [[NSFileManager alloc] init]; } return self; } // -[NSFileManager attributesOfItemAtPath:error:] won't follow symbolic links - (BOOL)_itemExistsAtURL:(NSURL *)fileURL #ifndef BUILDING_SPARKLE_TESTS SPU_OBJC_DIRECT #endif { NSString *path = fileURL.path; if (path == nil) { return NO; } return [_fileManager attributesOfItemAtPath:path error:NULL] != nil; } - (BOOL)_itemExistsAtURL:(NSURL *)fileURL isDirectory:(BOOL *)isDirectory #ifndef BUILDING_SPARKLE_TESTS SPU_OBJC_DIRECT #endif { NSString *path = fileURL.path; if (path == nil) { return NO; } NSDictionary *attributes = [_fileManager attributesOfItemAtPath:path error:NULL]; if (attributes == nil) { return NO; } if (isDirectory != NULL) { *isDirectory = [(NSString *)[attributes objectForKey:NSFileType] isEqualToString:NSFileTypeDirectory]; } return YES; } // Wrapper around getxattr() - (ssize_t)_getXAttr:(const char *)name fromFile:(NSString *)file options:(int)options SPU_OBJC_DIRECT { char path[PATH_MAX] = {0}; if (![file getFileSystemRepresentation:path maxLength:sizeof(path)]) { errno = 0; return -1; } return getxattr(path, name, NULL, 0, 0, options); } // Wrapper around removexattr() - (int)_removeXAttr:(const char *)attr fromFile:(NSString *)file options:(int)options SPU_OBJC_DIRECT { char path[PATH_MAX] = {0}; if (![file getFileSystemRepresentation:path maxLength:sizeof(path)]) { errno = 0; return -1; } return removexattr(path, attr, options); } // Removes the directory tree rooted at |root| from the file quarantine. // The quarantine was introduced on macOS 10.5 and is described at: // // http://developer.apple.com/releasenotes/Carbon/RN-LaunchServices/index.html#apple_ref/doc/uid/TP40001369-DontLinkElementID_2 // // If |root| is not a directory, then it alone is removed from the quarantine. // Symbolic links, including |root| if it is a symbolic link, will not be // traversed. // Ordinarily, the quarantine is managed by calling LSSetItemAttribute // to set the kLSItemQuarantineProperties attribute to a dictionary specifying // the quarantine properties to be applied. However, it does not appear to be // possible to remove an item from the quarantine directly through any public // Launch Services calls. Instead, this method takes advantage of the fact // that the quarantine is implemented in part by setting an extended attribute, // "com.apple.quarantine", on affected files. Removing this attribute is // sufficient to remove files from the quarantine. // This works by removing the quarantine extended attribute for every file we come across. // We used to have code similar to the method below that used -[NSURL getResourceValue:forKey:error:] and -[NSURL setResourceValue:forKey:error:] // However, those methods *really suck* - you can't rely on the return value from getting the resource value and if you set the resource value // when the key isn't present, errors are spewed out to the console - (BOOL)releaseItemFromQuarantineAtRootURL:(NSURL *)rootURL error:(NSError *__autoreleasing *)error { static const int removeXAttrOptions = XATTR_NOFOLLOW; BOOL success = YES; // First remove quarantine on the root item NSString *rootURLPath = rootURL.path; if ([self _getXAttr:SUAppleQuarantineIdentifier fromFile:rootURLPath options:removeXAttrOptions] >= 0) { BOOL removedRootQuarantine = ([self _removeXAttr:SUAppleQuarantineIdentifier fromFile:rootURLPath options:removeXAttrOptions] == 0); if (!removedRootQuarantine) { success = NO; if (error != NULL) { *error = [NSError errorWithDomain:NSPOSIXErrorDomain code:errno userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Failed to remove file quarantine on %@.", rootURL.lastPathComponent] }]; } } } // Only recurse if it's actually a directory. Don't recurse into a root-level symbolic link. // Even if we fail removing the quarantine from the root item or any single item in the directory, we will continue trying to remove the quarantine. // This is because often it may not be a fatal error from the caller to not remove the quarantine of an item NSDictionary *rootAttributes = [_fileManager attributesOfItemAtPath:rootURLPath error:nil]; NSString *rootType = rootAttributes[NSFileType]; if ([rootType isEqualToString:NSFileTypeDirectory]) { // The NSDirectoryEnumerator will avoid recursing into any contained // symbolic links, so no further type checks are needed. NSDirectoryEnumerator *directoryEnumerator = [_fileManager enumeratorAtURL:rootURL includingPropertiesForKeys:nil options:(NSDirectoryEnumerationOptions)0 errorHandler:nil]; for (NSURL *fileURL in directoryEnumerator) { if ([self _getXAttr:SUAppleQuarantineIdentifier fromFile:fileURL.path options:removeXAttrOptions] >= 0) { BOOL removedQuarantine = ([self _removeXAttr:SUAppleQuarantineIdentifier fromFile:fileURL.path options:removeXAttrOptions] == 0); if (!removedQuarantine && success) { success = NO; if (error != NULL) { *error = [NSError errorWithDomain:NSPOSIXErrorDomain code:errno userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Failed to remove file quarantine on %@.", fileURL.lastPathComponent] }]; } } } } } return success; } - (BOOL)copyItemAtURL:(NSURL *)sourceURL toURL:(NSURL *)destinationURL error:(NSError * __autoreleasing *)error { return [_fileManager copyItemAtURL:sourceURL toURL:destinationURL error:error]; } - (BOOL)_getVolumeID:(out id _Nullable __autoreleasing * _Nonnull)outVolumeIdentifier ofItemAtURL:(NSURL *)url SPU_OBJC_DIRECT { NSError *error = nil; return [url getResourceValue:outVolumeIdentifier forKey:NSURLVolumeIdentifierKey error:&error]; } - (BOOL)itemAtURL:(NSURL *)url1 isOnSameVolumeItemAsURL:(NSURL *)url2 { id volumeIdentifier1 = nil; BOOL foundVolume1 = [self _getVolumeID:&volumeIdentifier1 ofItemAtURL:url1]; id volumeIdentifier2 = nil; BOOL foundVolume2 = [self _getVolumeID:&volumeIdentifier2 ofItemAtURL:url2]; if (foundVolume1 && foundVolume2 && ![(NSObject *)volumeIdentifier1 isEqual:volumeIdentifier2]) { return NO; } else { return YES; } } - (BOOL)moveItemAtURL:(NSURL *)sourceURL toURL:(NSURL *)destinationURL error:(NSError *__autoreleasing *)error { if (![self _itemExistsAtURL:sourceURL]) { if (error != NULL) { *error = [NSError errorWithDomain:NSCocoaErrorDomain code:NSFileNoSuchFileError userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Source file to move (%@) does not exist.", sourceURL.lastPathComponent] }]; } return NO; } NSURL *destinationURLParent = destinationURL.URLByDeletingLastPathComponent; if (![self _itemExistsAtURL:destinationURLParent]) { if (error != NULL) { *error = [NSError errorWithDomain:NSCocoaErrorDomain code:NSFileNoSuchFileError userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Destination parent directory to move into (%@) does not exist.", destinationURLParent.lastPathComponent] }]; } return NO; } if ([self _itemExistsAtURL:destinationURL]) { if (error != NULL) { *error = [NSError errorWithDomain:NSCocoaErrorDomain code:NSFileWriteFileExistsError userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Destination file to move (%@) already exists.", destinationURL.lastPathComponent] }]; } return NO; } // If the source and destination are on different volumes, we should not do a move; // from my experience a move may fail when moving particular files from // one network mount to another one. This is possibly related to the fact that // moving a file will try to preserve ownership but copying won't if (![self itemAtURL:sourceURL isOnSameVolumeItemAsURL:destinationURLParent]) { return ([self copyItemAtURL:sourceURL toURL:destinationURL error:error] && [self removeItemAtURL:sourceURL error:error]); } return [_fileManager moveItemAtURL:sourceURL toURL:destinationURL error:error]; } - (BOOL)swapItemAtURL:(NSURL *)originalItemURL withItemAtURL:(NSURL *)newItemURL error:(NSError * __autoreleasing *)error { char originalPath[PATH_MAX] = {0}; if (![originalItemURL.path getFileSystemRepresentation:originalPath maxLength:sizeof(originalPath)]) { if (error != NULL) { *error = [NSError errorWithDomain:NSCocoaErrorDomain code:NSFileReadInvalidFileNameError userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Original item %@ to replace cannot be represented as a valid file name", originalItemURL.lastPathComponent] }]; } return NO; } char newPath[PATH_MAX] = {0}; if (![newItemURL.path getFileSystemRepresentation:newPath maxLength:sizeof(newPath)]) { if (error != NULL) { *error = [NSError errorWithDomain:NSCocoaErrorDomain code:NSFileReadInvalidFileNameError userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"New item %@ to replace cannot be represented as a valid file name", newItemURL.lastPathComponent] }]; } return NO; } int status = renamex_np(newPath, originalPath, RENAME_SWAP); if (status != 0) { if (error != NULL) { *error = [NSError errorWithDomain:NSPOSIXErrorDomain code:errno userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Failed to replace %@ with %@.", originalItemURL.lastPathComponent, newItemURL.lastPathComponent] }]; } return NO; } return YES; } - (BOOL)changeOwnerAndGroupOfItemAtURL:(NSURL *)targetURL ownerID:(uid_t)ownerID groupID:(gid_t)groupID error:(NSError * __autoreleasing *)error { char path[PATH_MAX] = {0}; if (![targetURL.path getFileSystemRepresentation:path maxLength:sizeof(path)]) { if (error != NULL) { *error = [NSError errorWithDomain:NSCocoaErrorDomain code:NSFileReadInvalidFileNameError userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"File to change owner & group (%@) cannot be represented as a valid file name.", targetURL.path.lastPathComponent] }]; } return NO; } int fileDescriptor = open(path, O_RDONLY | O_SYMLINK); if (fileDescriptor == -1) { if (error != NULL) { *error = [NSError errorWithDomain:NSPOSIXErrorDomain code:errno userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Failed to open file descriptor to %@", targetURL.path.lastPathComponent] }]; } return NO; } // We use fchown instead of chown because the latter can follow symbolic links BOOL success = (fchown(fileDescriptor, ownerID, groupID) == 0); close(fileDescriptor); if (!success) { if (error != NULL) { *error = [NSError errorWithDomain:NSPOSIXErrorDomain code:errno userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Failed to change owner & group for %@ with owner ID %u and group ID %u.", targetURL.path.lastPathComponent, ownerID, groupID] }]; } } return success; } - (BOOL)changeOwnerAndGroupOfItemAtRootURL:(NSURL *)targetURL toMatchURL:(NSURL *)matchURL error:(NSError * __autoreleasing *)error { BOOL isTargetADirectory = NO; if (![self _itemExistsAtURL:targetURL isDirectory:&isTargetADirectory]) { if (error != NULL) { *error = [NSError errorWithDomain:NSCocoaErrorDomain code:NSFileNoSuchFileError userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Failed to change owner & group IDs because %@ does not exist.", targetURL.path.lastPathComponent] }]; } return NO; } if (![self _itemExistsAtURL:matchURL]) { if (error != NULL) { *error = [NSError errorWithDomain:NSCocoaErrorDomain code:NSFileNoSuchFileError userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Failed to match owner & group IDs because %@ does not exist.", matchURL.path.lastPathComponent] }]; } return NO; } NSError *matchFileAttributesError = nil; NSString *matchURLPath = matchURL.path; NSDictionary *matchFileAttributes = [_fileManager attributesOfItemAtPath:matchURLPath error:&matchFileAttributesError]; if (matchFileAttributes == nil) { if (error != NULL) { *error = matchFileAttributesError; } return NO; } NSError *targetFileAttributesError = nil; NSString *targetURLPath = targetURL.path; NSDictionary *targetFileAttributes = [_fileManager attributesOfItemAtPath:targetURLPath error:&targetFileAttributesError]; if (targetFileAttributes == nil) { if (error != NULL) { *error = targetFileAttributesError; } return NO; } NSNumber *ownerID = [matchFileAttributes objectForKey:NSFileOwnerAccountID]; if (ownerID == nil) { // shouldn't be possible to error here, but just in case if (error != NULL) { *error = [NSError errorWithDomain:NSCocoaErrorDomain code:NSFileReadNoPermissionError userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Owner ID could not be read from %@.", matchURL.path.lastPathComponent] }]; } return NO; } NSNumber *groupID = [matchFileAttributes objectForKey:NSFileGroupOwnerAccountID]; if (groupID == nil) { // shouldn't be possible to error here, but just in case if (error != NULL) { *error = [NSError errorWithDomain:NSCocoaErrorDomain code:NSFileReadNoPermissionError userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Group ID could not be read from %@.", matchURL.path.lastPathComponent] }]; } return NO; } NSNumber *targetOwnerID = [targetFileAttributes objectForKey:NSFileOwnerAccountID]; NSNumber *targetGroupID = [targetFileAttributes objectForKey:NSFileGroupOwnerAccountID]; if ((targetOwnerID != nil && [ownerID isEqualToNumber:targetOwnerID]) && (targetGroupID != nil && [groupID isEqualToNumber:targetGroupID])) { // Assume they're the same even if we don't check every file recursively // Speeds up the common case return YES; } // If we can't change both the new owner & group, try to only change the owner // If this works, this is sufficient enough for performing the update NSNumber *groupIDToUse; if (![self changeOwnerAndGroupOfItemAtURL:targetURL ownerID:ownerID.unsignedIntValue groupID:groupID.unsignedIntValue error:NULL]) { if ((targetOwnerID != nil && [ownerID isEqualToNumber:targetOwnerID])) { // Assume they're the same even if we don't check every file recursively // Speeds up the common case like above return YES; } if (![self changeOwnerAndGroupOfItemAtURL:targetURL ownerID:ownerID.unsignedIntValue groupID:targetGroupID.unsignedIntValue error:error]) { return NO; } groupIDToUse = targetGroupID; } else { groupIDToUse = groupID; } if (isTargetADirectory) { NSDirectoryEnumerator *directoryEnumerator = [_fileManager enumeratorAtURL:targetURL includingPropertiesForKeys:nil options:(NSDirectoryEnumerationOptions)0 errorHandler:nil]; for (NSURL *url in directoryEnumerator) { if (![self changeOwnerAndGroupOfItemAtURL:url ownerID:ownerID.unsignedIntValue groupID:groupIDToUse.unsignedIntValue error:error]) { return NO; } } } return YES; } - (BOOL)_updateItemAtURL:(NSURL *)targetURL withAccessTime:(struct timeval)accessTime error:(NSError * __autoreleasing *)error SPU_OBJC_DIRECT { char path[PATH_MAX] = {0}; // NOTE: At least on Mojave 10.14.1, running on an APFS filesystem, the act of asking // for a path's file system representation causes the access time of the containing folder // to be updated. Callers should take care when attempting to set a recursive directory's // access time to ensure that the inner-most items get set first, so that the implicitly // updated access times are replaced after this side-effect occurs. if (![targetURL.path getFileSystemRepresentation:path maxLength:sizeof(path)]) { if (error != NULL) { *error = [NSError errorWithDomain:NSCocoaErrorDomain code:NSFileReadInvalidFileNameError userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"File to update modification & access time (%@) cannot be represented as a valid file name.", targetURL.path.lastPathComponent] }]; } return NO; } int fileDescriptor = open(path, O_RDONLY | O_SYMLINK); if (fileDescriptor == -1) { if (error != NULL) { *error = [NSError errorWithDomain:NSPOSIXErrorDomain code:errno userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Failed to open file descriptor to %@", targetURL.path.lastPathComponent] }]; } return NO; } struct stat statInfo; if (fstat(fileDescriptor, &statInfo) != 0) { if (error != NULL) { *error = [NSError errorWithDomain:NSPOSIXErrorDomain code:errno userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Failed to stat file descriptor to %@", targetURL.path.lastPathComponent] }]; } close(fileDescriptor); return NO; } // Preserve the modification time struct timeval modTime; TIMESPEC_TO_TIMEVAL(&modTime, &statInfo.st_mtimespec) const struct timeval timeInputs[] = {accessTime, modTime}; // Using futimes() because utimes() follows symbolic links BOOL updatedTime = (futimes(fileDescriptor, timeInputs) == 0); close(fileDescriptor); if (!updatedTime) { if (error != NULL) { *error = [NSError errorWithDomain:NSPOSIXErrorDomain code:errno userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Failed to update modification & access time for %@", targetURL.path.lastPathComponent] }]; } return NO; } return YES; } - (BOOL)updateAccessTimeOfItemAtRootURL:(NSURL *)targetURL error:(NSError * __autoreleasing *)error { if (![self _itemExistsAtURL:targetURL]) { if (error != NULL) { *error = [NSError errorWithDomain:NSCocoaErrorDomain code:NSFileNoSuchFileError userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Failed to update modification & access time recursively because %@ does not exist.", targetURL.path.lastPathComponent] }]; } return NO; } // We want to update all files with the same exact time struct timeval currentTime = {0, 0}; if (gettimeofday(¤tTime, NULL) != 0) { if (error != NULL) { *error = [NSError errorWithDomain:NSPOSIXErrorDomain code:errno userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Failed to update modification & access time recursively because gettimeofday failed."] }]; } return NO; } NSString *rootURLPath = targetURL.path; NSDictionary *rootAttributes = [_fileManager attributesOfItemAtPath:rootURLPath error:nil]; NSString *rootType = [rootAttributes objectForKey:NSFileType]; // Only recurse if it's actually a directory. Don't recurse into a // root-level symbolic link. if ([rootType isEqualToString:NSFileTypeDirectory]) { // The NSDirectoryEnumerator will avoid recursing into any contained // symbolic links, so no further type checks are needed. NSDirectoryEnumerator *directoryEnumerator = [_fileManager enumeratorAtURL:targetURL includingPropertiesForKeys:nil options:(NSDirectoryEnumerationOptions)0 errorHandler:nil]; for (NSURL *file in directoryEnumerator) { if (![self _updateItemAtURL:file withAccessTime:currentTime error:error]) { return NO; } } } // Set the access time on the container last because the process of setting the access // time on children actually causes the access time of the container directory to be // updated. if (![self _updateItemAtURL:targetURL withAccessTime:currentTime error:error]) { return NO; } return YES; } // /usr/bin/touch can be used to update an application, as described in: // https://developer.apple.com/library/mac/documentation/Carbon/Conceptual/LaunchServicesConcepts/LSCConcepts/LSCConcepts.html // The document says LSRegisterURL() can be used as well but this hasn't worked out well for me in practice // Anyway, updating the modification time of the application is important because the system will be aware a new version of your app is available, // Finder will report the correct file size and other metadata for it, URL schemes your app may register will be updated, etc. // Behind the scenes, touch calls to utimes() which is what we use here - (BOOL)updateModificationAndAccessTimeOfItemAtURL:(NSURL *)targetURL error:(NSError * __autoreleasing *)error { if (![self _itemExistsAtURL:targetURL]) { if (error != NULL) { *error = [NSError errorWithDomain:NSCocoaErrorDomain code:NSFileNoSuchFileError userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Failed to update modification & access time because %@ does not exist.", targetURL.path.lastPathComponent] }]; } return NO; } char path[PATH_MAX] = {0}; if (![targetURL.path getFileSystemRepresentation:path maxLength:sizeof(path)]) { if (error != NULL) { *error = [NSError errorWithDomain:NSCocoaErrorDomain code:NSFileReadInvalidFileNameError userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"File to update modification & access time (%@) cannot be represented as a valid file name.", targetURL.path.lastPathComponent] }]; } return NO; } int fileDescriptor = open(path, O_RDONLY | O_SYMLINK); if (fileDescriptor == -1) { if (error != NULL) { *error = [NSError errorWithDomain:NSPOSIXErrorDomain code:errno userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Failed to open file descriptor to %@", targetURL.path.lastPathComponent] }]; } return NO; } // Using futimes() because utimes() follows symbolic links BOOL updatedTime = (futimes(fileDescriptor, NULL) == 0); close(fileDescriptor); if (!updatedTime) { if (error != NULL) { *error = [NSError errorWithDomain:NSPOSIXErrorDomain code:errno userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Failed to update modification & access time for %@", targetURL.path.lastPathComponent] }]; } } return updatedTime; } // Creates a directory at the item pointed by url // An item cannot already exist at the url, but the parent must be a directory that exists - (BOOL)makeDirectoryAtURL:(NSURL *)url error:(NSError * __autoreleasing *)error { if ([self _itemExistsAtURL:url]) { if (error != NULL) { *error = [NSError errorWithDomain:NSCocoaErrorDomain code:NSFileWriteFileExistsError userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Failed to create directory because file %@ already exists.", url.path.lastPathComponent] }]; } return NO; } NSURL *parentURL = [url URLByDeletingLastPathComponent]; BOOL isParentADirectory = NO; if (![self _itemExistsAtURL:parentURL isDirectory:&isParentADirectory] || !isParentADirectory) { if (error != NULL) { *error = [NSError errorWithDomain:NSCocoaErrorDomain code:NSFileNoSuchFileError userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Failed to create directory because parent directory %@ does not exist.", parentURL.path.lastPathComponent] }]; } return NO; } NSError *createDirectoryError = nil; if (![_fileManager createDirectoryAtURL:url withIntermediateDirectories:NO attributes:nil error:&createDirectoryError]) { if (error != NULL) { *error = createDirectoryError; } return NO; } return YES; } - (NSURL *)makeTemporaryDirectoryAppropriateForDirectoryURL:(NSURL *)directoryURL error:(NSError * __autoreleasing *)error { return [_fileManager URLForDirectory:NSItemReplacementDirectory inDomain:NSUserDomainMask appropriateForURL:directoryURL create:YES error:error]; } - (BOOL)removeItemAtURL:(NSURL *)url error:(NSError * __autoreleasing *)error { if (![self _itemExistsAtURL:url]) { if (error != NULL) { *error = [NSError errorWithDomain:NSCocoaErrorDomain code:NSFileNoSuchFileError userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Failed to remove file %@ because it does not exist.", url.path.lastPathComponent] }]; } return NO; } NSError *removeError = nil; if (![_fileManager removeItemAtURL:url error:&removeError]) { if (error != NULL) { *error = removeError; } return NO; } return YES; } @end ================================================ FILE: Sparkle/SUHost.h ================================================ // // SUHost.h // Sparkle // // Copyright 2008 Andy Matuschak. All rights reserved. // #import <Foundation/Foundation.h> NS_ASSUME_NONNULL_BEGIN @class SUPublicKeys; #ifndef BUILDING_SPARKLE_TESTS #define SUHostDefinitionAttribute SPU_OBJC_DIRECT_MEMBERS #else #define SUHostDefinitionAttribute __attribute__((objc_runtime_name("SUTestHost"))) #endif SUHostDefinitionAttribute @interface SUHost : NSObject @property (nonatomic, readonly) NSBundle *bundle; - (instancetype)initWithBundle:(NSBundle *)aBundle; - (instancetype)init NS_UNAVAILABLE; @property (readonly, nonatomic, copy) NSString *bundlePath; @property (readonly, nonatomic, copy) NSString *name; @property (readonly, nonatomic, copy) NSString *version; @property (readonly, nonatomic) BOOL validVersion; @property (readonly, nonatomic, copy) NSString *displayVersion; @property (readonly, nonatomic) SUPublicKeys *publicKeys; @property (getter=isRunningOnReadOnlyVolume, nonatomic, readonly) BOOL runningOnReadOnlyVolume; @property (getter=isRunningTranslocated, nonatomic, readonly) BOOL runningTranslocated; @property (readonly, nonatomic, copy, nullable) NSString *publicDSAKeyFileKey; @property (nonatomic, readonly) BOOL hasUpdateSecurityPolicy; @property (nonatomic, readonly) BOOL requiresSignedAppcast; - (nullable id)objectForInfoDictionaryKey:(NSString *)key ofClass:(Class)aClass; - (nullable NSNumber *)boolNumberForInfoDictionaryKey:(NSString *)key; - (BOOL)boolForInfoDictionaryKey:(NSString *)key; - (nullable NSNumber *)doubleNumberForInfoDictionaryKey:(NSString *)key; - (nullable id)objectForUserDefaultsKey:(NSString *)defaultName ofClass:(Class)aClass; - (void)setObject:(nullable id)value forUserDefaultsKey:(NSString *)defaultName; - (nullable NSNumber *)boolNumberForUserDefaultsKey:(NSString *)key; - (BOOL)boolForUserDefaultsKey:(NSString *)defaultName; - (void)setBool:(BOOL)value forUserDefaultsKey:(NSString *)defaultName; - (nullable NSNumber *)doubleNumberForUserDefaultsKey:(NSString *)key; - (nullable id)objectForKey:(NSString *)key ofClass:(Class)aClass; - (nullable NSNumber *)boolNumberForKey:(NSString *)key; - (BOOL)boolForKey:(NSString *)key; - (nullable NSNumber *)doubleNumberForKey:(NSString *)key; - (void)observeChangesFromUserDefaultKeys:(NSSet<NSString *> *)keyPaths changeHandler:(void (^)(NSString *))changeHandler; @end NS_ASSUME_NONNULL_END ================================================ FILE: Sparkle/SUHost.m ================================================ // // SUHost.m // Sparkle // // Copyright 2008 Andy Matuschak. All rights reserved. // #import "SUHost.h" #import "SUConstants.h" #include <sys/mount.h> // For statfs for isRunningOnReadOnlyVolume #import "SULog.h" #import "SUSignatures.h" #include "AppKitPrevention.h" NS_ASSUME_NONNULL_BEGIN // This class should not rely on AppKit and should also be process independent // For example, it should not have code that tests writabilty to somewhere on disk, // as that may depend on the privileges of the process owner. Or code that depends on // if the process is sandboxed or not; eg: finding the user's caches directory. Or code that depends // on compilation flags and if other files exist relative to the host bundle. static void *SUHostObservableContext = &SUHostObservableContext; @implementation SUHost { NSUserDefaults *_userDefaults; NSSet<NSString *> *_observedUserDefaultKeyPaths; NSMutableSet<NSString *> *_modifyingKeyPaths; void (^_changeObservationHandler)(NSString *); BOOL _isMainBundle; } @synthesize bundle = _bundle; - (instancetype)initWithBundle:(NSBundle *)aBundle { if ((self = [super init])) { NSParameterAssert(aBundle); _bundle = aBundle; if (_bundle.bundleIdentifier == nil) { SULog(SULogLevelError, @"Error: the bundle being updated at %@ has no %@! This will cause preference read/write to not work properly.", _bundle, kCFBundleIdentifierKey); } _isMainBundle = [aBundle isEqualTo:[NSBundle mainBundle]]; NSString *domainIdentifier; { NSString *defaultsDomain = [self objectForInfoDictionaryKey:SUDefaultsDomainKey ofClass:NSString.class]; if (defaultsDomain != nil) { domainIdentifier = defaultsDomain; } else if (!_isMainBundle) { domainIdentifier = aBundle.bundleIdentifier; } else { domainIdentifier = nil; } } if (domainIdentifier == nil) { _userDefaults = [NSUserDefaults standardUserDefaults]; } else { _userDefaults = [[NSUserDefaults alloc] initWithSuiteName:domainIdentifier]; } } return self; } - (void)dealloc { if (_observedUserDefaultKeyPaths != nil) { for (NSString *keyPath in _observedUserDefaultKeyPaths) { [_userDefaults removeObserver:self forKeyPath:keyPath]; } } } - (void)observeChangesFromUserDefaultKeys:(NSSet<NSString *> *)keyPaths changeHandler:(void (^)(NSString *))changeHandler { _modifyingKeyPaths = [NSMutableSet set]; for (NSString *keyPath in keyPaths) { [_userDefaults addObserver:self forKeyPath:keyPath options:NSKeyValueObservingOptionNew context:SUHostObservableContext]; } _observedUserDefaultKeyPaths = keyPaths; _changeObservationHandler = [changeHandler copy]; } - (void)observeValueForKeyPath:(nullable NSString *)keyPath ofObject:(nullable id)object change:(nullable NSDictionary<NSKeyValueChangeKey,id> *)change context:(nullable void *)context { if (context != SUHostObservableContext) { [super observeValueForKeyPath:keyPath ofObject:object change:change context:context]; return; } if (keyPath == nil || [_modifyingKeyPaths containsObject:(NSString * _Nonnull)keyPath]) { return; } if (_changeObservationHandler == nil) { return; } _changeObservationHandler((NSString * _Nonnull)keyPath); } - (NSString *)description { return [NSString stringWithFormat:@"%@ <%@>", [self class], [self bundlePath]]; } - (NSString *)bundlePath { return _bundle.bundlePath; } - (NSString * _Nonnull)name { NSString *name; // Allow host bundle to provide a custom name name = [self objectForInfoDictionaryKey:@"SUBundleName" ofClass:NSString.class]; if (name && name.length > 0) return name; name = [self objectForInfoDictionaryKey:@"CFBundleDisplayName" ofClass:NSString.class]; if (name && name.length > 0) return name; name = [self objectForInfoDictionaryKey:(__bridge NSString *)kCFBundleNameKey ofClass:NSString.class]; if (name && name.length > 0) return name; return [[[NSFileManager defaultManager] displayNameAtPath:[self bundlePath]] stringByDeletingPathExtension]; } - (BOOL)validVersion { return [self isValidVersion:[self _version]]; } - (BOOL)isValidVersion:(NSString * _Nullable)version SPU_OBJC_DIRECT { return (version != nil && version.length != 0); } - (NSString * _Nullable)_version SPU_OBJC_DIRECT { NSString *version = [self objectForInfoDictionaryKey:(__bridge NSString *)kCFBundleVersionKey ofClass:NSString.class]; return ([self isValidVersion:version] ? version : nil); } - (NSString * _Nonnull)version { NSString *version = [self _version]; if (version == nil) { SULog(SULogLevelError, @"This host (%@) has no %@! This attribute is required.", [self bundlePath], (__bridge NSString *)kCFBundleVersionKey); // Instead of abort()-ing, return an empty string to satisfy the _Nonnull contract. return @""; } return version; } - (NSString * _Nonnull)displayVersion { NSString *shortVersionString = [self objectForInfoDictionaryKey:@"CFBundleShortVersionString" ofClass:NSString.class]; if (shortVersionString) return shortVersionString; else return [self version]; // Fall back on the normal version string. } - (BOOL)isRunningOnReadOnlyVolume { struct statfs statfs_info; if (statfs(_bundle.bundlePath.fileSystemRepresentation, &statfs_info) != 0) { return NO; } return (statfs_info.f_flags & MNT_RDONLY) != 0; } - (BOOL)isRunningTranslocated { NSString *path = _bundle.bundlePath; return [path rangeOfString:@"/AppTranslocation/"].location != NSNotFound; } - (NSString *_Nullable)publicEDKey SPU_OBJC_DIRECT { return [self objectForInfoDictionaryKey:SUPublicEDKeyKey ofClass:NSString.class]; } - (NSString *_Nullable)publicDSAKey SPU_OBJC_DIRECT { // Maybe the key is just a string in the Info.plist. NSString *key = [self objectForInfoDictionaryKey:SUPublicDSAKeyKey ofClass:NSString.class]; if (key) { return key; } // More likely, we've got a reference to a Resources file by filename: NSString *keyFilename = [self publicDSAKeyFileKey]; if (!keyFilename) { return nil; } NSString *keyPath = [_bundle pathForResource:keyFilename ofType:nil]; if (!keyPath) { return nil; } NSError *error = nil; key = [NSString stringWithContentsOfFile:keyPath encoding:NSASCIIStringEncoding error:&error]; if (error) { SULog(SULogLevelError, @"Error loading %@: %@", keyPath, error); } return key; } - (BOOL)hasUpdateSecurityPolicy { NSDictionary<NSString *, id> *updateSecurityPolicy = [self objectForInfoDictionaryKey:@"NSUpdateSecurityPolicy" ofClass:NSDictionary.class]; return (updateSecurityPolicy != nil); } - (BOOL)requiresSignedAppcast { return [self boolForInfoDictionaryKey:SURequireSignedFeedKey]; } - (SUPublicKeys *)publicKeys { return [[SUPublicKeys alloc] initWithEd:[self publicEDKey] dsa:[self publicDSAKey]]; } - (NSString * _Nullable)publicDSAKeyFileKey { return [self objectForInfoDictionaryKey:SUPublicDSAKeyFileKey ofClass:NSString.class]; } static _Nullable id validateObject(id _Nullable object, NSSet<Class> * classes, NSString *key, NSString *keyType) { if (object == nil) { return nil; } for (Class aClass in classes) { if ([(NSObject *)object isKindOfClass:aClass]) { return object; } } SULog(SULogLevelError, @"Error: Reading %@ key %@ with expected classes %@ but instead found %@", keyType, key, classes, ((NSObject *)object).className); return nil; } - (nullable id)objectForInfoDictionaryKey:(NSString *)key ofClasses:(NSSet<Class> *)classes SPU_OBJC_DIRECT { id object; if (_isMainBundle) { // Common fast path - if we're updating the main bundle, that means our updater and host bundle's lifetime is the same // If the bundle happens to be updated or change, that means our updater process needs to be terminated first to do it safely // Thus we can rely on the cached Info dictionary object = [_bundle objectForInfoDictionaryKey:key]; } else { // Slow path - if we're updating another bundle, we should read in the most up to date Info dictionary because // the bundle can be replaced externally or even by us. // This is the easiest way to read the Info dictionary values *correctly* despite some performance loss. // A mutable method to reload the Info dictionary at certain points and have it cached at other points is challenging to do correctly. CFDictionaryRef cfInfoDictionary = CFBundleCopyInfoDictionaryInDirectory((CFURLRef)_bundle.bundleURL); NSDictionary *infoDictionary = CFBridgingRelease(cfInfoDictionary); object = [infoDictionary objectForKey:key]; } return validateObject(object, classes, key, @"info dictionary"); } - (nullable id)objectForInfoDictionaryKey:(NSString *)key ofClass:(Class)aClass { return [self objectForInfoDictionaryKey:key ofClasses:[NSSet setWithObject:aClass]]; } static NSNumber * _Nullable convertObjectToBoolNumber(NSObject * _Nullable object, NSString *key, NSString *keyType) { if (object == nil) { return nil; } if ([object isKindOfClass:NSNumber.class]) { return (NSNumber *)object; } if ([object isKindOfClass:NSString.class]) { return @(((NSString *)object).boolValue); } SULog(SULogLevelError, @"Error: Reading %@ key %@ expecting convertible bool but instead found class %@", keyType, key, ((NSObject *)object).className); return nil; } static NSNumber * _Nullable convertObjectToDoubleNumber(NSObject * _Nullable object, NSString *key, NSString *keyType) { if (object == nil) { return nil; } if ([object isKindOfClass:NSNumber.class]) { return (NSNumber *)object; } if ([object isKindOfClass:NSString.class]) { return @(((NSString *)object).doubleValue); } SULog(SULogLevelError, @"Error: Reading %@ key %@ expecting convertible double but instead found class %@", keyType, key, ((NSObject *)object).className); return nil; } - (nullable NSNumber *)boolNumberForInfoDictionaryKey:(NSString *)key { NSObject *object = [self objectForInfoDictionaryKey:key ofClasses:[NSSet setWithArray:@[NSNumber.class, NSString.class]]]; return convertObjectToBoolNumber(object, key, @"info dictionary"); } - (BOOL)boolForInfoDictionaryKey:(NSString *)key { return [[self boolNumberForInfoDictionaryKey:key] boolValue]; } - (nullable NSNumber *)doubleNumberForInfoDictionaryKey:(NSString *)key { NSObject *object = [self objectForInfoDictionaryKey:key ofClasses:[NSSet setWithArray:@[NSNumber.class, NSString.class]]]; return convertObjectToDoubleNumber(object, key, @"info dictionary"); } - (nullable id)objectForUserDefaultsKey:(NSString *)defaultName ofClasses:(NSSet<Class> *)classes SPU_OBJC_DIRECT { if (defaultName == nil || _userDefaults == nil) { return nil; } id object = [_userDefaults objectForKey:defaultName]; return validateObject(object, classes, defaultName, @"user default"); } - (nullable id)objectForUserDefaultsKey:(NSString *)defaultName ofClass:(Class)aClass { return [self objectForUserDefaultsKey:defaultName ofClasses:[NSSet setWithObject:aClass]]; } // Note this handles nil being passed for defaultName, in which case the user default will be removed - (void)setObject:(nullable id)value forUserDefaultsKey:(NSString *)defaultName { [_modifyingKeyPaths addObject:defaultName]; [_userDefaults setObject:value forKey:defaultName]; [_modifyingKeyPaths removeObject:defaultName]; } - (nullable NSNumber *)boolNumberForUserDefaultsKey:(NSString *)key; { NSObject *object = [self objectForUserDefaultsKey:key ofClasses:[NSSet setWithArray:@[NSNumber.class, NSString.class]]]; return convertObjectToBoolNumber(object, key, @"user default"); } - (BOOL)boolForUserDefaultsKey:(NSString *)defaultName { return [[self boolNumberForUserDefaultsKey:defaultName] boolValue]; } - (void)setBool:(BOOL)value forUserDefaultsKey:(NSString *)defaultName { [_modifyingKeyPaths addObject:defaultName]; [_userDefaults setBool:value forKey:defaultName]; [_modifyingKeyPaths removeObject:defaultName]; } - (nullable NSNumber *)doubleNumberForUserDefaultsKey:(NSString *)key { NSObject *object = [self objectForUserDefaultsKey:key ofClasses:[NSSet setWithArray:@[NSNumber.class, NSString.class]]]; return convertObjectToDoubleNumber(object, key, @"user default"); } - (nullable id)objectForKey:(NSString *)key ofClass:(Class)aClass { id userDefaultsObject = [self objectForUserDefaultsKey:key ofClass:aClass]; return userDefaultsObject != nil ? userDefaultsObject : [self objectForInfoDictionaryKey:key ofClass:aClass]; } - (nullable NSNumber *)boolNumberForKey:(NSString *)key { NSNumber *boolFromUserDefaults = [self boolNumberForUserDefaultsKey:key]; return (boolFromUserDefaults != nil) ? boolFromUserDefaults : [self boolNumberForInfoDictionaryKey:key]; } - (BOOL)boolForKey:(NSString *)key { return [[self boolNumberForKey:key] boolValue]; } - (nullable NSNumber *)doubleNumberForKey:(NSString *)key { NSNumber *doubleFromUserDefaults = [self doubleNumberForUserDefaultsKey:key]; return (doubleFromUserDefaults != nil) ? doubleFromUserDefaults : [self doubleNumberForInfoDictionaryKey:key]; } @end NS_ASSUME_NONNULL_END ================================================ FILE: Sparkle/SUInstallerProtocol.h ================================================ // // SUInstallerProtocol.h // Sparkle // // Created by Mayur Pawashe on 12/26/16. // Copyright © 2016 Sparkle Project. All rights reserved. // #import <Foundation/Foundation.h> NS_ASSUME_NONNULL_BEGIN @protocol SUInstallerProtocol <NSObject> // Any installation work can be done prior to user application being terminated and relaunched // Currently this is invoked after the user application is terminated, but this may change in the future. // No UI should occur during this stage (i.e, do not show package installer apps, etc..) // Should be able to be called from non-main thread - (BOOL)performInitialInstallation:(NSError **)error; // Any installation work after the user application has been terminated. This is where the final installation work can be done. // After this stage is done, the user application may be relaunched. // Should be able to be called from non-main thread - (BOOL)performFinalInstallationProgressBlock:(nullable void(^)(double))cb error:(NSError **)error; // Indicates whether or not this installer can install the update silently in the background, without hindering the user // Should be thread safe - (BOOL)canInstallSilently; // The destination and installation path of the bundle being updated // Should be thread safe - (NSString *)installationPath; @end NS_ASSUME_NONNULL_END ================================================ FILE: Sparkle/SULegacyWebView.h ================================================ // // SULegacyWebView.h // Sparkle // // Created by Mayur Pawashe on 12/30/20. // Copyright © 2020 Sparkle Project. All rights reserved. // #if SPARKLE_BUILD_UI_BITS && DOWNLOADER_XPC_SERVICE_EMBEDDED #import <Foundation/Foundation.h> #import "SUReleaseNotesView.h" NS_ASSUME_NONNULL_BEGIN SPU_OBJC_DIRECT_MEMBERS @interface SULegacyWebView : NSObject <SUReleaseNotesView> - (instancetype)initWithColorStyleSheetLocation:(NSURL *)colorStyleSheetLocation fontFamily:(NSString *)fontFamily fontPointSize:(int)fontPointSize javaScriptEnabled:(BOOL)javaScriptEnabled customAllowedURLSchemes:(NSArray<NSString *> *)customAllowedURLSchemes; @end NS_ASSUME_NONNULL_END #endif ================================================ FILE: Sparkle/SULegacyWebView.m ================================================ // // SULegacyWebView.m // Sparkle // // Created by Mayur Pawashe on 12/30/20. // Copyright © 2020 Sparkle Project. All rights reserved. // #if SPARKLE_BUILD_UI_BITS && DOWNLOADER_XPC_SERVICE_EMBEDDED #import "SULegacyWebView.h" #import "SUReleaseNotesCommon.h" #import "SULog.h" #import <WebKit/WebKit.h> #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdeprecated-declarations" @interface SULegacyWebView () <WebPolicyDelegate, WebFrameLoadDelegate, WebUIDelegate> @end @implementation SULegacyWebView { WebView *_webView; NSArray<NSString *> *_customAllowedURLSchemes; void (^_completionHandler)(NSError * _Nullable); } - (instancetype)initWithColorStyleSheetLocation:(NSURL *)colorStyleSheetLocation fontFamily:(NSString *)fontFamily fontPointSize:(int)fontPointSize javaScriptEnabled:(BOOL)javaScriptEnabled customAllowedURLSchemes:(NSArray<NSString *> *)customAllowedURLSchemes { self = [super init]; if (self != nil) { _webView = [[WebView alloc] initWithFrame:NSZeroRect]; WebPreferences *preferences = [[WebPreferences alloc] initWithIdentifier:@"sparkle-project.org.legacy-web-view"]; preferences.autosaves = NO; preferences.javaScriptEnabled = javaScriptEnabled; preferences.javaEnabled = NO; preferences.plugInsEnabled = NO; // Mimicking settings when WebView used to be in SUUpdateAlert nib preferences.loadsImagesAutomatically = YES; preferences.allowsAnimatedImages = YES; preferences.allowsAnimatedImageLooping = YES; // Settings for default style preferences.userStyleSheetEnabled = YES; preferences.userStyleSheetLocation = colorStyleSheetLocation; preferences.standardFontFamily = fontFamily; preferences.defaultFontSize = fontPointSize; _webView.preferences = preferences; _webView.policyDelegate = self; _webView.frameLoadDelegate = self; _webView.UIDelegate = self; _customAllowedURLSchemes = customAllowedURLSchemes; } return self; } - (NSView *)view { return _webView; } - (void)loadString:(NSString *)htmlString baseURL:(NSURL * _Nullable)baseURL completionHandler:(void (^)(NSError * _Nullable))completionHandler { _completionHandler = [completionHandler copy]; [[_webView mainFrame] loadHTMLString:htmlString baseURL:baseURL]; } - (void)loadData:(NSData *)data MIMEType:(NSString *)MIMEType textEncodingName:(NSString *)textEncodingName baseURL:(NSURL *)baseURL completionHandler:(void (^)(NSError * _Nullable))completionHandler { _completionHandler = [completionHandler copy]; [[_webView mainFrame] loadData:data MIMEType:MIMEType textEncodingName:textEncodingName baseURL:baseURL]; } - (void)stopLoading { _completionHandler = nil; [_webView stopLoading:self]; } - (void)setDrawsBackground:(BOOL)drawsBackground { _webView.drawsBackground = drawsBackground; } - (void)webView:(WebView *)sender didFinishLoadForFrame:(WebFrame *)frame { if ([frame parentFrame] == nil) { if (_completionHandler != nil) { _completionHandler(nil); _completionHandler = nil; } [sender display]; // necessary to prevent weird scroll bar artifacting } } - (void)webView:(WebView *)sender didFailLoadWithError:(NSError *)error forFrame:(WebFrame *)frame { if ([frame parentFrame] == nil) { if (_completionHandler != nil) { _completionHandler(error); _completionHandler = nil; } } } - (void)webView:(WebView *)__unused sender decidePolicyForNavigationAction:(NSDictionary *)__unused actionInformation request:(NSURLRequest *)request frame:(WebFrame *)__unused frame decisionListener:(id<WebPolicyDecisionListener>)listener { NSURL *requestURL = request.URL; BOOL isAboutBlank = NO; BOOL safeURL = SUReleaseNotesIsSafeURL(requestURL, _customAllowedURLSchemes, &isAboutBlank); // Do not allow redirects to dangerous protocols such as file:// if (!safeURL) { SULog(SULogLevelDefault, @"Blocked display of %@ URL which may be dangerous", requestURL.scheme); [listener ignore]; return; } // Ensure we are finished loading if (_completionHandler == nil) { if (requestURL && !isAboutBlank) { [[NSWorkspace sharedWorkspace] openURL:requestURL]; } [listener ignore]; } else { [listener use]; } } // Clean up the contextual menu. - (NSArray *)webView:(WebView *)__unused sender contextMenuItemsForElement:(NSDictionary *)__unused element defaultMenuItems:(NSArray *)defaultMenuItems { NSMutableArray *webViewMenuItems = [defaultMenuItems mutableCopy]; if (webViewMenuItems) { for (NSMenuItem *menuItem in defaultMenuItems) { NSInteger tag = [menuItem tag]; switch (tag) { case WebMenuItemTagOpenLinkInNewWindow: case WebMenuItemTagDownloadLinkToDisk: case WebMenuItemTagOpenImageInNewWindow: case WebMenuItemTagDownloadImageToDisk: case WebMenuItemTagOpenFrameInNewWindow: case WebMenuItemTagGoBack: case WebMenuItemTagGoForward: case WebMenuItemTagStop: case WebMenuItemTagReload: [webViewMenuItems removeObjectIdenticalTo:menuItem]; } } } return webViewMenuItems; } @end #pragma clang diagnostic pop #endif ================================================ FILE: Sparkle/SULocalizations.h ================================================ // // SULocalizations.h // Sparkle // // Created by Mayur Pawashe on 2/28/16. // Copyright © 2016 Sparkle Project. All rights reserved. // #ifndef SULocalizations_h #define SULocalizations_h #if SPARKLE_COPY_LOCALIZATIONS #import "SUConstants.h" // This should only be used from inside the framework (not helper tools) #define SUSparkleBundle() ((NSBundle * _Nonnull)([NSBundle bundleWithIdentifier:SUBundleIdentifier])) #define SPARKLE_TABLE @"Sparkle" #define SULocalizedStringFromTableInBundle(key, tbl, bundle, comment) (NSLocalizedStringFromTableInBundle(key, tbl, bundle, comment) ?: key) #else #define SULocalizedStringFromTableInBundle(key, tbl, bundle, comment) key #endif #endif /* SULocalizations_h */ ================================================ FILE: Sparkle/SULog+NSError.h ================================================ // // SULog+NSError.h // Sparkle // // Created by Mayur Pawashe on 3/19/21. // Copyright © 2021 Sparkle Project. All rights reserved. // #ifndef SULog_NSError_h #define SULog_NSError_h #import <Foundation/Foundation.h> void SULogError(NSError *error); #endif /* SULog_NSError_h */ ================================================ FILE: Sparkle/SULog+NSError.m ================================================ // // SULog+NSError.m // Sparkle // // Created by Mayur Pawashe on 3/19/21. // Copyright © 2021 Sparkle Project. All rights reserved. // #import "SULog+NSError.h" #import "SULog.h" #include "AppKitPrevention.h" static void SULogErrors(NSArray<NSError *> *errors, int recursionLimit) { if (recursionLimit == 0) { return; } for (NSError *error in errors) { SULog(SULogLevelError, @"Error: %@ %@ (URL %@)", error.localizedDescription, error.localizedFailureReason, error.userInfo[NSURLErrorFailingURLErrorKey]); NSDictionary<NSErrorUserInfoKey, id> *userInfo = error.userInfo; if (@available(macOS 11.3, *)) { NSArray<NSError *> *underlyingErrors = userInfo[NSMultipleUnderlyingErrorsKey]; if (underlyingErrors != nil) { SULogErrors(underlyingErrors, recursionLimit - 1); continue; } } NSError *underlyingError = userInfo[NSUnderlyingErrorKey]; if (underlyingError != nil) { SULogErrors(@[underlyingError], recursionLimit - 1); } } } void SULogError(NSError *error) { if (error == nil) { return; } SULogErrors(@[error], 7); } ================================================ FILE: Sparkle/SULog.h ================================================ // // SULog.h // Sparkle // // Created by Mayur Pawashe on 5/18/16. // Copyright © 2016 Sparkle Project. All rights reserved. // #ifndef SULOG_H #define SULOG_H #include <Foundation/Foundation.h> typedef NS_ENUM(uint8_t, SULogLevel) { // This level is for information that *might* result a failure // For now until other levels are added, this may serve as a level for other information as well SULogLevelDefault, // This level is for errors that occurred SULogLevelError }; // Logging utility function that is thread-safe and uses os_log // For debugging command line tools, you may have to use Console.app or log(1) to view log messages // Try to keep log messages as compact/short as possible void SULog(SULogLevel level, NSString *format, ...) NS_FORMAT_FUNCTION(2, 3); #endif ================================================ FILE: Sparkle/SULog.m ================================================ // // SULog.m // Sparkle // // Created by Mayur Pawashe on 5/18/16. // Copyright © 2016 Sparkle Project. All rights reserved. // #include "SULog.h" #include <os/log.h> #include "AppKitPrevention.h" void SULog(SULogLevel level, NSString *format, ...) { static dispatch_once_t onceToken; static os_log_t logger; dispatch_once(&onceToken, ^{ const char *subsystem = SPARKLE_BUNDLE_IDENTIFIER; // This creates a thread-safe object logger = os_log_create(subsystem, "Sparkle"); }); va_list ap; va_start(ap, format); NSString *logMessage = [[NSString alloc] initWithFormat:format arguments:ap]; va_end(ap); // We'll make all of our messages formatted as public; just don't log sensitive information. // Note we don't take advantage of info like the source line number because we wrap this macro inside our own function // And we don't really leverage of os_log's deferred formatting processing because we format the string before passing it in switch (level) { #pragma clang diagnostic push #if __has_warning("-Wpre-c11-compat") #pragma clang diagnostic ignored "-Wpre-c11-compat" #endif case SULogLevelDefault: // See docs for OS_LOG_TYPE_DEFAULT // By default, OS_LOG_TYPE_DEFAULT seems to be more noticeable than OS_LOG_TYPE_INFO os_log(logger, "%{public}@", logMessage); break; case SULogLevelError: // See docs for OS_LOG_TYPE_ERROR os_log_error(logger, "%{public}@", logMessage); break; #pragma clang diagnostic pop } } ================================================ FILE: Sparkle/SUNormalization.h ================================================ // // SUNormalization.h // Sparkle // // Created by Mayur Pawashe on 3/26/21. // Copyright © 2021 Sparkle Project. All rights reserved. // #import <Foundation/Foundation.h> #import "SUHost.h" NS_ASSUME_NONNULL_BEGIN NSString *SUNormalizedInstallationPath(SUHost *host); NS_ASSUME_NONNULL_END ================================================ FILE: Sparkle/SUNormalization.m ================================================ // // SUNormalization.m // Sparkle // // Created by Mayur Pawashe on 3/26/21. // Copyright © 2021 Sparkle Project. All rights reserved. // #import "SUNormalization.h" #include "AppKitPrevention.h" NSString *SUNormalizedInstallationPath(SUHost *host) { NSBundle *bundle = host.bundle; assert(bundle != nil); NSString * baseBundleName = [host objectForInfoDictionaryKey:@"SUBundleName" ofClass:NSString.class]; if (baseBundleName == nil) { baseBundleName = [host objectForInfoDictionaryKey:(__bridge NSString *)kCFBundleNameKey ofClass:NSString.class]; } NSString *normalizedAppPath = [[[bundle bundlePath] stringByDeletingLastPathComponent] stringByAppendingPathComponent:[NSString stringWithFormat:@"%@.%@", baseBundleName, [[bundle bundlePath] pathExtension]]]; // Roundtrip string through fileSystemRepresentation to ensure it uses filesystem's Unicode normalization // rather than arbitrary Unicode form from Info.plist - #1017 NSString *unicodeNormalizedPath = [NSString stringWithUTF8String:[normalizedAppPath fileSystemRepresentation]]; if (unicodeNormalizedPath != nil) { return unicodeNormalizedPath; } else { return normalizedAppPath; } } ================================================ FILE: Sparkle/SUOperatingSystem.h ================================================ // // SUOperatingSystem.h // Sparkle // // Copyright © 2015 Sparkle Project. All rights reserved. // #import <Foundation/Foundation.h> SPU_OBJC_DIRECT_MEMBERS @interface SUOperatingSystem : NSObject + (NSString *)systemVersionString; @end ================================================ FILE: Sparkle/SUOperatingSystem.m ================================================ // // SUOperatingSystem.m // Sparkle // // Copyright © 2015 Sparkle Project. All rights reserved. // #import "SUOperatingSystem.h" #include "AppKitPrevention.h" @implementation SUOperatingSystem + (NSString *)systemVersionString { NSOperatingSystemVersion version = [[NSProcessInfo processInfo] operatingSystemVersion]; return [NSString stringWithFormat:@"%ld.%ld.%ld", (long)version.majorVersion, (long)version.minorVersion, (long)version.patchVersion]; } @end ================================================ FILE: Sparkle/SUPhasedUpdateGroupInfo.h ================================================ // // SUPhasedUpdateGroupInfo.h // Sparkle // // Created by Mayur Pawashe on 01/24/21. // Copyright © 2016 Sparkle Project. All rights reserved. // #import <Foundation/Foundation.h> NS_ASSUME_NONNULL_BEGIN @class SUHost; #ifndef BUILDING_SPARKLE_TESTS SPU_OBJC_DIRECT_MEMBERS #endif @interface SUPhasedUpdateGroupInfo : NSObject + (NSUInteger)updateGroupForHost:(SUHost*)host; + (NSNumber*)setNewUpdateGroupIdentifierForHost:(SUHost*)host; @end NS_ASSUME_NONNULL_END ================================================ FILE: Sparkle/SUPhasedUpdateGroupInfo.m ================================================ // // SUPhasedUpdateGroupInfo.m // Sparkle // // Created by Mayur Pawashe on 01/24/21. // Copyright © 2016 Sparkle Project. All rights reserved. // #import "SUPhasedUpdateGroupInfo.h" #import "SUHost.h" #import "SUConstants.h" #include "AppKitPrevention.h" @implementation SUPhasedUpdateGroupInfo #define NUM_UPDATE_GROUPS 7 + (NSUInteger)updateGroupForHost:(SUHost*)host { NSNumber* updateGroupIdentifier = [self updateGroupIdentifierForHost:host]; return ([updateGroupIdentifier unsignedIntValue] % NUM_UPDATE_GROUPS); } + (NSNumber*)updateGroupIdentifierForHost:(SUHost*)host SPU_OBJC_DIRECT { // Only Sparkle should set this user default, so we don't need to convert NSString -> integer number // We can just require NSNumber class. NSNumber* updateGroupIdentifier = [host objectForUserDefaultsKey:SUUpdateGroupIdentifierKey ofClass:NSNumber.class]; if(updateGroupIdentifier == nil) { updateGroupIdentifier = [self setNewUpdateGroupIdentifierForHost:host]; } return updateGroupIdentifier; } + (NSNumber*)setNewUpdateGroupIdentifierForHost:(SUHost*)host { uint32_t r = arc4random_uniform(UINT_MAX); NSNumber* updateGroupIdentifier = @(r); [host setObject:updateGroupIdentifier forUserDefaultsKey:SUUpdateGroupIdentifierKey]; return updateGroupIdentifier; } @end ================================================ FILE: Sparkle/SUReleaseNotesCommon.h ================================================ // // SUReleaseNotesCommon.h // Sparkle // // Created by Mayur Pawashe on 12/31/20. // Copyright © 2020 Sparkle Project. All rights reserved. // #if SPARKLE_BUILD_UI_BITS #import <Foundation/Foundation.h> NS_ASSUME_NONNULL_BEGIN BOOL SUReleaseNotesIsSafeURL(NSURL *url, NSArray<NSString *> *customAllowedURLSchemes, BOOL *isAboutBlankURL); NS_ASSUME_NONNULL_END #endif ================================================ FILE: Sparkle/SUReleaseNotesCommon.m ================================================ // // SUReleaseNotesCommon.m // Sparkle // // Created by Mayur Pawashe on 12/31/20. // Copyright © 2020 Sparkle Project. All rights reserved. // #if SPARKLE_BUILD_UI_BITS #import "SUReleaseNotesCommon.h" #include "AppKitPrevention.h" BOOL SUReleaseNotesIsSafeURL(NSURL *url, NSArray<NSString *> *customAllowedURLSchemes, BOOL *isAboutBlankURL) { NSString *scheme = url.scheme; BOOL isAboutBlank = [url.absoluteString isEqualToString:@"about:blank"] || [url.absoluteString isEqualToString:@"about:srcdoc"]; BOOL safeURL = isAboutBlank || [@[@"http", @"https", @"macappstore", @"macappstores", @"itms-apps", @"itms-appss"] containsObject:scheme] || [customAllowedURLSchemes containsObject:scheme.lowercaseString]; *isAboutBlankURL = isAboutBlank; return safeURL; } #endif ================================================ FILE: Sparkle/SUReleaseNotesView.h ================================================ // // SUReleaseNotesView.h // Sparkle // // Created by Mayur Pawashe on 12/30/20. // Copyright © 2020 Sparkle Project. All rights reserved. // #if SPARKLE_BUILD_UI_BITS #import <Foundation/Foundation.h> @class NSView; NS_ASSUME_NONNULL_BEGIN @protocol SUReleaseNotesView <NSObject> @property (nonatomic, readonly) NSView *view; - (void)loadString:(NSString *)string baseURL:(NSURL * _Nullable)baseURL completionHandler:(void (^)(NSError * _Nullable))completionHandler; - (void)loadData:(NSData *)data MIMEType:(NSString *)MIMEType textEncodingName:(NSString *)textEncodingName baseURL:(NSURL *)baseURL completionHandler:(void (^)(NSError * _Nullable))completionHandler; - (void)stopLoading; - (void)setDrawsBackground:(BOOL)drawsBackground; @end NS_ASSUME_NONNULL_END #endif ================================================ FILE: Sparkle/SUSignatures.h ================================================ // // SUSignatures.h // Sparkle // // Created by Kornel on 15/09/2018. // Copyright © 2018 Sparkle Project. All rights reserved. // #import <Foundation/Foundation.h> NS_ASSUME_NONNULL_BEGIN typedef NS_ENUM(uint8_t, SUSigningInputStatus) { /// An input was not provided at all. SUSigningInputStatusAbsent = 0, /// An input was provided, but did not have the correct format. SUSigningInputStatusInvalid, /// An input was provided and can be used for verifying signing information. SUSigningInputStatusPresent, SUSigningInputStatusLastValidCase = SUSigningInputStatusPresent }; #ifndef BUILDING_SPARKLE_TESTS #define SUSignaturesDefinitionAttribute SPU_OBJC_DIRECT_MEMBERS #else #define SUSignaturesDefinitionAttribute __attribute__((objc_runtime_name("SUTestSignatures"))) #endif SUSignaturesDefinitionAttribute @interface SUSignatures : NSObject <NSSecureCoding> #if SPARKLE_BUILD_LEGACY_DSA_SUPPORT @property (nonatomic, readonly, nullable) NSData *dsaSignature; @property (nonatomic, readonly) SUSigningInputStatus dsaSignatureStatus; #endif @property (nonatomic, readonly, nullable) const unsigned char *ed25519Signature; @property (nonatomic, readonly) SUSigningInputStatus ed25519SignatureStatus; - (instancetype)initWithEd:(NSString * _Nullable)ed #if SPARKLE_BUILD_LEGACY_DSA_SUPPORT dsa:(NSString * _Nullable)dsa #endif ; @end #ifndef BUILDING_SPARKLE_TESTS #define SUPublicKeysDefinitionAttribute SPU_OBJC_DIRECT_MEMBERS #else #define SUPublicKeysDefinitionAttribute __attribute__((objc_runtime_name("SUTestPublicKeys"))) #endif SUPublicKeysDefinitionAttribute @interface SUPublicKeys : NSObject @property (nonatomic, readonly, nullable) NSString *dsaPubKey; @property (nonatomic, readonly) SUSigningInputStatus dsaPubKeyStatus; @property (nonatomic, readonly, nullable) const unsigned char *ed25519PubKey; @property (nonatomic, readonly) SUSigningInputStatus ed25519PubKeyStatus; /// Returns YES if either key is present (though they may be invalid). @property (nonatomic, readonly) BOOL hasAnyKeys; - (instancetype)initWithEd:(NSString * _Nullable)ed dsa:(NSString * _Nullable)dsa; @end NS_ASSUME_NONNULL_END ================================================ FILE: Sparkle/SUSignatures.m ================================================ // // SUSignatures.m // Sparkle // // Created by Kornel on 15/09/2018. // Copyright © 2018 Sparkle Project. All rights reserved. // #import "SUSignatures.h" #import <assert.h> #import "SULog.h" #include "AppKitPrevention.h" #if SPARKLE_BUILD_LEGACY_DSA_SUPPORT static NSString *SUDSASignatureKey = @"SUDSASignature"; static NSString *SUDSASignatureStatusKey = @"SUDSASignatureStatus"; #endif static NSString *SUEDSignatureKey = @"SUEDSignature"; static NSString *SUEDSignatureStatusKey = @"SUEDSignatureStatus"; @implementation SUSignatures { unsigned char _ed25519_signature[64]; } #if SPARKLE_BUILD_LEGACY_DSA_SUPPORT @synthesize dsaSignature = _dsaSignature; @synthesize dsaSignatureStatus = _dsaSignatureStatus; #endif @synthesize ed25519SignatureStatus = _ed25519SignatureStatus; static SUSigningInputStatus decode(NSString *str, NSData * __strong *outData) { if (str == nil) { return SUSigningInputStatusAbsent; } NSString *stripped = [str stringByTrimmingCharactersInSet:NSCharacterSet.whitespaceAndNewlineCharacterSet]; NSData *result = [[NSData alloc] initWithBase64EncodedString:stripped options:(NSDataBase64DecodingOptions)0]; if (!result) { return SUSigningInputStatusInvalid; } *outData = result; return SUSigningInputStatusPresent; } - (instancetype)initWithEd:(NSString * _Nullable)maybeEd25519 #if SPARKLE_BUILD_LEGACY_DSA_SUPPORT dsa:(NSString * _Nullable)maybeDsa #endif { self = [super init]; if (self) { #if SPARKLE_BUILD_LEGACY_DSA_SUPPORT _dsaSignatureStatus = decode(maybeDsa, &_dsaSignature); if (_dsaSignatureStatus == SUSigningInputStatusInvalid) { SULog(SULogLevelError, @"The provided DSA signature could not be decoded."); } #endif if (maybeEd25519 != nil) { NSData *data = nil; _ed25519SignatureStatus = decode(maybeEd25519, &data); if (data) { if ([data length] == sizeof(_ed25519_signature)) { [data getBytes:_ed25519_signature length:sizeof(_ed25519_signature)]; } else { _ed25519SignatureStatus = SUSigningInputStatusInvalid; } } if (_ed25519SignatureStatus == SUSigningInputStatusInvalid) { SULog(SULogLevelError, @"The provided EdDSA signature could not be decoded."); } } } return self; } - (const unsigned char *)ed25519Signature { if (_ed25519SignatureStatus == SUSigningInputStatusPresent) { return _ed25519_signature; } return NULL; } static BOOL decodeStatus(NSCoder *decoder, NSString *key, SUSigningInputStatus *outStatus) { NSInteger rawValue = [decoder decodeIntegerForKey:key]; if (rawValue > SUSigningInputStatusLastValidCase) { return NO; } *outStatus = (SUSigningInputStatus)rawValue; return YES; } - (instancetype)initWithCoder:(NSCoder *)decoder { self = [super init]; if (self) { #if SPARKLE_BUILD_LEGACY_DSA_SUPPORT if (!decodeStatus(decoder, SUDSASignatureStatusKey, &_dsaSignatureStatus)) { return nil; } NSData *dsaSignature = [decoder decodeObjectOfClass:[NSData class] forKey:SUDSASignatureKey]; if (dsaSignature) { _dsaSignature = dsaSignature; } #endif if (!decodeStatus(decoder, SUEDSignatureStatusKey, &_ed25519SignatureStatus)) { return nil; } NSData *edSignature = [decoder decodeObjectOfClass:[NSData class] forKey:SUEDSignatureKey]; if (edSignature) { if (edSignature.length != sizeof(_ed25519_signature)) { return nil; } [edSignature getBytes:_ed25519_signature length:sizeof(_ed25519_signature)]; } } return self; } - (void)encodeWithCoder:(NSCoder *)coder { #if SPARKLE_BUILD_LEGACY_DSA_SUPPORT [coder encodeInteger:_dsaSignatureStatus forKey:SUDSASignatureStatusKey]; if (_dsaSignature) { [coder encodeObject:_dsaSignature forKey:SUDSASignatureKey]; } #endif [coder encodeInteger:_ed25519SignatureStatus forKey:SUEDSignatureStatusKey]; if ([self ed25519Signature] != NULL) { NSData *edSignature = [NSData dataWithBytesNoCopy:&_ed25519_signature length:sizeof(_ed25519_signature) freeWhenDone:false]; [coder encodeObject:edSignature forKey:SUEDSignatureKey]; } } + (BOOL)supportsSecureCoding { return YES; } @end @implementation SUPublicKeys { unsigned char _ed25519_public_key[32]; } @synthesize dsaPubKey = _dsaPubKey; @synthesize ed25519PubKeyStatus = _ed25519PubKeyStatus; - (instancetype)initWithEd:(NSString * _Nullable)maybeEd25519 dsa:(NSString * _Nullable)maybeDsa { self = [super init]; if (self) { _dsaPubKey = maybeDsa; if (maybeEd25519 != nil) { NSData *ed = nil; _ed25519PubKeyStatus = decode(maybeEd25519, &ed); if (ed) { if ([ed length] == sizeof(_ed25519_public_key)) { [ed getBytes:_ed25519_public_key length:sizeof(_ed25519_public_key)]; } else { _ed25519PubKeyStatus = SUSigningInputStatusInvalid; } } if (_ed25519PubKeyStatus == SUSigningInputStatusInvalid) { SULog(SULogLevelError, @"The provided EdDSA key could not be decoded."); } } } return self; } - (SUSigningInputStatus)dsaPubKeyStatus { // We don't currently do any prevalidation of DSA public keys, // so this is always going to be "present" or "absent". return (_dsaPubKey != nil) ? SUSigningInputStatusPresent : SUSigningInputStatusAbsent; } - (const unsigned char *)ed25519PubKey { if (_ed25519PubKeyStatus == SUSigningInputStatusPresent) { return _ed25519_public_key; } return NULL; } - (BOOL)hasAnyKeys { return (_ed25519PubKeyStatus != SUSigningInputStatusAbsent) || ([self dsaPubKeyStatus] != SUSigningInputStatusAbsent); } @end ================================================ FILE: Sparkle/SUStandardVersionComparator.h ================================================ // // SUStandardVersionComparator.h // Sparkle // // Created by Andy Matuschak on 12/21/07. // Copyright 2007 Andy Matuschak. All rights reserved. // #ifndef SUSTANDARDVERSIONCOMPARATOR_H #define SUSTANDARDVERSIONCOMPARATOR_H #import <Foundation/Foundation.h> #if defined(BUILDING_SPARKLE_SOURCES_EXTERNALLY) // Ignore incorrect warning #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wquoted-include-in-framework-header" #import "SUExport.h" #import "SUVersionComparisonProtocol.h" #pragma clang diagnostic pop #else #import <Sparkle/SUExport.h> #import <Sparkle/SUVersionComparisonProtocol.h> #endif NS_ASSUME_NONNULL_BEGIN /** Sparkle's default version comparator. This comparator is adapted from MacPAD, by Kevin Ballard. It's "dumb" in that it does essentially string comparison, in components split by character type. */ SU_EXPORT @interface SUStandardVersionComparator : NSObject <SUVersionComparison> /** Initializes a new instance of the standard version comparator. */ - (instancetype)init; /** A singleton instance of the comparator. */ @property (nonatomic, class, readonly) SUStandardVersionComparator *defaultComparator; /** Compares two version strings through textual analysis. These version strings should be in the format of x, x.y, or x.y.z where each component is a number. For example, valid version strings include "1.5.3", "500", or "4000.1" These versions that are compared correspond to the @c CFBundleVersion values of the updates. @param versionA The first version string to compare. @param versionB The second version string to compare. @return A comparison result between @c versionA and @c versionB */ - (NSComparisonResult)compareVersion:(NSString *)versionA toVersion:(NSString *)versionB; @end NS_ASSUME_NONNULL_END #endif ================================================ FILE: Sparkle/SUStandardVersionComparator.m ================================================ // // SUStandardVersionComparator.m // Sparkle // // Created by Andy Matuschak on 12/21/07. // Copyright 2007 Andy Matuschak. All rights reserved. // #import "SUVersionComparisonProtocol.h" #import "SUStandardVersionComparator.h" #include "AppKitPrevention.h" @implementation SUStandardVersionComparator - (instancetype)init { return [super init]; } + (SUStandardVersionComparator *)defaultComparator { static SUStandardVersionComparator *defaultComparator = nil; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ defaultComparator = [[SUStandardVersionComparator alloc] init]; }); return defaultComparator; } typedef NS_ENUM(NSInteger, SUCharacterType) { kNumberType, kStringType, kPeriodSeparatorType, kPunctuationSeparatorType, kWhitespaceSeparatorType, kDashType, }; - (SUCharacterType)typeOfCharacter:(NSString *)character SPU_OBJC_DIRECT { if ([character isEqualToString:@"."]) { return kPeriodSeparatorType; } else if ([character isEqualToString:@"-"]) { return kDashType; } else if ([[NSCharacterSet decimalDigitCharacterSet] characterIsMember:[character characterAtIndex:0]]) { return kNumberType; } else if ([[NSCharacterSet whitespaceAndNewlineCharacterSet] characterIsMember:[character characterAtIndex:0]]) { return kWhitespaceSeparatorType; } else if ([[NSCharacterSet punctuationCharacterSet] characterIsMember:[character characterAtIndex:0]]) { return kPunctuationSeparatorType; } else { return kStringType; } } - (BOOL)isSeparatorType:(SUCharacterType)characterType SPU_OBJC_DIRECT { switch (characterType) { case kNumberType: case kStringType: case kDashType: return NO; case kPeriodSeparatorType: case kPunctuationSeparatorType: case kWhitespaceSeparatorType: return YES; } } // If type A and type B are some sort of separator, consider them to be equal - (BOOL)isEqualCharacterTypeClassForTypeA:(SUCharacterType)typeA typeB:(SUCharacterType)typeB SPU_OBJC_DIRECT { switch (typeA) { case kNumberType: case kStringType: case kDashType: return (typeA == typeB); case kPeriodSeparatorType: case kPunctuationSeparatorType: case kWhitespaceSeparatorType: { switch (typeB) { case kPeriodSeparatorType: case kPunctuationSeparatorType: case kWhitespaceSeparatorType: return YES; case kNumberType: case kStringType: case kDashType: return NO; } } } } - (NSMutableArray<NSString *> *)splitVersionString:(NSString *)version SPU_OBJC_DIRECT { NSString *character; NSMutableString *s; NSUInteger i, n; SUCharacterType oldType, newType; NSMutableArray<NSString *> *parts = [NSMutableArray array]; if ([version length] == 0) { // Nothing to do here return parts; } s = [[version substringToIndex:1] mutableCopy]; oldType = [self typeOfCharacter:s]; n = [version length] - 1; for (i = 1; i <= n; ++i) { character = [version substringWithRange:NSMakeRange(i, 1)]; newType = [self typeOfCharacter:character]; if (newType == kDashType) { break; } if (oldType != newType || [self isSeparatorType:oldType]) { // We've reached a new segment NSString *aPart = [[NSString alloc] initWithString:s]; [parts addObject:aPart]; [s setString:character]; } else { // Add character to string and continue [s appendString:character]; } oldType = newType; } // Add the last part onto the array [parts addObject:[NSString stringWithString:s]]; return parts; } // This returns the count of number and period parts at the beginning of the version // See -balanceVersionPartsA:partsB below - (NSUInteger)countOfNumberAndPeriodStartingParts:(NSArray<NSString *> *)parts SPU_OBJC_DIRECT { NSUInteger count = 0; for (NSString *part in parts) { SUCharacterType characterType = [self typeOfCharacter:part]; if (characterType == kNumberType || characterType == kPeriodSeparatorType) { count++; } else { break; } } return count; } // See -balanceVersionPartsA:partsB below - (void)addNumberAndPeriodPartsToParts:(NSMutableArray<NSString *> *)toParts toNumberAndPeriodPartsCount:(NSUInteger)toNumberAndPeriodPartsCount fromParts:(NSArray<NSString *> *)fromParts fromNumberAndPeriodPartsCount:(NSUInteger)fromNumberAndPeriodPartsCount SPU_OBJC_DIRECT { NSUInteger partsCountDifference = (fromNumberAndPeriodPartsCount - toNumberAndPeriodPartsCount); for (NSUInteger insertionIndex = toNumberAndPeriodPartsCount; insertionIndex < toNumberAndPeriodPartsCount + partsCountDifference; insertionIndex++) { SUCharacterType typeA = [self typeOfCharacter:fromParts[insertionIndex]]; if (typeA == kPeriodSeparatorType) { [toParts insertObject:@"." atIndex:insertionIndex]; } else if (typeA == kNumberType) { [toParts insertObject:@"0" atIndex:insertionIndex]; } else { // It should not be possible to get here assert(false); } } } // If one version starts with "1.0.0" and the other starts with "1.1" we make sure they're balanced // such that the latter version now becomes "1.1.0". This helps ensure that versions like "1.0" and "1.0.0" are equal. - (void)balanceVersionPartsA:(NSMutableArray<NSString *> *)partsA partsB:(NSMutableArray<NSString *> *)partsB SPU_OBJC_DIRECT { NSUInteger partANumberAndPeriodPartsCount = [self countOfNumberAndPeriodStartingParts:partsA]; NSUInteger partBNumberAndPeriodPartsCount = [self countOfNumberAndPeriodStartingParts:partsB]; if (partANumberAndPeriodPartsCount > partBNumberAndPeriodPartsCount) { [self addNumberAndPeriodPartsToParts:partsB toNumberAndPeriodPartsCount:partBNumberAndPeriodPartsCount fromParts:partsA fromNumberAndPeriodPartsCount:partANumberAndPeriodPartsCount]; } else if (partBNumberAndPeriodPartsCount > partANumberAndPeriodPartsCount) { [self addNumberAndPeriodPartsToParts:partsA toNumberAndPeriodPartsCount:partANumberAndPeriodPartsCount fromParts:partsB fromNumberAndPeriodPartsCount:partBNumberAndPeriodPartsCount]; } } - (NSComparisonResult)compareVersion:(NSString *)versionA toVersion:(NSString *)versionB { NSMutableArray<NSString *> *splitPartsA = [self splitVersionString:versionA]; NSMutableArray<NSString *> *splitPartsB = [self splitVersionString:versionB]; [self balanceVersionPartsA:splitPartsA partsB:splitPartsB]; NSArray<NSString *> *partsA = splitPartsA; NSArray<NSString *> *partsB = splitPartsB; NSString *partA, *partB; NSUInteger i, n; long long valueA, valueB; SUCharacterType typeA, typeB; n = MIN([partsA count], [partsB count]); for (i = 0; i < n; ++i) { partA = [partsA objectAtIndex:i]; partB = [partsB objectAtIndex:i]; typeA = [self typeOfCharacter:partA]; typeB = [self typeOfCharacter:partB]; // Compare types if ([self isEqualCharacterTypeClassForTypeA:typeA typeB:typeB]) { // Same type; we can compare if (typeA == kNumberType) { valueA = [partA longLongValue]; valueB = [partB longLongValue]; if (valueA > valueB) { return NSOrderedDescending; } else if (valueA < valueB) { return NSOrderedAscending; } } else if (typeA == kStringType) { NSComparisonResult result = [partA compare:partB]; if (result != NSOrderedSame) { return result; } } } else { // Not the same type? Now we have to do some validity checking if (typeA != kStringType && typeB == kStringType) { // typeA wins return NSOrderedDescending; } else if (typeA == kStringType && typeB != kStringType) { // typeB wins return NSOrderedAscending; } else { // One is a number and the other is a period. The period is invalid if (typeA == kNumberType) { return NSOrderedDescending; } else { return NSOrderedAscending; } } } } // The versions are equal up to the point where they both still have parts // Lets check to see if one is larger than the other if ([partsA count] != [partsB count]) { // Yep. Lets get the next part of the larger // n holds the index of the part we want. NSString *missingPart; SUCharacterType missingType; NSComparisonResult shorterResult, largerResult; if ([partsA count] > [partsB count]) { missingPart = [partsA objectAtIndex:n]; shorterResult = NSOrderedAscending; largerResult = NSOrderedDescending; } else { missingPart = [partsB objectAtIndex:n]; shorterResult = NSOrderedDescending; largerResult = NSOrderedAscending; } missingType = [self typeOfCharacter:missingPart]; // Check the type if (missingType == kStringType) { // It's a string. Shorter version wins return shorterResult; } else { // It's a number/period. Larger version wins return largerResult; } } // The 2 strings are identical return NSOrderedSame; } @end ================================================ FILE: Sparkle/SUStatus.xib ================================================ <?xml version="1.0" encoding="UTF-8"?> <document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="24764" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES"> <dependencies> <deployment identifier="macosx"/> <plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="24764"/> <capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/> </dependencies> <objects> <customObject id="-2" userLabel="File's Owner" customClass="SUStatusController"> <connections> <outlet property="_actionButton" destination="12" id="0jE-Zn-L18"/> <outlet property="_progressBar" destination="11" id="Cwm-99-fCT"/> <outlet property="_statusTextField" destination="16" id="oOr-uc-6cX"/> <outlet property="window" destination="5" id="25"/> </connections> </customObject> <customObject id="-1" userLabel="First Responder" customClass="FirstResponder"/> <customObject id="-3" userLabel="Application" customClass="NSObject"/> <window identifier="SUStatus" title="Set in Code" allowsToolTipsWhenApplicationIsInactive="NO" autorecalculatesKeyViewLoop="NO" releasedWhenClosed="NO" visibleAtLaunch="NO" animationBehavior="default" id="5" userLabel="Window"> <windowStyleMask key="styleMask" titled="YES"/> <windowCollectionBehavior key="collectionBehavior" fullScreenAuxiliary="YES"/> <windowPositionMask key="initialPositionMask" leftStrut="YES" rightStrut="YES" topStrut="YES" bottomStrut="YES"/> <rect key="contentRect" x="200" y="222" width="400" height="107"/> <rect key="screenRect" x="0.0" y="0.0" width="1470" height="923"/> <value key="minSize" type="size" width="213" height="107"/> <view key="contentView" misplaced="YES" id="6"> <rect key="frame" x="0.0" y="0.0" width="400" height="107"/> <autoresizingMask key="autoresizingMask"/> <subviews> <imageView translatesAutoresizingMaskIntoConstraints="NO" id="7"> <rect key="frame" x="20" y="42" width="64" height="64"/> <constraints> <constraint firstAttribute="height" constant="64" id="BT1-iv-l2H"/> <constraint firstAttribute="width" constant="64" id="eYK-yn-PVe"/> </constraints> <imageCell key="cell" refusesFirstResponder="YES" alignment="left" imageScaling="axesIndependently" image="NSApplicationIcon" id="53"/> <connections> <binding destination="-2" name="value" keyPath="applicationIcon" id="9"/> </connections> </imageView> <customView translatesAutoresizingMaskIntoConstraints="NO" id="E7u-iB-1VW" userLabel="Status text and progress"> <rect key="frame" x="92" y="52" width="288" height="44"/> <subviews> <textField focusRingType="none" verticalHuggingPriority="750" horizontalCompressionResistancePriority="1000" translatesAutoresizingMaskIntoConstraints="NO" id="8"> <rect key="frame" x="-2" y="28" width="292" height="16"/> <textFieldCell key="cell" sendsActionOnEndEditing="YES" title="Status Text (set by loc. string in code)" id="54"> <font key="font" metaFont="systemBold"/> <color key="textColor" name="controlTextColor" catalog="System" colorSpace="catalog"/> <color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/> </textFieldCell> <connections> <binding destination="-2" name="value" keyPath="title" id="26"/> </connections> </textField> <progressIndicator wantsLayer="YES" verticalHuggingPriority="750" maxValue="100" bezeled="NO" indeterminate="YES" style="bar" translatesAutoresizingMaskIntoConstraints="NO" id="11"> <rect key="frame" x="0.0" y="0.0" width="288" height="20"/> <connections> <binding destination="-2" name="maxValue" keyPath="maxProgressValue" id="13"/> <binding destination="-2" name="value" keyPath="progressValue" previousBinding="13" id="27"/> </connections> </progressIndicator> </subviews> <constraints> <constraint firstAttribute="trailing" secondItem="8" secondAttribute="trailing" id="9Rq-vq-cJo"/> <constraint firstAttribute="bottom" secondItem="11" secondAttribute="bottom" id="PDQ-9x-h82"/> <constraint firstItem="8" firstAttribute="top" secondItem="E7u-iB-1VW" secondAttribute="top" id="Rtg-8u-ed1"/> <constraint firstAttribute="trailing" secondItem="11" secondAttribute="trailing" id="YFJ-Gi-yRc"/> <constraint firstItem="8" firstAttribute="leading" secondItem="E7u-iB-1VW" secondAttribute="leading" id="f16-mO-wfq"/> <constraint firstItem="11" firstAttribute="leading" secondItem="E7u-iB-1VW" secondAttribute="leading" id="jvG-0L-tcl"/> <constraint firstItem="11" firstAttribute="top" secondItem="8" secondAttribute="bottom" constant="8" symbolic="YES" id="mKu-OU-ujj"/> </constraints> </customView> <button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="12"> <rect key="frame" x="280" y="20" width="100" height="24"/> <buttonCell key="cell" type="push" title="Button" bezelStyle="rounded" alignment="center" borderStyle="border" inset="2" id="55"> <behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/> <font key="font" metaFont="system"/> </buttonCell> <constraints> <constraint firstAttribute="width" relation="greaterThanOrEqual" constant="100" id="3tI-Ef-LLb"/> </constraints> <accessibility identifier="SUStatusButton"/> <connections> <binding destination="-2" name="title" keyPath="buttonTitle" id="21"/> </connections> </button> <textField focusRingType="none" verticalHuggingPriority="750" horizontalCompressionResistancePriority="1000" translatesAutoresizingMaskIntoConstraints="NO" id="16"> <rect key="frame" x="90" y="24" width="146" height="16"/> <textFieldCell key="cell" sendsActionOnEndEditing="YES" title="Small System Font Text" id="56"> <font key="font" metaFont="system"/> <color key="textColor" name="controlTextColor" catalog="System" colorSpace="catalog"/> <color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/> </textFieldCell> <connections> <binding destination="-2" name="value" keyPath="statusText" id="17"/> <binding destination="-2" name="hidden" keyPath="statusText" id="33"> <dictionary key="options"> <string key="NSValueTransformerName">NSIsNil</string> </dictionary> </binding> </connections> </textField> </subviews> <constraints> <constraint firstAttribute="trailing" secondItem="E7u-iB-1VW" secondAttribute="trailing" constant="20" symbolic="YES" id="3uo-mB-Q9M"/> <constraint firstItem="7" firstAttribute="top" secondItem="6" secondAttribute="top" constant="4" id="9Mg-Ac-X6m"/> <constraint firstItem="E7u-iB-1VW" firstAttribute="centerY" secondItem="7" secondAttribute="centerY" id="Jcg-BS-tJ1"/> <constraint firstItem="16" firstAttribute="leading" secondItem="7" secondAttribute="trailing" constant="8" symbolic="YES" id="LTo-e9-myp"/> <constraint firstItem="7" firstAttribute="leading" secondItem="6" secondAttribute="leading" constant="20" symbolic="YES" id="ZLG-dr-gmv"/> <constraint firstItem="12" firstAttribute="top" secondItem="E7u-iB-1VW" secondAttribute="bottom" constant="8" id="dHH-r0-at5"/> <constraint firstItem="16" firstAttribute="firstBaseline" secondItem="12" secondAttribute="firstBaseline" id="dHQ-hW-3QN"/> <constraint firstAttribute="bottom" secondItem="12" secondAttribute="bottom" constant="20" symbolic="YES" id="iY3-LJ-ivz"/> <constraint firstItem="12" firstAttribute="leading" relation="greaterThanOrEqual" secondItem="16" secondAttribute="trailing" constant="25" id="k07-gJ-v9t"/> <constraint firstAttribute="trailing" secondItem="12" secondAttribute="trailing" constant="20" symbolic="YES" id="onJ-J5-mra"/> <constraint firstItem="E7u-iB-1VW" firstAttribute="leading" secondItem="7" secondAttribute="trailing" constant="8" symbolic="YES" id="wiO-j2-ud9"/> </constraints> </view> <point key="canvasLocation" x="-158" y="132"/> </window> </objects> <resources> <image name="NSApplicationIcon" width="32" height="32"/> </resources> </document> ================================================ FILE: Sparkle/SUStatusController.h ================================================ // // SUStatusController.h // Sparkle // // Created by Andy Matuschak on 3/14/06. // Copyright 2006 Andy Matuschak. All rights reserved. // #if SPARKLE_BUILD_UI_BITS || !BUILDING_SPARKLE #ifndef SUSTATUSCONTROLLER_H #define SUSTATUSCONTROLLER_H #import <Cocoa/Cocoa.h> @class SUHost; @interface SUStatusController : NSWindowController // These three properties are connected via bindings @property (nonatomic, copy) NSString *statusText; @property (nonatomic) double progressValue; @property (nonatomic) double maxProgressValue; @property (nonatomic, getter=isButtonEnabled, direct) BOOL buttonEnabled; - (instancetype)initWithHost:(SUHost *)aHost windowTitle:(NSString *)windowTitle centerPointValue:(NSValue *)centerPointValue minimizable:(BOOL)minimizable closable:(BOOL)closable SPU_OBJC_DIRECT; // Pass 0 for the max progress value to get an indeterminate progress bar. // Pass nil for the status text to not show it. - (void)beginActionWithTitle:(NSString *)title maxProgressValue:(double)maxProgressValue statusText:(NSString *)statusText SPU_OBJC_DIRECT; // If isDefault is YES, the button's key equivalent will be \r. - (void)setButtonTitle:(NSString *)buttonTitle target:(id)target action:(SEL)action isDefault:(BOOL)isDefault accessibilityIdentifier:(NSString *)accessibilityIdentifier SPU_OBJC_DIRECT; @end #endif #endif ================================================ FILE: Sparkle/SUStatusController.m ================================================ // // SUStatusController.m // Sparkle // // Created by Andy Matuschak on 3/14/06. // Copyright 2006 Andy Matuschak. All rights reserved. // #if SPARKLE_BUILD_UI_BITS || !BUILDING_SPARKLE #import "SUStatusController.h" #import "SUHost.h" #import "SUApplicationInfo.h" #import "SULocalizations.h" #import "SUTouchBarButtonGroup.h" static NSString *const SUStatusControllerTouchBarIdentifier = @"" SPARKLE_BUNDLE_IDENTIFIER ".SUStatusController"; @interface SUStatusController () <NSTouchBarDelegate> // These properties are used for bindings @property (nonatomic, copy) NSString *title; @property (nonatomic, copy) NSString *buttonTitle; @end @implementation SUStatusController { NSString *_windowTitle; NSValue *_centerPointValue; NSString *_title; NSString *_buttonTitle; SUHost *_host; NSButton *_touchBarButton; IBOutlet NSButton *_actionButton; IBOutlet NSTextField *_statusTextField; IBOutlet NSProgressIndicator *_progressBar; BOOL _minimizable; BOOL _closable; } @synthesize title = _title; @synthesize buttonTitle = _buttonTitle; @synthesize progressValue = _progressValue; @synthesize maxProgressValue = _maxProgressValue; @synthesize statusText = _statusText; - (instancetype)initWithHost:(SUHost *)aHost windowTitle:(NSString *)windowTitle centerPointValue:(NSValue *)centerPointValue minimizable:(BOOL)minimizable closable:(BOOL)closable { self = [super initWithWindowNibName:@"SUStatus" owner:self]; if (self) { _host = aHost; _centerPointValue = centerPointValue; _minimizable = minimizable; _closable = closable; _windowTitle = [windowTitle copy]; [self setShouldCascadeWindows:NO]; } return self; } - (NSString *)description { return [NSString stringWithFormat:@"%@ <%@>", [self class], _host.bundlePath]; } - (void)windowDidLoad { NSWindow *window = self.window; NSRect windowFrame = window.frame; if (_centerPointValue != nil) { NSPoint centerPoint = _centerPointValue.pointValue; [window setFrameOrigin:NSMakePoint(centerPoint.x - windowFrame.size.width / 2.0, centerPoint.y - windowFrame.size.height / 2.0)]; } else { [window center]; } if (_minimizable) { window.styleMask = (NSWindowStyleMask)(window.styleMask | NSWindowStyleMaskMiniaturizable); } if (_closable) { window.styleMask = (NSWindowStyleMask)(window.styleMask | NSWindowStyleMaskClosable); } [_progressBar setUsesThreadedAnimation:YES]; [_statusTextField setFont:[NSFont monospacedDigitSystemFontOfSize:0 weight:NSFontWeightRegular]]; if (@available(macOS 16, *)) { _actionButton.controlSize = NSControlSizeLarge; } window.title = _windowTitle; } - (NSImage *)applicationIcon { return [SUApplicationInfo bestIconForHost:_host]; } - (void)beginActionWithTitle:(NSString *)aTitle maxProgressValue:(double)aMaxProgressValue statusText:(NSString *)aStatusText { self.title = aTitle; self.maxProgressValue = aMaxProgressValue; self.statusText = aStatusText; } - (void)setButtonTitle:(NSString *)aButtonTitle target:(id)target action:(SEL)action isDefault:(BOOL)isDefault accessibilityIdentifier:(NSString *)accessibilityIdentifier { self.buttonTitle = aButtonTitle; _actionButton.accessibilityIdentifier = [accessibilityIdentifier copy]; [self window]; [_actionButton sizeToFit]; // Except we're going to add 15 px for padding. [_actionButton setFrameSize:NSMakeSize(_actionButton.frame.size.width + 15, _actionButton.frame.size.height)]; // Now we have to move it over so that it's always 15px from the side of the window. [_actionButton setFrameOrigin:NSMakePoint([[self window] frame].size.width - 15 - _actionButton.frame.size.width, _actionButton.frame.origin.y)]; // Redisplay superview to clean up artifacts [[_actionButton superview] display]; [_actionButton setTarget:target]; [_actionButton setAction:action]; [_actionButton setKeyEquivalent:isDefault ? @"\r" : @""]; // False warning #pragma clang diagnostic push #pragma clang diagnostic ignored "-Warc-repeated-use-of-weak" _touchBarButton.target = _actionButton.target; #pragma clang diagnostic pop _touchBarButton.action = _actionButton.action; _touchBarButton.keyEquivalent = _actionButton.keyEquivalent; // 06/05/2008 Alex: Avoid a crash when cancelling during the extraction [self setButtonEnabled:(target != nil)]; } - (BOOL)progressBarShouldAnimate { return YES; } - (void)setButtonEnabled:(BOOL)enabled { [_actionButton setEnabled:enabled]; } - (BOOL)isButtonEnabled { return [_actionButton isEnabled]; } - (void)setMaxProgressValue:(double)value { if (value < 0.0) value = 0.0; _maxProgressValue = value; [self setProgressValue:0.0]; [_progressBar setIndeterminate:(value == 0.0)]; [_progressBar startAnimation:self]; [_progressBar setUsesThreadedAnimation:YES]; } - (NSTouchBar *)makeTouchBar { NSTouchBar *touchBar = [[NSTouchBar alloc] init]; touchBar.defaultItemIdentifiers = @[ SUStatusControllerTouchBarIdentifier,]; touchBar.principalItemIdentifier = SUStatusControllerTouchBarIdentifier; touchBar.delegate = self; return touchBar; } - (NSTouchBarItem *)touchBar:(NSTouchBar * __unused)touchBar makeItemForIdentifier:(NSTouchBarItemIdentifier)identifier { if ([identifier isEqualToString:SUStatusControllerTouchBarIdentifier]) { NSCustomTouchBarItem *item = [[NSCustomTouchBarItem alloc] initWithIdentifier:identifier]; SUTouchBarButtonGroup *group = [[SUTouchBarButtonGroup alloc] initByReferencingButtons:@[_actionButton,]]; item.viewController = group; _touchBarButton = group.buttons.firstObject; [_touchBarButton bind:@"title" toObject:_actionButton withKeyPath:@"title" options:nil]; [_touchBarButton bind:@"enabled" toObject:_actionButton withKeyPath:@"enabled" options:nil]; return item; } return nil; } @end #endif ================================================ FILE: Sparkle/SUSystemProfiler.h ================================================ // // SUSystemProfiler.h // Sparkle // // Created by Andy Matuschak on 12/22/07. // Copyright 2007 Andy Matuschak. All rights reserved. // #ifndef SUSYSTEMPROFILER_H #define SUSYSTEMPROFILER_H #import <Foundation/Foundation.h> NS_ASSUME_NONNULL_BEGIN @class SUHost; SPU_OBJC_DIRECT_MEMBERS @interface SUSystemProfiler : NSObject + (NSArray<NSDictionary<NSString *, NSString *> *> *)systemProfileArrayForHost:(SUHost *)host; @end NS_ASSUME_NONNULL_END #endif ================================================ FILE: Sparkle/SUSystemProfiler.m ================================================ // // SUSystemProfiler.m // Sparkle // // Created by Andy Matuschak on 12/22/07. // Copyright 2007 Andy Matuschak. All rights reserved. // Adapted from Sparkle+, by Tom Harrington. // #import "SUSystemProfiler.h" #import "SUHost.h" #import "SUOperatingSystem.h" #include <sys/sysctl.h> #import "SPUUpdaterDelegate.h" #import "SULocalizations.h" #include "AppKitPrevention.h" NSString *const SUSystemProfilerApplicationNameKey = @"appName"; NSString *const SUSystemProfilerApplicationVersionKey = @"appVersion"; NSString *const SUSystemProfilerCPU64bitKey = @"cpu64bit"; NSString *const SUSystemProfilerCPUCountKey = @"ncpu"; NSString *const SUSystemProfilerCPUFrequencyKey = @"cpuFreqMHz"; NSString *const SUSystemProfilerCPUTypeKey = @"cputype"; NSString *const SUSystemProfilerCPUSubtypeKey = @"cpusubtype"; NSString *const SUSystemProfilerHardwareModelKey = @"model"; NSString *const SUSystemProfilerMemoryKey = @"ramMB"; NSString *const SUSystemProfilerOperatingSystemVersionKey = @"osVersion"; NSString *const SUSystemProfilerPreferredLanguageKey = @"lang"; @implementation SUSystemProfiler + (NSArray<NSDictionary<NSString *, NSString *> *> *)systemProfileArrayForHost:(SUHost *)host { #if SPARKLE_COPY_LOCALIZATIONS NSBundle *sparkleBundle = SUSparkleBundle(); #endif // Gather profile information and append it to the URL. NSMutableArray<NSDictionary<NSString *, NSString *> *> *profileArray = [NSMutableArray array]; NSArray *profileDictKeys = @[@"key", @"displayKey", @"value", @"displayValue"]; int error = 0; int value = 0; size_t length = sizeof(value); // OS version NSString *currentSystemVersion = [SUOperatingSystem systemVersionString]; if (currentSystemVersion != nil) { [profileArray addObject:[NSDictionary dictionaryWithObjects:@[SUSystemProfilerOperatingSystemVersionKey, SULocalizedStringFromTableInBundle(@"OS Version", SPARKLE_TABLE, sparkleBundle, nil), currentSystemVersion, currentSystemVersion] forKeys:profileDictKeys]]; } // CPU type (decoder info for values found here is in mach/machine.h) error = sysctlbyname("hw.cputype", &value, &length, NULL, 0); int cpuType = -1; if (error == 0) { // Only the lower 24 bits of sysctl hw.cputype values contain the CPU type. On Macs with ARM processor, one of the top eight bits may be set. cpuType = value & (int)~CPU_ARCH_MASK; NSString *visibleCPUType; switch (cpuType) { case CPU_TYPE_ARM: visibleCPUType = @"ARM"; break; case CPU_TYPE_X86: visibleCPUType = @"Intel"; break; case CPU_TYPE_POWERPC: visibleCPUType = @"PowerPC"; break; default: visibleCPUType = @"Other"; break; } [profileArray addObject:[NSDictionary dictionaryWithObjects:@[SUSystemProfilerCPUTypeKey, SULocalizedStringFromTableInBundle(@"CPU Type", SPARKLE_TABLE, sparkleBundle, nil), [NSString stringWithFormat:@"%d", value], visibleCPUType] forKeys:profileDictKeys]]; } error = sysctlbyname("hw.cpu64bit_capable", &value, &length, NULL, 0); if (error != 0) { error = sysctlbyname("hw.optional.x86_64", &value, &length, NULL, 0); //x86 specific } if (error != 0) { error = sysctlbyname("hw.optional.64bitops", &value, &length, NULL, 0); //PPC specific } BOOL is64bit = NO; if (error == 0) { is64bit = value == 1; [profileArray addObject:[NSDictionary dictionaryWithObjects:@[SUSystemProfilerCPU64bitKey, SULocalizedStringFromTableInBundle(@"CPU is 64-Bit?", SPARKLE_TABLE, sparkleBundle, nil), [NSString stringWithFormat:@"%d", is64bit], is64bit ? SULocalizedStringFromTableInBundle(@"Yes", SPARKLE_TABLE, sparkleBundle, nil) : SULocalizedStringFromTableInBundle(@"No", SPARKLE_TABLE, sparkleBundle, nil)] forKeys:profileDictKeys]]; } error = sysctlbyname("hw.cpusubtype", &value, &length, NULL, 0); if (error == 0) { NSString *visibleCPUSubType; if (cpuType == CPU_TYPE_X86) { // Intel // TODO: other Intel processors, like Core i7, i5, i3, Xeon? visibleCPUSubType = is64bit ? @"Intel Core 2" : @"Intel Core"; // If anyone knows how to tell a Core Duo from a Core Solo, please email tph@atomicbird.com } else if (cpuType == CPU_TYPE_POWERPC) { // PowerPC switch (value) { case CPU_SUBTYPE_POWERPC_750: visibleCPUSubType=@"G3"; break; case CPU_SUBTYPE_POWERPC_7400: case CPU_SUBTYPE_POWERPC_7450: visibleCPUSubType=@"G4"; break; case CPU_SUBTYPE_POWERPC_970: visibleCPUSubType=@"G5"; break; default: visibleCPUSubType=@"Other"; break; } } else if (cpuType == CPU_TYPE_ARM) { switch (value) { case CPU_SUBTYPE_ARM64E: visibleCPUSubType=@"ARM64E"; break; default: visibleCPUSubType = @"Other"; break; } } else { visibleCPUSubType = @"Other"; } [profileArray addObject:[NSDictionary dictionaryWithObjects:@[SUSystemProfilerCPUSubtypeKey, SULocalizedStringFromTableInBundle(@"CPU Subtype", SPARKLE_TABLE, sparkleBundle, nil), [NSString stringWithFormat:@"%d", value], visibleCPUSubType] forKeys:profileDictKeys]]; } error = sysctlbyname("hw.model", NULL, &length, NULL, 0); if (error == 0) { char *cpuModel = (char *)malloc(sizeof(char) * length); if (cpuModel != NULL) { error = sysctlbyname("hw.model", cpuModel, &length, NULL, 0); if (error == 0) { NSString *rawModelName = @(cpuModel); NSString *visibleModelName = rawModelName; [profileArray addObject:[NSDictionary dictionaryWithObjects:@[SUSystemProfilerHardwareModelKey, SULocalizedStringFromTableInBundle(@"Mac Model", SPARKLE_TABLE, sparkleBundle, nil), rawModelName, visibleModelName] forKeys:profileDictKeys]]; } free(cpuModel); } } // Number of CPUs error = sysctlbyname("hw.ncpu", &value, &length, NULL, 0); if (error == 0) { NSString *stringValue = [NSString stringWithFormat:@"%d", value]; [profileArray addObject:[NSDictionary dictionaryWithObjects:@[SUSystemProfilerCPUCountKey, SULocalizedStringFromTableInBundle(@"Number of CPUs", SPARKLE_TABLE, sparkleBundle, nil), stringValue, stringValue] forKeys:profileDictKeys]]; } // User preferred language NSUserDefaults *defs = [NSUserDefaults standardUserDefaults]; NSArray *languages = [defs objectForKey:@"AppleLanguages"]; if ([languages count] > 0) { [profileArray addObject:[NSDictionary dictionaryWithObjects:@[SUSystemProfilerPreferredLanguageKey, SULocalizedStringFromTableInBundle(@"Preferred Language", SPARKLE_TABLE, sparkleBundle, nil), [languages objectAtIndex:0], [languages objectAtIndex:0]] forKeys:profileDictKeys]]; } // Application sending the request NSString *appName = [host name]; if (appName) { [profileArray addObject:[NSDictionary dictionaryWithObjects:@[SUSystemProfilerApplicationNameKey, SULocalizedStringFromTableInBundle(@"Application Name", SPARKLE_TABLE, sparkleBundle, nil), appName, appName] forKeys:profileDictKeys]]; } NSString *appVersion = [host version]; if (appVersion) { [profileArray addObject:[NSDictionary dictionaryWithObjects:@[SUSystemProfilerApplicationVersionKey, SULocalizedStringFromTableInBundle(@"Application Version", SPARKLE_TABLE, sparkleBundle, nil), appVersion, appVersion] forKeys:profileDictKeys]]; } // Number of displays? // CPU speed unsigned long hz; size_t hz_size = sizeof(unsigned long); if (sysctlbyname("hw.cpufrequency", &hz, &hz_size, NULL, 0) == 0) { unsigned long mhz = hz / 1000000; NSString *stringValue = [NSString stringWithFormat:@"%lu", mhz]; [profileArray addObject:[NSDictionary dictionaryWithObjects:@[SUSystemProfilerCPUFrequencyKey, SULocalizedStringFromTableInBundle(@"CPU Speed (MHz)", SPARKLE_TABLE, sparkleBundle, nil), stringValue, stringValue] forKeys:profileDictKeys]]; } // amount of RAM unsigned long bytes; size_t bytes_size = sizeof(unsigned long); if (sysctlbyname("hw.memsize", &bytes, &bytes_size, NULL, 0) == 0) { double megabytes = (double)bytes / (1024. * 1024.); NSString *stringValue = [NSString stringWithFormat:@"%lu", (unsigned long)megabytes]; [profileArray addObject:[NSDictionary dictionaryWithObjects:@[SUSystemProfilerMemoryKey, SULocalizedStringFromTableInBundle(@"Memory (MB)", SPARKLE_TABLE, sparkleBundle, nil), stringValue, stringValue] forKeys:profileDictKeys]]; } return [profileArray copy]; } @end ================================================ FILE: Sparkle/SUTextViewReleaseNotesView.h ================================================ // // SUTextViewReleaseNotesView.h // Sparkle // // Created on 9/11/22. // Copyright © 2022 Sparkle Project. All rights reserved. // #if SPARKLE_BUILD_UI_BITS #import <Foundation/Foundation.h> #import "SUReleaseNotesView.h" @protocol SPUStandardUserDriverDelegate; @class SUAppcastItem; @class SUHost; NS_ASSUME_NONNULL_BEGIN SPU_OBJC_DIRECT_MEMBERS @interface SUTextViewReleaseNotesView : NSObject <SUReleaseNotesView> - (instancetype)initWithFontPointSize:(int)fontPointSize appcastItem:(SUAppcastItem *)appcastItem host:(SUHost *)host delegate:(id<SPUStandardUserDriverDelegate>)delegate prefersMarkdown:(BOOL)prefersMarkdown customAllowedURLSchemes:(NSArray<NSString *> *)customAllowedURLSchemes; @end NS_ASSUME_NONNULL_END #endif ================================================ FILE: Sparkle/SUTextViewReleaseNotesView.m ================================================ // // SUTextViewReleaseNotesView.m // Sparkle // // Created on 9/11/22. // Copyright © 2022 Sparkle Project. All rights reserved. // #if SPARKLE_BUILD_UI_BITS #import "SUTextViewReleaseNotesView.h" #import "SUReleaseNotesCommon.h" #import "SPUStandardUserDriverDelegate.h" #import "SULog.h" #import "SUErrors.h" #import "SUHost.h" #import <AppKit/AppKit.h> @interface SUTextViewReleaseNotesView () <NSTextViewDelegate> @end @implementation SUTextViewReleaseNotesView { NSScrollView *_scrollView; NSTextView *_textView; #if DEBUG id _textViewSwitchedToTextKit1Observer; #endif NSArray<NSString *> *_customAllowedURLSchemes; SUAppcastItem *_updateItem; SUHost *_host; __weak id<SPUStandardUserDriverDelegate> _delegate; int _fontPointSize; BOOL _prefersMarkdown; } - (instancetype)initWithFontPointSize:(int)fontPointSize appcastItem:(SUAppcastItem *)appcastItem host:(SUHost *)host delegate:(id<SPUStandardUserDriverDelegate>)delegate prefersMarkdown:(BOOL)prefersMarkdown customAllowedURLSchemes:(NSArray<NSString *> *)customAllowedURLSchemes { self = [super init]; if (self != nil) { _fontPointSize = fontPointSize; _customAllowedURLSchemes = customAllowedURLSchemes; _prefersMarkdown = prefersMarkdown; _scrollView = [[NSScrollView alloc] initWithFrame:NSZeroRect]; _updateItem = appcastItem; _host = host; _delegate = delegate; // On macOS 12.7, TextKit 2 is very buggy in handling our simple text with NSParagraphStyle attributes. // So even though macOS 12 supports TextKit 2 we do not use it there. // My development machines are currently on macOS 26 so I know TextKit 2 works well there. // macOS 13 - 15 requires more testing if we care to make the switch there. if (@available(macOS 16, *)) { // Create NSTextView using TextKit 2 // https://developer.apple.com/documentation/appkit/nstextview/1449347-initwithframe NSTextContainer *textContainer = [[NSTextContainer alloc] initWithContainerSize:NSMakeSize(0, (CGFloat)FLT_MAX)]; textContainer.widthTracksTextView = YES; NSTextLayoutManager *textLayoutManager = [[NSTextLayoutManager alloc] init]; textLayoutManager.textContainer = textContainer; NSTextContentStorage *textContentStorage = [[NSTextContentStorage alloc] init]; [textContentStorage addTextLayoutManager:textLayoutManager]; _textView = [[NSTextView alloc] initWithFrame:NSZeroRect textContainer:textLayoutManager.textContainer]; #if DEBUG _textViewSwitchedToTextKit1Observer = [NSNotificationCenter.defaultCenter addObserverForName:NSTextViewDidSwitchToNSLayoutManagerNotification object:_textView queue:nil usingBlock:^(NSNotification * _Nonnull __unused notification) { SULog(SULogLevelError, @"Error: Plain text release notes text view switched to TextKit 1. This should not happen. Was some TextKit 1 API called that is causing this?"); }]; #endif } else { _textView = [[NSTextView alloc] initWithFrame:NSZeroRect]; } _textView.delegate = self; _scrollView.documentView = _textView; } return self; } #if DEBUG - (void)dealloc { [NSNotificationCenter.defaultCenter removeObserver:_textViewSwitchedToTextKit1Observer]; } #endif - (NSView *)view { return _scrollView; } static void processMarkdownFragmentAttributedString(NSAttributedString *fragmentAttributedString, NSMutableAttributedString *outputAttributedSubString, NSMutableParagraphStyle *paragraphStyle, BOOL canProcessListItem, NSMutableSet<NSNumber *> *previousVisitedListItemIntents, NSPresentationIntent *intent, NSFont *inputParagraphFont, NSFont *monospacedParagraphFont, NSAttributedString *tabAttributedString, NSAttributedString *newlineAttributedString, NSAttributedString *listBulletAttributedString) API_AVAILABLE(macos(12.0)) { // Pre-pass processing of intent // This info must be computed before processing parent intent BOOL isListItem = NO; NSFont *font = inputParagraphFont; switch (intent.intentKind) { case NSPresentationIntentKindHeader: switch (intent.headerLevel) { case 1: font = [NSFont boldSystemFontOfSize:(CGFloat)inputParagraphFont.pointSize * 1.5]; break; case 2: font = [NSFont boldSystemFontOfSize:(CGFloat)inputParagraphFont.pointSize * 1.3]; break; case 3: font = [NSFont boldSystemFontOfSize:(CGFloat)inputParagraphFont.pointSize * 1.2]; break; default: font = [NSFont boldSystemFontOfSize:(CGFloat)inputParagraphFont.pointSize * 1.1]; break; } break; case NSPresentationIntentKindListItem: isListItem = YES; break; case NSPresentationIntentKindParagraph: case NSPresentationIntentKindThematicBreak: case NSPresentationIntentKindBlockQuote: case NSPresentationIntentKindCodeBlock: case NSPresentationIntentKindOrderedList: case NSPresentationIntentKindUnorderedList: case NSPresentationIntentKindTable: case NSPresentationIntentKindTableHeaderRow: case NSPresentationIntentKindTableRow: case NSPresentationIntentKindTableCell: break; } // Process parent intent if available // A paragraph's intent may be a list item, or a block quote for example. A header's parent intent could be a block quote. // In these cases, we may pre-append attributed string to the output before processing current intent. NSPresentationIntent *parentIntent = intent.parentIntent; if (parentIntent != nil) { processMarkdownFragmentAttributedString(fragmentAttributedString, outputAttributedSubString, paragraphStyle, canProcessListItem && !isListItem, previousVisitedListItemIntents, parentIntent, font, monospacedParagraphFont, tabAttributedString, newlineAttributedString, listBulletAttributedString); } // Process the current intent switch (intent.intentKind) { case NSPresentationIntentKindHeader: { CGFloat paragraphSpacing = font.pointSize * 0.8; paragraphStyle.paragraphSpacingBefore += paragraphSpacing; paragraphStyle.paragraphSpacing += paragraphSpacing; NSMutableAttributedString *headerAttributedString = [fragmentAttributedString mutableCopy]; [headerAttributedString addAttributes:@{NSFontAttributeName: font} range:NSMakeRange(0, headerAttributedString.length)]; [outputAttributedSubString appendAttributedString:headerAttributedString]; break; } case NSPresentationIntentKindParagraph: { if (parentIntent != nil && parentIntent.intentKind == NSPresentationIntentKindListItem) { // If the parent intent is a list item we don't want to apply paragraphSpacingBefore, // and we'll apply less spacing paragraphStyle.paragraphSpacing += font.pointSize * 0.3; } else { CGFloat paragraphSpacing = font.pointSize * 0.5; paragraphStyle.paragraphSpacing += paragraphSpacing; paragraphStyle.paragraphSpacingBefore += paragraphSpacing; } NSMutableAttributedString *contentAttributedString = [fragmentAttributedString mutableCopy]; [contentAttributedString addAttributes:@{NSFontAttributeName: font} range:NSMakeRange(0, contentAttributedString.length)]; [outputAttributedSubString appendAttributedString:contentAttributedString]; break; } case NSPresentationIntentKindListItem: { // We only process (the innermost first) list item once when we encounter nested lists, // to avoid outputting multiple list bullets // Also avoid processing list items that were processed from previous passes / fragments if (canProcessListItem) { CGFloat firstLineIdentation = (CGFloat)intent.indentationLevel * (font.pointSize * 1.5); paragraphStyle.firstLineHeadIndent += firstLineIdentation; // Advance subsequent lines and text that wraps to next line by next tab interval past the firstLineIdentation CGFloat defaultTabInterval = paragraphStyle.defaultTabInterval; paragraphStyle.headIndent += ceil(firstLineIdentation / defaultTabInterval) * defaultTabInterval; NSNumber *intentIdentity = @(intent.identity); BOOL didVisitListItemFromPreviousPass = [previousVisitedListItemIntents containsObject:intentIdentity]; BOOL insertUnorderedBullet = (parentIntent == nil || parentIntent.intentKind == NSPresentationIntentKindUnorderedList); if (!didVisitListItemFromPreviousPass) { if (insertUnorderedBullet) { [outputAttributedSubString appendAttributedString:listBulletAttributedString]; } else { NSString *ordinalStringWithSpacing = [NSString stringWithFormat:@"%ld.", intent.ordinal]; NSAttributedString *listItemAttributedString = [[NSAttributedString alloc] initWithString:ordinalStringWithSpacing attributes:@{NSFontAttributeName: font}]; [outputAttributedSubString appendAttributedString:listItemAttributedString]; } [previousVisitedListItemIntents addObject:intentIdentity]; } [outputAttributedSubString appendAttributedString:tabAttributedString]; } break; } case NSPresentationIntentKindBlockQuote: { // Advance text that wraps to next line by this divider width // Multiple levels of block quotes will be advanced mulitiple times // Special rendering via text attachments or decorations is not done because // it's complex and may have tradeoffs paragraphStyle.firstLineHeadIndent += paragraphStyle.defaultTabInterval; paragraphStyle.headIndent += paragraphStyle.defaultTabInterval; break; } case NSPresentationIntentKindCodeBlock: { paragraphStyle.paragraphSpacing += font.pointSize * 0.25; // A parent of a code block could be a block quote or list item // It's more correct to use tab rather than leading paragraph indentation in this case [outputAttributedSubString appendAttributedString:tabAttributedString]; // Advance text that wraps to next line by next tab interval paragraphStyle.headIndent += paragraphStyle.defaultTabInterval; NSMutableAttributedString *blockquoteAttributedString = [fragmentAttributedString mutableCopy]; [blockquoteAttributedString addAttributes:@{NSFontAttributeName: monospacedParagraphFont, NSForegroundColorAttributeName: NSColor.labelColor} range:NSMakeRange(0, blockquoteAttributedString.length)]; [outputAttributedSubString appendAttributedString:blockquoteAttributedString]; break; } case NSPresentationIntentKindOrderedList: case NSPresentationIntentKindUnorderedList: // Nothing special is rendered for thematic breaks // Rendering them via text attachments or decorations is complex and has tradeoffs case NSPresentationIntentKindThematicBreak: // Note: TextKit 2 doesn't support NSTextTable // Tables don't show up in release notes often, so they're not that worthwhile supporting case NSPresentationIntentKindTable: case NSPresentationIntentKindTableHeaderRow: case NSPresentationIntentKindTableRow: case NSPresentationIntentKindTableCell: break; } } // Note: this function can be called from a background thread and shouldn't use main-thread only APIs // More decorative rendering for blockquotes and line breaks (i.e. rendering dividers) was tried out, // using a. NSTextAttachmentCell based subclass, or b. NSTextAttachmentViewProvider, or c. adopting custom NSTextViewportLayoutControllerDelegate. // This was ultimatily given up on and they all have various tradeoffs. NSCell based text attachments don't work in Catalyst, // view based attachments take up additional space, and TextKit2 CALayer decorations are hard to (re)size/position correctly. // Also each increases code complexity and risk. In the end, changelogs can can live without these. // Furthermore we currently support TextKit 1 (on older systems) and TextKit 2 so this function needs to handle both paths. static NSAttributedString *formatMarkdownAttributedString(NSAttributedString *originalAttributedString, CGFloat defaultFontPointSize) API_AVAILABLE(macos(12.0)) { // Create our fonts and cache some common attributed strings up front (list bullets, newline) NSFont *paragraphFont = [NSFont systemFontOfSize:defaultFontPointSize]; NSFont *monospacedParagraphFont = [NSFont monospacedSystemFontOfSize:defaultFontPointSize weight:NSFontWeightRegular]; NSMutableAttributedString *outputAttributedString = [[NSMutableAttributedString alloc] init]; NSAttributedString *newlineAttributedString = [[NSAttributedString alloc] initWithString:@"\n"]; NSAttributedString *listBulletAttributedString; { // The bullet character looks too small in the system font, so switch to another font where it's bigger at same point size NSFont *listBulletPreferredFont = [NSFont fontWithName:@"Menlo Regular" size:defaultFontPointSize]; NSFont *listBulletFont = (listBulletPreferredFont != nil) ? listBulletPreferredFont : paragraphFont; listBulletAttributedString = [[NSAttributedString alloc] initWithString:@"•" attributes:@{NSFontAttributeName : listBulletFont}]; } NSAttributedString *tabAttributedString = [[NSAttributedString alloc] initWithString:@"\t" attributes:@{NSFontAttributeName: paragraphFont}]; NSMutableSet<NSNumber *> *previousVisitedListItemIntents = [[NSMutableSet alloc] init]; // Enumerate through every presentation intent fragment and create a new attributed string that we append to the output // Foundation handles formatting some things for us already in the attributed string such as bold/itatlics and hyperlinks, // but we need to handle formatting paragraphs, headers, lists, block quotes, etc in the attributed string. [originalAttributedString enumerateAttribute:NSPresentationIntentAttributeName inRange:NSMakeRange(0, originalAttributedString.length) options:(NSAttributedStringEnumerationOptions)0 usingBlock:^(NSPresentationIntent *intent, NSRange presentationIntentRange, BOOL * _Nonnull __unused stopPresentationIntentEnumeration) { // Split the presentation intent by lines so we treat every line as a separate paragraph so we present them properly (with correct indentation / tabs) // Normally multiple lines aren't in the same paragraph, but this can happen in some cases like code blocks [originalAttributedString.string enumerateSubstringsInRange:presentationIntentRange options:NSStringEnumerationByLines usingBlock:^(NSString * _Nullable __unused substring, NSRange substringRange, NSRange __unused enclosingRange, BOOL * _Nonnull __unused stopLineEnumeration) { // Insert newline after outputting previous paragraph // This check ensures an extra newline is not inserted after the last outputted paragraph if (outputAttributedString.length > 0) { [outputAttributedString appendAttributedString:newlineAttributedString]; } NSAttributedString *fragmentAttributedString = [originalAttributedString attributedSubstringFromRange:substringRange]; // Properties of the paragraph start as 0 and later get incremented based on what is processed NSMutableParagraphStyle *paragraphStyle = [[NSMutableParagraphStyle alloc] init]; paragraphStyle.paragraphSpacingBefore = 0; paragraphStyle.paragraphSpacing = 0; paragraphStyle.headIndent = 0; paragraphStyle.firstLineHeadIndent = 0; // Assume tabs won't be used in headers so we'll just use regular paragraph font size paragraphStyle.tabStops = @[]; paragraphStyle.defaultTabInterval = paragraphFont.pointSize * 1.38; NSUInteger previousOutputLength = outputAttributedString.length; BOOL canProcessListItem = YES; processMarkdownFragmentAttributedString(fragmentAttributedString, outputAttributedString, paragraphStyle, canProcessListItem, previousVisitedListItemIntents, intent, paragraphFont, monospacedParagraphFont, tabAttributedString, newlineAttributedString, listBulletAttributedString); [outputAttributedString addAttributes:@{NSParagraphStyleAttributeName: paragraphStyle} range:NSMakeRange(previousOutputLength, outputAttributedString.length - previousOutputLength)]; }]; }]; return outputAttributedString; } - (void)_loadAttributedString:(NSAttributedString * _Nullable)attributedString completionHandler:(void (^)(NSError * _Nullable))completionHandler SPU_OBJC_DIRECT { if (attributedString == nil) { completionHandler([NSError errorWithDomain:SUSparkleErrorDomain code:SUReleaseNotesError userInfo:@{NSLocalizedDescriptionKey: @"Failed to create attributed string of contents to load"}]); return; } // Give delegate a chance to process and modify the attributed string NSAttributedString *finalAttributedString; id<SPUStandardUserDriverDelegate> delegate = _delegate; if ([(NSObject *)delegate respondsToSelector:@selector(standardUserDriverWillShowReleaseNotesText:forUpdate:withBundleDisplayVersion:bundleVersion:)]) { NSAttributedString *attributedStringFromDelegate = [delegate standardUserDriverWillShowReleaseNotesText:(NSAttributedString * _Nonnull)attributedString forUpdate:_updateItem withBundleDisplayVersion:_host.displayVersion bundleVersion:_host.version]; if (attributedStringFromDelegate != nil) { finalAttributedString = attributedStringFromDelegate; } else { finalAttributedString = attributedString; } } else { finalAttributedString = attributedString; } [_textView.textStorage setAttributedString:finalAttributedString]; completionHandler(nil); } - (void)_loadString:(NSString *)contents baseURL:(nullable NSURL *)baseURL completionHandler:(void (^)(NSError * _Nullable))completionHandler SPU_OBJC_DIRECT { NSSize contentSize = [_scrollView contentSize]; [_textView setFrame:NSMakeRect(0, 0, contentSize.width, contentSize.height)]; [_textView setMinSize:NSMakeSize(0.0, contentSize.height)]; [_textView setMaxSize:NSMakeSize(DBL_MAX, DBL_MAX)]; [_textView setVerticallyResizable:YES]; [_textView setHorizontallyResizable:NO]; [_textView setAutoresizingMask:NSViewWidthSizable]; [_textView setTextContainerInset:NSMakeSize(4, 8)]; [_textView setContinuousSpellCheckingEnabled:NO]; _textView.usesFontPanel = NO; _textView.editable = NO; if (@available(macOS 10.14, *)) { _textView.usesAdaptiveColorMappingForDarkAppearance = YES; } [_scrollView setHasVerticalScroller:YES]; [_scrollView setHasHorizontalScroller:NO]; if (_prefersMarkdown) { if (@available(macOS 12, *)) { dispatch_queue_attr_t queuePriority = dispatch_queue_attr_make_with_qos_class(DISPATCH_QUEUE_SERIAL, QOS_CLASS_USER_INITIATED, 0); dispatch_queue_t markdownDispatchQueue = dispatch_queue_create("org.sparkle-project.markdown-loader", queuePriority); dispatch_async(markdownDispatchQueue, ^{ NSError *loadMarkdownError = nil; NSAttributedString *originalMarkdownAttributedString = [[NSAttributedString alloc] initWithMarkdownString:contents options:nil baseURL:baseURL error:&loadMarkdownError]; if (originalMarkdownAttributedString == nil) { // Fallback to plain-text NSAttributedString *attributedString = [[NSAttributedString alloc] initWithString:contents attributes:@{ NSFontAttributeName : [NSFont systemFontOfSize:(CGFloat)self->_fontPointSize] }]; dispatch_async(dispatch_get_main_queue(), ^{ [self _loadAttributedString:attributedString completionHandler:completionHandler]; }); } else { NSAttributedString *formattedAttributedString = formatMarkdownAttributedString(originalMarkdownAttributedString, (CGFloat)self->_fontPointSize); dispatch_async(dispatch_get_main_queue(), ^{ [self _loadAttributedString:formattedAttributedString completionHandler:completionHandler]; }); } }); return; } else { SULog(SULogLevelDefault, @"Warning: falling back to plain text because markdown support requires macOS 12 or newer"); } } NSAttributedString *attributedString = [[NSAttributedString alloc] initWithString:contents attributes:@{ NSFontAttributeName : [NSFont systemFontOfSize:(CGFloat)_fontPointSize] }]; [self _loadAttributedString:attributedString completionHandler:completionHandler]; } - (void)loadString:(NSString *)contents baseURL:(NSURL * _Nullable)baseURL completionHandler:(void (^)(NSError * _Nullable))completionHandler { [self _loadString:contents baseURL:baseURL completionHandler:completionHandler]; } - (void)loadData:(NSData *)data MIMEType:(NSString *)MIMEType textEncodingName:(NSString *)textEncodingName baseURL:(NSURL *)baseURL completionHandler:(void (^)(NSError * _Nullable))completionHandler { CFStringEncoding cfEncoding = CFStringConvertIANACharSetNameToEncoding((CFStringRef)textEncodingName); NSStringEncoding encoding; if (cfEncoding != kCFStringEncodingInvalidId) { encoding = CFStringConvertEncodingToNSStringEncoding(cfEncoding); } else { encoding = NSUTF8StringEncoding; } NSString *contents = [[NSString alloc] initWithData:data encoding:encoding]; if (contents == nil) { completionHandler([NSError errorWithDomain:SUSparkleErrorDomain code:SUReleaseNotesError userInfo:@{NSLocalizedDescriptionKey: @"Failed to convert data contents to string"}]); return; } [self _loadString:contents baseURL:baseURL completionHandler:completionHandler]; } - (void)stopLoading { } - (void)setDrawsBackground:(BOOL)drawsBackground { } - (BOOL)textView:(NSTextView *)textView clickedOnLink:(id)link atIndex:(NSUInteger)charIndex { NSURL *linkURL; if ([(NSObject *)link isKindOfClass:[NSURL class]]) { linkURL = link; } else if ([(NSObject *)link isKindOfClass:[NSString class]]) { linkURL = [NSURL URLWithString:link]; } else { SULog(SULogLevelDefault, @"Blocked display of %@ link of unknown type", link); return YES; } BOOL isAboutBlankURL; if (!SUReleaseNotesIsSafeURL(linkURL, _customAllowedURLSchemes, &isAboutBlankURL)) { SULog(SULogLevelDefault, @"Blocked display of %@ URL which may be dangerous", linkURL.scheme); return YES; } return NO; } @end #endif ================================================ FILE: Sparkle/SUTouchBarButtonGroup.h ================================================ // // SUTouchBarButtonGroup.h // Sparkle // // Created by Yuxin Wang on 05/01/2017. // Copyright © 2017 Sparkle Project. All rights reserved. // #if SPARKLE_BUILD_UI_BITS || !BUILDING_SPARKLE #import <Cocoa/Cocoa.h> NS_ASSUME_NONNULL_BEGIN SPU_OBJC_DIRECT_MEMBERS @interface SUTouchBarButtonGroup : NSViewController @property (nonatomic, readonly, copy) NSArray<NSButton *> *buttons; - (instancetype)initByReferencingButtons:(NSArray<NSButton *> *)buttons; @end NS_ASSUME_NONNULL_END #endif ================================================ FILE: Sparkle/SUTouchBarButtonGroup.m ================================================ // // SUTouchBarButtonGroup.m // Sparkle // // Created by Yuxin Wang on 05/01/2017. // Copyright © 2017 Sparkle Project. All rights reserved. // #if SPARKLE_BUILD_UI_BITS || !BUILDING_SPARKLE #import "SUTouchBarButtonGroup.h" @implementation SUTouchBarButtonGroup @synthesize buttons = _buttons; - (instancetype)initByReferencingButtons:(NSArray<NSButton *> *)buttons { if (!(self = [super init])) return self; NSView *buttonGroup = [[NSView alloc] initWithFrame:NSZeroRect]; self.view = buttonGroup; NSMutableArray<NSLayoutConstraint *> *constraints = [NSMutableArray array]; NSMutableArray<NSButton *> *buttonCopies = [NSMutableArray arrayWithCapacity:buttons.count]; for (NSUInteger i = 0; i < buttons.count; i++) { NSButton *button = [buttons objectAtIndex:i]; NSButton *buttonCopy = [NSButton buttonWithTitle:button.title target:button.target action:button.action]; buttonCopy.tag = button.tag; buttonCopy.enabled = button.enabled; // Must be set explicitly, because NSWindow clears it // https://github.com/sparkle-project/Sparkle/pull/987#issuecomment-271539319 if (i == 0) { buttonCopy.keyEquivalent = @"\r"; } buttonCopy.translatesAutoresizingMaskIntoConstraints = NO; [buttonCopies addObject:buttonCopy]; [buttonGroup addSubview:buttonCopy]; // Custom layout is used for equal width buttons, to look more keyboard-like and mimic standard alerts // https://github.com/sparkle-project/Sparkle/pull/987#issuecomment-272324726 [constraints addObject:[NSLayoutConstraint constraintWithItem:buttonCopy attribute:NSLayoutAttributeTop relatedBy:NSLayoutRelationEqual toItem:buttonGroup attribute:NSLayoutAttributeTop multiplier:1.0 constant:0.0]]; [constraints addObject:[NSLayoutConstraint constraintWithItem:buttonCopy attribute:NSLayoutAttributeBottom relatedBy:NSLayoutRelationEqual toItem:buttonGroup attribute:NSLayoutAttributeBottom multiplier:1.0 constant:0.0]]; if (i == 0) { [constraints addObject:[NSLayoutConstraint constraintWithItem:buttonCopy attribute:NSLayoutAttributeTrailing relatedBy:NSLayoutRelationEqual toItem:buttonGroup attribute:NSLayoutAttributeTrailing multiplier:1.0 constant:0.0]]; } else { [constraints addObject:[NSLayoutConstraint constraintWithItem:buttonCopy attribute:NSLayoutAttributeTrailing relatedBy:NSLayoutRelationEqual toItem:[buttonCopies objectAtIndex:i - 1] attribute:NSLayoutAttributeLeading multiplier:1.0 constant:(i == 1) ? -8 : -32]]; [constraints addObject:[NSLayoutConstraint constraintWithItem:buttonCopy attribute:NSLayoutAttributeWidth relatedBy:NSLayoutRelationEqual toItem:[buttonCopies objectAtIndex:i - 1] attribute:NSLayoutAttributeWidth multiplier:1.0 constant:0.0]]; constraints.lastObject.priority = 250; } if (i == buttons.count - 1) { [constraints addObject:[NSLayoutConstraint constraintWithItem:buttonCopy attribute:NSLayoutAttributeLeading relatedBy:NSLayoutRelationEqual toItem:buttonGroup attribute:NSLayoutAttributeLeading multiplier:1.0 constant:0.0]]; } } [NSLayoutConstraint activateConstraints:constraints]; _buttons = buttonCopies; return self; } @end #endif ================================================ FILE: Sparkle/SUUpdateAlert.h ================================================ // // SUUpdateAlert.h // Sparkle // // Created by Andy Matuschak on 3/12/06. // Copyright 2006 Andy Matuschak. All rights reserved. // #if SPARKLE_BUILD_UI_BITS #ifndef SUUPDATEALERT_H #define SUUPDATEALERT_H #import <Cocoa/Cocoa.h> #import "SUVersionDisplayProtocol.h" #import "SPUUserUpdateState.h" @protocol SUUpdateAlertDelegate; @protocol SPUStandardUserDriverDelegate; @class SUAppcastItem, SPUDownloadData, SUHost, SPUUpdaterSettings; SPU_OBJC_DIRECT_MEMBERS @interface SUUpdateAlert : NSWindowController - (instancetype)initWithAppcastItem:(SUAppcastItem *)item state:(SPUUserUpdateState *)state host:(SUHost *)aHost versionDisplayer:(id<SUVersionDisplay>)versionDisplayer updaterSettings:(SPUUpdaterSettings *)updaterSettings delegate:(id<SPUStandardUserDriverDelegate>)delegate completionBlock:(void (^)(SPUUserUpdateChoice, NSRect, BOOL))completionBlock didBecomeKeyBlock:(void (^)(void))didBecomeKeyBlock; - (void)showUpdateReleaseNotesWithDownloadData:(SPUDownloadData *)downloadData; - (void)showReleaseNotesFailedToDownloadWithError:(NSError *)error; - (void)setInstallButtonFocus:(BOOL)focus; @end #endif #endif ================================================ FILE: Sparkle/SUUpdateAlert.m ================================================ // // SUUpdateAlert.m // Sparkle // // Created by Andy Matuschak on 3/12/06. // Copyright 2006 Andy Matuschak. All rights reserved. // // ----------------------------------------------------------------------------- // Headers: // ----------------------------------------------------------------------------- #if SPARKLE_BUILD_UI_BITS #import "SUUpdateAlert.h" #import "SUHost.h" #import "SUReleaseNotesView.h" #import "SUWKWebView.h" #import "SULegacyWebView.h" #import "SUTextViewReleaseNotesView.h" #import "SUConstants.h" #import "SULog.h" #import "SULocalizations.h" #import "SUAppcastItem.h" #import "SPUDownloadData.h" #import "SUApplicationInfo.h" #import "SPUUpdaterSettings.h" #import "SUTouchBarButtonGroup.h" #import "SPUXPCServiceInfo.h" #import "SPUUserUpdateState.h" static NSString *const SUUpdateAlertTouchBarIdentifier = @"" SPARKLE_BUNDLE_IDENTIFIER ".SUUpdateAlert"; static NSString *const SUAllowsAutomaticUpdatesKeyPath = @"allowsAutomaticUpdates"; static const CGFloat SUUpdateAlertGroupElementSpacing = 12.0; typedef NS_ENUM(NSInteger, SUReleaseNotesFormat) { SUReleaseNotesFormatHTML, SUReleaseNotesFormatPlainText, SUReleaseNotesFormatMarkdown }; @interface SUUpdateAlert () <NSTouchBarDelegate> @end @implementation SUUpdateAlert { SPUUpdaterSettings *_updaterSettings; SUAppcastItem *_updateItem; SUHost *_host; SPUUserUpdateState *_state; NSProgressIndicator *_releaseNotesSpinner; id<SUReleaseNotesView> _releaseNotesView; id<SUVersionDisplay> _versionDisplayer; __weak id<SPUStandardUserDriverDelegate> _delegate; IBOutlet NSStackView *_stackView; IBOutlet NSButton *_installButton; IBOutlet NSButton *_laterButton; IBOutlet NSButton *_skipButton; IBOutlet NSBox *_releaseNotesBoxView; IBOutlet NSView *_releaseNotesContentView; IBOutlet NSButton *_automaticallyInstallUpdatesButton; IBOutlet NSView *_titleView; void (^_didBecomeKeyBlock)(void); void(^_completionBlock)(SPUUserUpdateChoice, NSRect, BOOL); BOOL _windowLoadedAndShowsReleaseNotes; } - (instancetype)initWithAppcastItem:(SUAppcastItem *)item state:(SPUUserUpdateState *)state host:(SUHost *)aHost versionDisplayer:(id<SUVersionDisplay>)versionDisplayer updaterSettings:(SPUUpdaterSettings *)updaterSettings delegate:(id<SPUStandardUserDriverDelegate>)delegate completionBlock:(void (^)(SPUUserUpdateChoice, NSRect, BOOL))completionBlock didBecomeKeyBlock:(void (^)(void))didBecomeKeyBlock { self = [super initWithWindowNibName:@"SUUpdateAlert"]; if (self != nil) { _host = aHost; _updateItem = item; _versionDisplayer = versionDisplayer; _state = state; _delegate = delegate; _completionBlock = [completionBlock copy]; _didBecomeKeyBlock = [didBecomeKeyBlock copy]; _updaterSettings = updaterSettings; [self setShouldCascadeWindows:NO]; } else { assert(false); } return self; } - (void)dealloc { if (self.windowLoaded) { [_updaterSettings removeObserver:self forKeyPath:SUAllowsAutomaticUpdatesKeyPath]; } } - (NSString *)description { return [NSString stringWithFormat:@"%@ <%@>", [self class], _host.bundlePath]; } - (void)setInstallButtonFocus:(BOOL)focus { if (focus) { _installButton.keyEquivalent = @"\r"; } else { _installButton.keyEquivalent = @""; } } - (void)endWithSelection:(SPUUserUpdateChoice)choice SPU_OBJC_DIRECT { [_releaseNotesView stopLoading]; [_releaseNotesView.view removeFromSuperview]; // Otherwise it gets sent Esc presses (why?!) and gets very confused. NSWindow *window = self.window; BOOL wasKeyWindow = window.keyWindow; NSRect windowFrame = window.frame; [self close]; if (_completionBlock != nil) { _completionBlock(choice, windowFrame, wasKeyWindow); _completionBlock = nil; } } - (IBAction)installUpdate:(id)__unused sender { [self endWithSelection:SPUUserUpdateChoiceInstall]; } - (IBAction)openInfoURL:(id)__unused sender { NSURL *infoURL = _updateItem.infoURL; assert(infoURL); [[NSWorkspace sharedWorkspace] openURL:infoURL]; [self endWithSelection:SPUUserUpdateChoiceDismiss]; } - (IBAction)skipThisVersion:(id)__unused sender { [self endWithSelection:SPUUserUpdateChoiceSkip]; } - (IBAction)remindMeLater:(id)__unused sender { [self endWithSelection:SPUUserUpdateChoiceDismiss]; } - (void)displayReleaseNotesSpinner SPU_OBJC_DIRECT { // Stick a nice big spinner in the middle of the release notes view until the page is loaded. _releaseNotesSpinner = [[NSProgressIndicator alloc] init]; _releaseNotesSpinner.controlSize = NSControlSizeRegular; [_releaseNotesSpinner setStyle:NSProgressIndicatorStyleSpinning]; [_releaseNotesContentView addSubview:_releaseNotesSpinner]; _releaseNotesSpinner.translatesAutoresizingMaskIntoConstraints = NO; [NSLayoutConstraint activateConstraints:@[ [_releaseNotesSpinner.centerXAnchor constraintEqualToAnchor:_releaseNotesContentView.centerXAnchor], [_releaseNotesSpinner.centerYAnchor constraintEqualToAnchor:_releaseNotesContentView.centerYAnchor] ]]; _releaseNotesSpinner.displayedWhenStopped = NO; [_releaseNotesSpinner startAnimation:self]; // If there's no release notes URL, just stick the contents of the description into the release notes view // Otherwise we'll wait until the client wants us to show release notes if (_updateItem.releaseNotesURL == nil) { NSString *itemDescription = _updateItem.itemDescription; if (itemDescription != nil) { NSString *itemDescriptionFormat = _updateItem.itemDescriptionFormat; SUReleaseNotesFormat releaseNotesFormat; if ([itemDescriptionFormat isEqualToString:@"plain-text"]) { releaseNotesFormat = SUReleaseNotesFormatPlainText; } else if ([itemDescriptionFormat isEqualToString:@"markdown"]) { releaseNotesFormat = SUReleaseNotesFormatMarkdown; } else { releaseNotesFormat = SUReleaseNotesFormatHTML; } [self _createReleaseNotesViewPreferringFormat:releaseNotesFormat]; __weak __typeof__(self) weakSelf = self; [_releaseNotesView loadString:itemDescription baseURL:nil completionHandler:^(NSError * _Nullable error) { if (error != nil) { SULog(SULogLevelError, @"Failed to load HTML string from release notes view: %@", error); } [weakSelf stopReleaseNotesSpinner]; }]; } } } - (void)showUpdateReleaseNotesWithDownloadData:(SPUDownloadData *)downloadData { if (!_windowLoadedAndShowsReleaseNotes) { if (self.window == nil) { // Window was not properly loaded. // This can happen if the app moves and the update alert nib fails to load // This puts Sparkle in an unsupported state but we will try to avoid crashing SULog(SULogLevelError, @"Error: SUUpdateAlert window is nil and failed to load, which may mean the app was moved. Sparkle is running in an unsupported state."); } else if ([_host.bundle isEqual:NSBundle.mainBundle]) { SULog(SULogLevelError, @"Warning: '%@' is configured to not show release notes but release notes for version %@ were downloaded. Consider either removing release notes from your appcast or implementing -[SPUUpdaterDelegate updater:shouldDownloadReleaseNotesForUpdate:]", _host.name, _updateItem.displayVersionString); } return; } NSURL *releaseNotesURL = _updateItem.releaseNotesURL; NSURL *baseURL = releaseNotesURL.URLByDeletingLastPathComponent; // If a MIME type isn't provided, we will pick html as the default, as opposed to plain text. Questionable decision.. NSString *chosenMIMEType = (downloadData.MIMEType != nil) ? downloadData.MIMEType : @"text/html"; // We'll pick utf-8 as the default text encoding name if one isn't provided which I think is reasonable NSString *chosenTextEncodingName = (downloadData.textEncodingName != nil) ? downloadData.textEncodingName : @"utf-8"; // We don't support markdown but prepare for the future in case we support it one day NSString *pathExtension = releaseNotesURL.pathExtension; SUReleaseNotesFormat releaseNotesFormat; // Make sure we test for markdown first because text/plain may be used for MIME type if ([chosenMIMEType isEqualToString:@"text/markdown"] || [chosenMIMEType isEqualToString:@"text/x-markdown"] || [pathExtension caseInsensitiveCompare:@"md"] == NSOrderedSame || [pathExtension caseInsensitiveCompare:@"markdown"] == NSOrderedSame) { releaseNotesFormat = SUReleaseNotesFormatMarkdown; } else if ([chosenMIMEType isEqualToString:@"text/plain"] || [pathExtension caseInsensitiveCompare:@"txt"] == NSOrderedSame) { releaseNotesFormat = SUReleaseNotesFormatPlainText; } else { releaseNotesFormat = SUReleaseNotesFormatHTML; } [self _createReleaseNotesViewPreferringFormat:releaseNotesFormat]; __weak __typeof__(self) weakSelf = self; [_releaseNotesView loadData:downloadData.data MIMEType:chosenMIMEType textEncodingName:chosenTextEncodingName baseURL:baseURL completionHandler:^(NSError * _Nullable error) { if (error != nil) { SULog(SULogLevelError, @"Failed to load data from release notes view: %@", error); } [weakSelf stopReleaseNotesSpinner]; }]; } - (void)showReleaseNotesFailedToDownloadWithError:(NSError *)error { [self _createReleaseNotesViewPreferringFormat:SUReleaseNotesFormatPlainText]; __weak __typeof__(self) weakSelf = self; [_releaseNotesView loadString:error.localizedDescription baseURL:nil completionHandler:^(NSError * _Nullable loadCompletionError) { if (loadCompletionError != nil) { SULog(SULogLevelError, @"Failed to load HTML error string from release notes view: %@", loadCompletionError); } [weakSelf stopReleaseNotesSpinner]; }]; } - (void)stopReleaseNotesSpinner SPU_OBJC_DIRECT { [_releaseNotesSpinner stopAnimation:self]; } - (BOOL)showsReleaseNotes { NSNumber *shouldShowReleaseNotes = [_host boolNumberForInfoDictionaryKey:SUShowReleaseNotesKey]; if (shouldShowReleaseNotes == nil) { // Don't show release notes if RSS item contains no description and no release notes URL: return (([_updateItem itemDescription] != nil && [[[_updateItem itemDescription] stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]] length] > 0) || [_updateItem releaseNotesURL] != nil); } else return [shouldShowReleaseNotes boolValue]; } - (void)_createReleaseNotesViewPreferringFormat:(SUReleaseNotesFormat)preferredReleaseNotesFormat SPU_OBJC_DIRECT { // "-apple-system-font" is a reference to the system UI font. "-apple-system" is the new recommended token, but for backward compatibility we can't use it. NSString *defaultFontFamily = @"-apple-system-font"; int defaultFontSize = (int)[NSFont systemFontSize]; SUReleaseNotesFormat usedReleaseNotesFormat; switch (preferredReleaseNotesFormat) { case SUReleaseNotesFormatPlainText: case SUReleaseNotesFormatMarkdown: usedReleaseNotesFormat = preferredReleaseNotesFormat; break; case SUReleaseNotesFormatHTML: if (@available(macOS 10.15, *)) { if ([[NSProcessInfo processInfo] isMacCatalystApp]) { usedReleaseNotesFormat = SUReleaseNotesFormatPlainText; SULog(SULogLevelError, @"Error: Showing HTML release notes for Catalyst apps is not supported. The release notes will be interpreted as plain text. Please serve a plain-text (.txt) or markdown (.md) release notes file. If you are using a <description> element then please specify the %@=\"plain-text\" or %@=\"markdown\" attribute in that element.", SUAppcastAttributeFormat, SUAppcastAttributeFormat); } else { usedReleaseNotesFormat = preferredReleaseNotesFormat; } } else { usedReleaseNotesFormat = preferredReleaseNotesFormat; } break; } NSArray<NSString *> *customAllowedURLSchemes; { NSMutableArray<NSString *> *allowedSchemes = [NSMutableArray array]; NSArray *hostAllowedURLSchemes = [_host objectForInfoDictionaryKey:SUAllowedURLSchemesKey ofClass:NSArray.class]; if (hostAllowedURLSchemes != nil) { for (id urlScheme in hostAllowedURLSchemes) { if ([(NSObject *)urlScheme isKindOfClass:[NSString class]]) { NSString *allowedURLScheme = [(NSString *)urlScheme lowercaseString]; if (![allowedURLScheme isEqualToString:@"file"]) { [allowedSchemes addObject:allowedURLScheme]; } else { SULog(SULogLevelError, @"Error: Found 'file' scheme in %@. Ignoring because this scheme is unsafe.", SUAllowedURLSchemesKey); } } } } customAllowedURLSchemes = [allowedSchemes copy]; } id<SPUStandardUserDriverDelegate> delegate = _delegate; switch (usedReleaseNotesFormat) { case SUReleaseNotesFormatPlainText: _releaseNotesView = [[SUTextViewReleaseNotesView alloc] initWithFontPointSize:defaultFontSize appcastItem:_updateItem host:_host delegate:delegate prefersMarkdown:NO customAllowedURLSchemes:customAllowedURLSchemes]; break; case SUReleaseNotesFormatMarkdown: _releaseNotesView = [[SUTextViewReleaseNotesView alloc] initWithFontPointSize:defaultFontSize appcastItem:_updateItem host:_host delegate:delegate prefersMarkdown:YES customAllowedURLSchemes:customAllowedURLSchemes]; break; case SUReleaseNotesFormatHTML: { NSURL *colorStyleURL = [[NSBundle bundleForClass:[self class]] URLForResource:@"ReleaseNotesColorStyle" withExtension:@"css"]; BOOL javaScriptEnabled = [_host boolForInfoDictionaryKey:SUEnableJavaScriptKey]; #if DOWNLOADER_XPC_SERVICE_EMBEDDED // WKWebView has a bug where it won't work in loading local HTML content in sandboxed apps that do not have an outgoing network entitlement // FB6993802: https://twitter.com/sindresorhus/status/1160577243929878528 | https://github.com/feedback-assistant/reports/issues/1 // If the developer is using the downloader XPC service, they are very most likely are a) sandboxed b) do not use outgoing network entitlement. // In this case, fall back to legacy WebKit view. // (In theory it is possible for a non-sandboxed app or sandboxed app with outgoing network entitlement to use the XPC service, it's just unlikely and unsupported). // Note: because legacy web view is only supported with using downloader XPC Service, and the app // should not have an outgoing network client, there's no need to be concerned about the // appcast signing validation status for loading external resources, which shouldn't be possible. BOOL useWKWebView = !SPUXPCServiceIsEnabled(SUEnableDownloaderServiceKey); if (!useWKWebView) { _releaseNotesView = [[SULegacyWebView alloc] initWithColorStyleSheetLocation:colorStyleURL fontFamily:defaultFontFamily fontPointSize:defaultFontSize javaScriptEnabled:javaScriptEnabled customAllowedURLSchemes:customAllowedURLSchemes]; } else #endif { BOOL allowsLoadingExternalReferences = (_updateItem.signingValidationStatus == SPUAppcastSigningValidationStatusSkipped); _releaseNotesView = [[SUWKWebView alloc] initWithColorStyleSheetLocation:colorStyleURL fontFamily:defaultFontFamily fontPointSize:defaultFontSize javaScriptEnabled:javaScriptEnabled customAllowedURLSchemes:customAllowedURLSchemes allowsLoadingExternalReferences:allowsLoadingExternalReferences installedVersion:_host.version]; } break; } } assert(_releaseNotesSpinner != nil); [_releaseNotesContentView addSubview:_releaseNotesView.view positioned:NSWindowBelow relativeTo:_releaseNotesSpinner]; _releaseNotesView.view.frame = _releaseNotesContentView.bounds; _releaseNotesView.view.autoresizingMask = (NSAutoresizingMaskOptions)(NSViewWidthSizable | NSViewHeightSizable); if (@available(macOS 10.14, *)) { // We need a transparent background // This avoids a "white flash" that may be present when the webview initially loads in dark mode // This also is necessary for macOS 10.14, otherwise the background may stay white on 10.14 (but not in later OS's) [_releaseNotesView setDrawsBackground:NO]; } } - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSString *,id> *)change context:(void *)context { if ([keyPath isEqualToString:SUAllowsAutomaticUpdatesKeyPath]) { _automaticallyInstallUpdatesButton.superview.hidden = !_updaterSettings.allowsAutomaticUpdates; } else { [super observeValueForKeyPath:keyPath ofObject:object change:change context:context]; } } - (void)windowDidLoad { NSWindow *window = self.window; window.movableByWindowBackground = YES; #if SPARKLE_COPY_LOCALIZATIONS NSBundle *sparkleBundle = SUSparkleBundle(); #endif [_stackView setCustomSpacing:SUUpdateAlertGroupElementSpacing afterView:_titleView]; // Customize custom NSBox { CGFloat boxCornerRadius = 6.0; CGFloat boxBorderWidth = 1.0; _releaseNotesBoxView.boxType = NSBoxCustom; _releaseNotesBoxView.cornerRadius = boxCornerRadius; if (@available(macOS 10.14, *)) { _releaseNotesBoxView.borderColor = NSColor.separatorColor; } else { _releaseNotesBoxView.borderColor = [NSColor colorWithCalibratedWhite:0.84 alpha:1.0]; } _releaseNotesBoxView.borderWidth = boxBorderWidth; _releaseNotesBoxView.fillColor = NSColor.textBackgroundColor; // Needed so we don't clip the corners if the CSS uses a custom background _releaseNotesBoxView.contentView.wantsLayer = YES; _releaseNotesBoxView.contentView.layer.masksToBounds = YES; _releaseNotesBoxView.contentView.layer.cornerRadius = boxCornerRadius - boxBorderWidth; } _laterButton.title = SULocalizedStringFromTableInBundle(@"Remind Me Later", SPARKLE_TABLE, sparkleBundle, @""); _skipButton.title = SULocalizedStringFromTableInBundle(@"Skip This Version", SPARKLE_TABLE, sparkleBundle, @""); _installButton.title = SULocalizedStringFromTableInBundle(@"Install Update", SPARKLE_TABLE, sparkleBundle, @""); _automaticallyInstallUpdatesButton.title = SULocalizedStringFromTableInBundle(@"Automatically download and install updates in the future", SPARKLE_TABLE, sparkleBundle, @""); if (@available(macOS 16, *)) { _skipButton.controlSize = NSControlSizeLarge; _laterButton.controlSize = NSControlSizeLarge; _installButton.controlSize = NSControlSizeLarge; } BOOL showReleaseNotes = [self showsReleaseNotes]; if (showReleaseNotes) { window.frameAutosaveName = @"SUUpdateAlert2"; } else { // Update alert should not be resizable when no release notes are available window.styleMask = (NSWindowStyleMask)(window.styleMask & ~NSWindowStyleMaskResizable); } _windowLoadedAndShowsReleaseNotes = showReleaseNotes; if (_updateItem.informationOnlyUpdate) { [_installButton setTitle:SULocalizedStringFromTableInBundle(@"Learn More…", SPARKLE_TABLE, sparkleBundle, @"Alternate title for 'Install Update' button when there's no download in RSS feed.")]; [_installButton setAction:@selector(openInfoURL:)]; } if (showReleaseNotes) { [self displayReleaseNotesSpinner]; // Add more spacing to give choices and automatic installs checkbox better grouping [_stackView setCustomSpacing:SUUpdateAlertGroupElementSpacing afterView:_releaseNotesBoxView]; } else { _releaseNotesBoxView.hidden = YES; } // NOTE: The code below for deciding what buttons to hide is complex! Due to array of feature configurations :) [_updaterSettings addObserver:self forKeyPath:SUAllowsAutomaticUpdatesKeyPath options:(NSKeyValueObservingOptions)(NSKeyValueObservingOptionInitial | NSKeyValueObservingOptionNew) context:NULL]; if (_state.stage == SPUUserUpdateStageInstalling) { // We're going to be relaunching pretty instantaneously _installButton.title = SULocalizedStringFromTableInBundle(@"Install and Relaunch", SPARKLE_TABLE, sparkleBundle, nil); // We should be explicit that the update will be installed on quit _laterButton.title = SULocalizedStringFromTableInBundle(@"Install on Quit", SPARKLE_TABLE, sparkleBundle, @"Alternate title for 'Remind Me Later' button when downloaded updates can be resumed"); } if (_updateItem.criticalUpdate && !_updateItem.majorUpgrade) { _skipButton.hidden = YES; _laterButton.hidden = YES; } // Reminding user later doesn't make sense when automatic update checks are off if (![_host boolForKey:SUEnableAutomaticChecksKey]) { _laterButton.hidden = YES; } [window center]; } - (void)windowDidBecomeKey:(NSNotification *)__unused note { if (_didBecomeKeyBlock != NULL) { _didBecomeKeyBlock(); } } - (BOOL)windowShouldClose:(NSNotification *) __unused note { [self endWithSelection:SPUUserUpdateChoiceDismiss]; return YES; } - (NSImage *)applicationIcon { return [SUApplicationInfo bestIconForHost:_host]; } - (NSString *)titleText { #if SPARKLE_COPY_LOCALIZATIONS NSBundle *sparkleBundle = SUSparkleBundle(); #endif if (_updateItem.criticalUpdate) { return [NSString stringWithFormat:SULocalizedStringFromTableInBundle(@"An important update to %@ is ready to install", SPARKLE_TABLE, sparkleBundle, nil), _host.name]; } else if (_state.stage == SPUUserUpdateStageDownloaded || _state.stage == SPUUserUpdateStageInstalling) { return [NSString stringWithFormat:SULocalizedStringFromTableInBundle(@"A new version of %@ is ready to install!", SPARKLE_TABLE, sparkleBundle, nil), _host.name]; } else { return [NSString stringWithFormat:SULocalizedStringFromTableInBundle(@"A new version of %@ is available!", SPARKLE_TABLE, sparkleBundle, nil), _host.name]; } } - (NSString *)descriptionText { NSString *updateItemDisplayVersion = [_updateItem displayVersionString]; NSString *hostDisplayVersion = [_host displayVersion]; if ([_versionDisplayer respondsToSelector:@selector(formatUpdateDisplayVersionFromUpdate:andBundleDisplayVersion:withBundleVersion:)]) { updateItemDisplayVersion = [_versionDisplayer formatUpdateDisplayVersionFromUpdate:_updateItem andBundleDisplayVersion:&hostDisplayVersion withBundleVersion:_host.version]; } else { #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdeprecated-declarations" [_versionDisplayer formatVersion:&updateItemDisplayVersion andVersion:&hostDisplayVersion]; #pragma clang diagnostic pop } // We display a different summary depending on if it's an "info-only" item, or a "critical update" item, or if we've already downloaded the update and just need to relaunch NSString *finalString = nil; #if SPARKLE_COPY_LOCALIZATIONS NSBundle *sparkleBundle = SUSparkleBundle(); #endif if (_updateItem.informationOnlyUpdate) { finalString = [NSString stringWithFormat:SULocalizedStringFromTableInBundle(@"%@ %@ is now available—you have %@. Would you like to learn more about this update on the web?", SPARKLE_TABLE, sparkleBundle, @"Description text for SUUpdateAlert when the update informational with no download."), _host.name, updateItemDisplayVersion, hostDisplayVersion]; } else if (_updateItem.criticalUpdate) { if (_state.stage == SPUUserUpdateStageNotDownloaded) { finalString = [NSString stringWithFormat:SULocalizedStringFromTableInBundle(@"%@ %@ is now available—you have %@. This is an important update; would you like to download it now?", SPARKLE_TABLE, sparkleBundle, @"Description text for SUUpdateAlert when the critical update is downloadable."), _host.name, updateItemDisplayVersion, hostDisplayVersion]; } else { finalString = [NSString stringWithFormat:SULocalizedStringFromTableInBundle(@"%1$@ %2$@ has been downloaded and is ready to use! This is an important update; would you like to install it and relaunch %1$@ now?", SPARKLE_TABLE, sparkleBundle, @"Description text for SUUpdateAlert when the critical update has already been downloaded and ready to install."), _host.name, updateItemDisplayVersion]; } } else { if (_state.stage == SPUUserUpdateStageNotDownloaded) { finalString = [NSString stringWithFormat:SULocalizedStringFromTableInBundle(@"%@ %@ is now available—you have %@. Would you like to download it now?", SPARKLE_TABLE, sparkleBundle, @"Description text for SUUpdateAlert when the update is downloadable."), _host.name, updateItemDisplayVersion, hostDisplayVersion]; } else { finalString = [NSString stringWithFormat:SULocalizedStringFromTableInBundle(@"%1$@ %2$@ has been downloaded and is ready to use! Would you like to install it and relaunch %1$@ now?", SPARKLE_TABLE, sparkleBundle, @"Description text for SUUpdateAlert when the update has already been downloaded and ready to install."), _host.name, updateItemDisplayVersion]; } } return finalString; } - (NSTouchBar *)makeTouchBar { NSTouchBar *touchBar = [[NSTouchBar alloc] init]; touchBar.defaultItemIdentifiers = @[SUUpdateAlertTouchBarIdentifier,]; touchBar.principalItemIdentifier = SUUpdateAlertTouchBarIdentifier; touchBar.delegate = self; return touchBar; } - (NSTouchBarItem *)touchBar:(NSTouchBar * __unused)touchBar makeItemForIdentifier:(NSTouchBarItemIdentifier)identifier { if ([identifier isEqualToString:SUUpdateAlertTouchBarIdentifier]) { NSCustomTouchBarItem* item = [[NSCustomTouchBarItem alloc] initWithIdentifier:identifier]; item.viewController = [[SUTouchBarButtonGroup alloc] initByReferencingButtons:@[_installButton, _laterButton, _skipButton]]; return item; } return nil; } @end #endif ================================================ FILE: Sparkle/SUUpdateAlert.xib ================================================ <?xml version="1.0" encoding="UTF-8"?> <document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="24412" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES"> <dependencies> <deployment identifier="macosx"/> <plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="24412"/> <capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/> </dependencies> <objects> <customObject id="-2" userLabel="File's Owner" customClass="SUUpdateAlert"> <connections> <outlet property="_automaticallyInstallUpdatesButton" destination="xYY-q8-ZyN" id="yjl-z6-hNj"/> <outlet property="_installButton" destination="sUq-y0-gA3" id="hQ1-WX-CFo"/> <outlet property="_laterButton" destination="uGk-Ks-HQR" id="QLX-rC-OVd"/> <outlet property="_releaseNotesBoxView" destination="ikY-gb-bgB" id="8ep-uz-XVh"/> <outlet property="_releaseNotesContentView" destination="YbC-Ky-Aam" id="nng-CZ-Hwe"/> <outlet property="_skipButton" destination="nRH-TL-zdL" id="1wh-qo-iMh"/> <outlet property="_stackView" destination="61v-5k-OXp" id="1rj-X0-yNf"/> <outlet property="_titleView" destination="cGD-9V-Gym" id="4EE-yh-bV0"/> <outlet property="window" destination="5" id="69"/> </connections> </customObject> <customObject id="-1" userLabel="First Responder" customClass="FirstResponder"/> <customObject id="-3" userLabel="Application" customClass="NSObject"/> <window identifier="SUUpdateAlert" allowsToolTipsWhenApplicationIsInactive="NO" autorecalculatesKeyViewLoop="NO" releasedWhenClosed="NO" visibleAtLaunch="NO" frameAutosaveName="" animationBehavior="default" titlebarAppearsTransparent="YES" id="5" userLabel="Update Alert (release notes)"> <windowStyleMask key="styleMask" titled="YES" closable="YES" miniaturizable="YES" resizable="YES"/> <windowCollectionBehavior key="collectionBehavior" fullScreenAuxiliary="YES"/> <windowPositionMask key="initialPositionMask" topStrut="YES" bottomStrut="YES"/> <rect key="contentRect" x="746" y="229" width="532" height="370"/> <rect key="screenRect" x="0.0" y="0.0" width="1470" height="922"/> <value key="minSize" type="size" width="462" height="150"/> <view key="contentView" misplaced="YES" id="6"> <rect key="frame" x="0.0" y="0.0" width="532" height="370"/> <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/> <subviews> <stackView distribution="fill" orientation="vertical" alignment="leading" horizontalStackHuggingPriority="249.99998474121094" verticalStackHuggingPriority="249.99998474121094" detachesHiddenViews="YES" translatesAutoresizingMaskIntoConstraints="NO" id="61v-5k-OXp"> <rect key="frame" x="0.0" y="0.0" width="680" height="381"/> <subviews> <customView translatesAutoresizingMaskIntoConstraints="NO" id="cGD-9V-Gym" userLabel="Title View"> <rect key="frame" x="0.0" y="317" width="680" height="64"/> <subviews> <imageView translatesAutoresizingMaskIntoConstraints="NO" id="F1L-zh-U8Z" userLabel="Program icon"> <rect key="frame" x="20" y="0.0" width="64" height="64"/> <constraints> <constraint firstAttribute="width" constant="64" id="66j-dF-FGm"/> <constraint firstAttribute="width" secondItem="F1L-zh-U8Z" secondAttribute="height" multiplier="1:1" id="OlN-VE-Lim"/> </constraints> <imageCell key="cell" refusesFirstResponder="YES" alignment="left" animates="YES" imageScaling="proportionallyUpOrDown" image="NSApplicationIcon" id="k4f-D1-7oF"/> <connections> <binding destination="-2" name="value" keyPath="applicationIcon" id="DAK-U8-9cN"/> </connections> </imageView> <customView translatesAutoresizingMaskIntoConstraints="NO" id="UU2-7C-LCJ" userLabel="Version & Question view"> <rect key="frame" x="92" y="13" width="568" height="38"/> <subviews> <textField focusRingType="none" verticalHuggingPriority="750" horizontalCompressionResistancePriority="1000" translatesAutoresizingMaskIntoConstraints="NO" id="Nvn-Dp-hAH" userLabel="Version text field"> <rect key="frame" x="-2" y="22" width="572" height="16"/> <textFieldCell key="cell" lineBreakMode="truncatingTail" allowsUndo="NO" sendsActionOnEndEditing="YES" title="Version title" usesSingleLineMode="YES" id="f6r-wP-EYG"> <font key="font" metaFont="systemBold"/> <color key="textColor" name="controlTextColor" catalog="System" colorSpace="catalog"/> <color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/> </textFieldCell> <connections> <binding destination="-2" name="value" keyPath="titleText" id="j4H-SG-IuG"/> </connections> </textField> <textField focusRingType="none" verticalHuggingPriority="999" horizontalCompressionResistancePriority="1000" verticalCompressionResistancePriority="1000" preferredMaxLayoutWidth="490" translatesAutoresizingMaskIntoConstraints="NO" id="WdU-eY-Ta9" userLabel="Question text field"> <rect key="frame" x="-2" y="0.0" width="572" height="14"/> <textFieldCell key="cell" allowsUndo="NO" sendsActionOnEndEditing="YES" title="Question" id="Jsa-P1-mWz"> <font key="font" metaFont="smallSystem"/> <color key="textColor" name="controlTextColor" catalog="System" colorSpace="catalog"/> <color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/> </textFieldCell> <connections> <binding destination="-2" name="value" keyPath="descriptionText" id="83R-0d-Obq"/> </connections> </textField> </subviews> <constraints> <constraint firstItem="Nvn-Dp-hAH" firstAttribute="leading" secondItem="UU2-7C-LCJ" secondAttribute="leading" id="25L-Ag-aFU"/> <constraint firstItem="WdU-eY-Ta9" firstAttribute="top" secondItem="Nvn-Dp-hAH" secondAttribute="bottom" constant="8" id="9M6-6n-Bro"/> <constraint firstAttribute="bottom" secondItem="WdU-eY-Ta9" secondAttribute="bottom" id="CZX-9a-gJl"/> <constraint firstAttribute="trailing" secondItem="WdU-eY-Ta9" secondAttribute="trailing" id="WZm-Si-7IJ"/> <constraint firstAttribute="trailing" secondItem="Nvn-Dp-hAH" secondAttribute="trailing" id="aGK-sK-0uf"/> <constraint firstItem="Nvn-Dp-hAH" firstAttribute="top" secondItem="UU2-7C-LCJ" secondAttribute="top" id="kgy-Vi-Ea3"/> <constraint firstItem="WdU-eY-Ta9" firstAttribute="leading" secondItem="Nvn-Dp-hAH" secondAttribute="leading" id="vtl-AO-H0b"/> </constraints> </customView> </subviews> <constraints> <constraint firstItem="F1L-zh-U8Z" firstAttribute="top" secondItem="cGD-9V-Gym" secondAttribute="top" id="29T-yc-60G"/> <constraint firstAttribute="bottom" secondItem="F1L-zh-U8Z" secondAttribute="bottom" id="72d-EQ-SSU"/> <constraint firstItem="F1L-zh-U8Z" firstAttribute="leading" secondItem="cGD-9V-Gym" secondAttribute="leading" constant="20" symbolic="YES" id="Mg4-Zl-nWT"/> <constraint firstItem="UU2-7C-LCJ" firstAttribute="centerY" secondItem="F1L-zh-U8Z" secondAttribute="centerY" id="WwT-ww-Twu"/> <constraint firstAttribute="trailing" secondItem="UU2-7C-LCJ" secondAttribute="trailing" constant="20" symbolic="YES" id="eyz-xg-wVZ"/> <constraint firstItem="UU2-7C-LCJ" firstAttribute="leading" secondItem="F1L-zh-U8Z" secondAttribute="trailing" constant="8" symbolic="YES" id="pUk-FW-06K"/> </constraints> </customView> <box boxType="custom" cornerRadius="4" title="Box" translatesAutoresizingMaskIntoConstraints="NO" id="ikY-gb-bgB" userLabel="Release Notes Box"> <rect key="frame" x="20" y="74" width="640" height="235"/> <view key="contentView" id="YbC-Ky-Aam" userLabel="Release Notes Content View"> <rect key="frame" x="1" y="1" width="638" height="233"/> <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/> </view> <constraints> <constraint firstAttribute="height" constant="235" placeholder="YES" id="MvR-0E-bDS"/> <constraint firstAttribute="height" relation="greaterThanOrEqual" priority="999" constant="200" id="g5s-9E-8SF"/> </constraints> </box> <customView translatesAutoresizingMaskIntoConstraints="NO" id="Dg5-0U-d14" userLabel="Options view"> <rect key="frame" x="0.0" y="52" width="680" height="14"/> <subviews> <button verticalHuggingPriority="1000" verticalCompressionResistancePriority="1000" translatesAutoresizingMaskIntoConstraints="NO" id="xYY-q8-ZyN"> <rect key="frame" x="20" y="0.0" width="313" height="14"/> <buttonCell key="cell" type="check" title="Automatically download and install updates in the future" bezelStyle="regularSquare" imagePosition="left" alignment="left" controlSize="small" inset="2" id="fPh-Q9-vLr"> <behavior key="behavior" changeContents="YES" doesNotDimImage="YES" lightByContents="YES"/> <font key="font" metaFont="smallSystem"/> </buttonCell> <connections> <binding destination="-2" name="value" keyPath="self.updaterSettings.automaticallyDownloadsUpdates" id="ckv-Jc-M6i"/> </connections> </button> </subviews> <constraints> <constraint firstItem="xYY-q8-ZyN" firstAttribute="top" secondItem="Dg5-0U-d14" secondAttribute="top" id="9SN-p3-BTb"/> <constraint firstAttribute="trailing" relation="greaterThanOrEqual" secondItem="xYY-q8-ZyN" secondAttribute="trailing" constant="20" symbolic="YES" id="CQ6-qx-HSc"/> <constraint firstAttribute="bottom" secondItem="xYY-q8-ZyN" secondAttribute="bottom" id="PoC-Dx-8Jc"/> <constraint firstItem="xYY-q8-ZyN" firstAttribute="leading" secondItem="Dg5-0U-d14" secondAttribute="leading" constant="20" symbolic="YES" id="wWK-qz-vla"/> </constraints> </customView> <customView translatesAutoresizingMaskIntoConstraints="NO" id="iUh-Md-dRX" userLabel="Choices view"> <rect key="frame" x="0.0" y="0.0" width="680" height="44"/> <subviews> <button horizontalHuggingPriority="150" verticalHuggingPriority="750" horizontalCompressionResistancePriority="997" translatesAutoresizingMaskIntoConstraints="NO" id="uGk-Ks-HQR"> <rect key="frame" x="395" y="20" width="127" height="24"/> <buttonCell key="cell" type="push" title="Remind Me Later" bezelStyle="rounded" alignment="center" borderStyle="border" inset="2" id="G8s-oM-9gf"> <behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/> <font key="font" metaFont="system"/> <string key="keyEquivalent" base64-UTF8="YES"> Gw </string> </buttonCell> <constraints> <constraint firstAttribute="width" relation="greaterThanOrEqual" priority="222" constant="90" id="8FZ-lB-M5G"/> </constraints> <accessibility identifier="SPUUserUpdateChoiceDismiss"/> <connections> <action selector="remindMeLater:" target="-2" id="108-L6-c2e"/> </connections> </button> <button horizontalHuggingPriority="20" verticalHuggingPriority="750" horizontalCompressionResistancePriority="1000" translatesAutoresizingMaskIntoConstraints="NO" id="sUq-y0-gA3"> <rect key="frame" x="534" y="20" width="126" height="24"/> <buttonCell key="cell" type="push" title="Install Update" bezelStyle="rounded" alignment="center" state="on" borderStyle="border" inset="2" id="IIY-s3-hkz"> <behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/> <font key="font" metaFont="system"/> <string key="keyEquivalent" base64-UTF8="YES"> DQ </string> </buttonCell> <constraints> <constraint firstAttribute="width" relation="greaterThanOrEqual" priority="222" constant="120" id="Gsu-Ko-2TJ"/> </constraints> <accessibility identifier="SPUUserUpdateChoiceInstall"/> <connections> <action selector="installUpdate:" target="-2" id="8TR-Ao-zjt"/> </connections> </button> <button verticalHuggingPriority="750" horizontalCompressionResistancePriority="998" translatesAutoresizingMaskIntoConstraints="NO" id="nRH-TL-zdL"> <rect key="frame" x="20" y="20" width="129" height="24"/> <buttonCell key="cell" type="push" title="Skip This Version" bezelStyle="rounded" alignment="center" borderStyle="border" inset="2" id="kVE-pO-gl0"> <behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/> <font key="font" metaFont="system"/> </buttonCell> <accessibility identifier="SPUUserUpdateChoiceSkip"/> <connections> <action selector="skipThisVersion:" target="-2" id="cXW-0s-yIT"/> </connections> </button> </subviews> <constraints> <constraint firstItem="uGk-Ks-HQR" firstAttribute="baseline" secondItem="sUq-y0-gA3" secondAttribute="baseline" id="7hK-NU-1pL"/> <constraint firstItem="nRH-TL-zdL" firstAttribute="top" secondItem="iUh-Md-dRX" secondAttribute="top" id="Cwv-ha-SRC"/> <constraint firstItem="uGk-Ks-HQR" firstAttribute="width" secondItem="sUq-y0-gA3" secondAttribute="width" id="EWD-pD-s8E"/> <constraint firstItem="sUq-y0-gA3" firstAttribute="leading" secondItem="uGk-Ks-HQR" secondAttribute="trailing" constant="12" symbolic="YES" id="GNV-C1-iwM"/> <constraint firstItem="uGk-Ks-HQR" firstAttribute="leading" relation="greaterThanOrEqual" secondItem="nRH-TL-zdL" secondAttribute="trailing" constant="12" symbolic="YES" id="JIK-Ix-vKz"/> <constraint firstItem="nRH-TL-zdL" firstAttribute="width" relation="greaterThanOrEqual" secondItem="sUq-y0-gA3" secondAttribute="width" id="ZG6-hO-ueO"/> <constraint firstAttribute="bottom" secondItem="nRH-TL-zdL" secondAttribute="bottom" constant="20" symbolic="YES" id="hi9-TB-2I7"/> <constraint firstAttribute="trailing" secondItem="sUq-y0-gA3" secondAttribute="trailing" constant="20" symbolic="YES" id="qFp-Lz-Zct"/> <constraint firstItem="nRH-TL-zdL" firstAttribute="baseline" secondItem="uGk-Ks-HQR" secondAttribute="baseline" id="u6y-Qc-AKF"/> <constraint firstItem="nRH-TL-zdL" firstAttribute="leading" secondItem="iUh-Md-dRX" secondAttribute="leading" constant="20" symbolic="YES" id="z77-Ds-Apc"/> </constraints> </customView> </subviews> <constraints> <constraint firstItem="iUh-Md-dRX" firstAttribute="leading" secondItem="61v-5k-OXp" secondAttribute="leading" id="9Qm-jR-l2n"/> <constraint firstAttribute="trailing" secondItem="cGD-9V-Gym" secondAttribute="trailing" id="Hzm-0Q-nAs"/> <constraint firstAttribute="trailing" secondItem="Dg5-0U-d14" secondAttribute="trailing" id="QtQ-Z7-Sy3"/> <constraint firstItem="cGD-9V-Gym" firstAttribute="leading" secondItem="61v-5k-OXp" secondAttribute="leading" id="fKq-7a-nM1"/> <constraint firstAttribute="trailing" secondItem="iUh-Md-dRX" secondAttribute="trailing" id="fly-DF-xg8"/> <constraint firstAttribute="trailing" secondItem="ikY-gb-bgB" secondAttribute="trailing" constant="20" symbolic="YES" id="gyA-Og-04f"/> <constraint firstItem="ikY-gb-bgB" firstAttribute="leading" secondItem="61v-5k-OXp" secondAttribute="leading" constant="20" symbolic="YES" id="zdI-MC-GvM"/> <constraint firstItem="Dg5-0U-d14" firstAttribute="leading" secondItem="61v-5k-OXp" secondAttribute="leading" id="zjt-Mp-9hy"/> </constraints> <visibilityPriorities> <integer value="1000"/> <integer value="1000"/> <integer value="1000"/> <integer value="1000"/> </visibilityPriorities> <customSpacing> <real value="3.4028234663852886e+38"/> <real value="3.4028234663852886e+38"/> <real value="3.4028234663852886e+38"/> <real value="3.4028234663852886e+38"/> </customSpacing> </stackView> </subviews> <constraints> <constraint firstAttribute="bottom" secondItem="61v-5k-OXp" secondAttribute="bottom" id="5XV-te-jdS"/> <constraint firstItem="61v-5k-OXp" firstAttribute="leading" secondItem="6" secondAttribute="leading" id="ISL-gI-Nqo"/> <constraint firstItem="61v-5k-OXp" firstAttribute="top" secondItem="6" secondAttribute="top" id="Xwp-R6-5u2"/> <constraint firstAttribute="trailing" secondItem="61v-5k-OXp" secondAttribute="trailing" id="pAA-qg-9qi"/> </constraints> </view> <connections> <outlet property="delegate" destination="-2" id="50"/> </connections> <point key="canvasLocation" x="602" y="352"/> </window> <userDefaultsController representsSharedInstance="YES" id="93" userLabel="Shared Defaults"/> </objects> <resources> <image name="NSApplicationIcon" width="32" height="32"/> </resources> </document> ================================================ FILE: Sparkle/SUUpdatePermissionPrompt.h ================================================ // // SUUpdatePermissionPrompt.h // Sparkle // // Created by Andy Matuschak on 1/24/08. // Copyright 2008 Andy Matuschak. All rights reserved. // #if SPARKLE_BUILD_UI_BITS #ifndef SUUPDATEPERMISSIONPROMPT_H #define SUUPDATEPERMISSIONPROMPT_H #import <Cocoa/Cocoa.h> @class SUHost, SPUUpdatePermissionRequest, SUUpdatePermissionResponse; SPU_OBJC_DIRECT_MEMBERS @interface SUUpdatePermissionPrompt : NSWindowController - (instancetype)initPromptWithHost:(SUHost *)theHost request:(SPUUpdatePermissionRequest *)request reply:(void (^)(SUUpdatePermissionResponse *))reply; @end #endif #endif ================================================ FILE: Sparkle/SUUpdatePermissionPrompt.m ================================================ // // SUUpdatePermissionPrompt.m // Sparkle // // Created by Andy Matuschak on 1/24/08. // Copyright 2008 Andy Matuschak. All rights reserved. // #if SPARKLE_BUILD_UI_BITS #import "SUUpdatePermissionPrompt.h" #import "SPUUpdatePermissionRequest.h" #import "SUUpdatePermissionResponse.h" #import "SULocalizations.h" #import "SUHost.h" #import "SUConstants.h" #import "SUApplicationInfo.h" #import "SUTouchBarButtonGroup.h" static NSString *const SUUpdatePermissionPromptTouchBarIdentifier = @"" SPARKLE_BUNDLE_IDENTIFIER ".SUUpdatePermissionPrompt"; @interface SUUpdatePermissionPrompt () <NSTouchBarDelegate> // These properties are used for bindings @property (nonatomic, readonly) NSArray *systemProfileInformationArray; @property (nonatomic) BOOL shouldSendProfile; @property (nonatomic) BOOL automaticallyDownloadUpdates; @end @implementation SUUpdatePermissionPrompt { SUHost *_host; IBOutlet NSStackView *_stackView; IBOutlet NSView *_promptView; IBOutlet NSView *_moreInfoView; IBOutlet NSView *_placeholderView; IBOutlet NSView *_responseView; IBOutlet NSView *_infoChoiceView; IBOutlet NSView *_automaticallyDownloadUpdatesView; IBOutlet NSButton *_cancelButton; IBOutlet NSButton *_checkButton; IBOutlet NSTextField *_checkForUpdatesAutomaticallyTextField; IBOutlet NSButton *_includeAnonymousSystemProfileButton; IBOutlet NSButton *_anonymousInfoDisclosureButton; IBOutlet NSButton *_automaticallyDownloadAndInstallUpdatesButton; IBOutlet NSTextField *_anonymousSystemProfileDisclosureInformation; IBOutlet NSLayoutConstraint *_placeholderHeightLayoutConstraint; void (^_reply)(SUUpdatePermissionResponse *); } @synthesize shouldSendProfile = _shouldSendProfile; @synthesize automaticallyDownloadUpdates = _automaticallyDownloadUpdates; @synthesize systemProfileInformationArray = _systemProfileInformationArray; - (instancetype)initPromptWithHost:(SUHost *)theHost request:(SPUUpdatePermissionRequest *)request reply:(void (^)(SUUpdatePermissionResponse *))reply { self = [super initWithWindowNibName:@"SUUpdatePermissionPrompt"]; if (self) { _reply = [reply copy]; _host = theHost; _shouldSendProfile = [self shouldAskAboutProfile]; _systemProfileInformationArray = request.systemProfile; _automaticallyDownloadUpdates = [theHost boolForKey:SUAutomaticallyUpdateKey]; [self setShouldCascadeWindows:NO]; } else { assert(false); } return self; } - (BOOL)shouldAskAboutProfile { return [_host boolForInfoDictionaryKey:SUEnableSystemProfilingKey]; } - (BOOL)allowsAutomaticUpdates { NSNumber *allowsAutomaticUpdates = [_host boolNumberForInfoDictionaryKey:SUAllowsAutomaticUpdatesKey]; return (allowsAutomaticUpdates == nil || allowsAutomaticUpdates.boolValue); } - (NSString *)description { return [NSString stringWithFormat:@"%@ <%@>", [self class], _host.bundlePath]; } - (void)windowDidLoad { [self.window center]; _infoChoiceView.hidden = ![self shouldAskAboutProfile]; _automaticallyDownloadUpdatesView.hidden = ![self allowsAutomaticUpdates]; [_stackView addArrangedSubview:_promptView]; [_stackView addArrangedSubview:_automaticallyDownloadUpdatesView]; [_stackView addArrangedSubview:_infoChoiceView]; [_stackView addArrangedSubview:_placeholderView]; [_stackView addArrangedSubview:_moreInfoView]; [_stackView addArrangedSubview:_responseView]; #if SPARKLE_COPY_LOCALIZATIONS NSBundle *sparkleBundle = SUSparkleBundle(); #endif _checkButton.title = SULocalizedStringFromTableInBundle(@"Check Automatically", SPARKLE_TABLE, sparkleBundle, nil); _cancelButton.title = SULocalizedStringFromTableInBundle(@"Don’t Check", SPARKLE_TABLE, sparkleBundle, nil); _checkForUpdatesAutomaticallyTextField.stringValue = SULocalizedStringFromTableInBundle(@"Check for updates automatically?", SPARKLE_TABLE, sparkleBundle, nil); _includeAnonymousSystemProfileButton.title = SULocalizedStringFromTableInBundle(@"Include anonymous system profile", SPARKLE_TABLE, sparkleBundle, nil); _automaticallyDownloadAndInstallUpdatesButton.title = SULocalizedStringFromTableInBundle(@"Automatically download and install updates", SPARKLE_TABLE, sparkleBundle, nil); _anonymousSystemProfileDisclosureInformation.stringValue = SULocalizedStringFromTableInBundle(@"Anonymous system profile information is used to help us plan future development work. Please contact us if you have any questions about this.\n\nThis is the information that would be sent:", SPARKLE_TABLE, sparkleBundle, nil); } - (BOOL)tableView:(NSTableView *) __unused tableView shouldSelectRow:(NSInteger) __unused row { return NO; } - (NSImage *)icon { return [SUApplicationInfo bestIconForHost:_host]; } - (NSString *)promptDescription { return [NSString stringWithFormat:SULocalizedStringFromTableInBundle(@"Should %1$@ automatically check for updates? You can always check for updates manually from the %1$@ menu.", SPARKLE_TABLE, SUSparkleBundle(), nil), _host.name]; } - (IBAction)toggleMoreInfo:(id)__unused sender { // Use a placeholder view to unhide/hide before putting the more info view in place // This allows us to animate resizing the more info view in place more easily static const CGFloat TOGGLE_INFO_ANIMATION_DURATION = 0.2; BOOL disclosingInfo = (_anonymousInfoDisclosureButton.state == NSControlStateValueOn); if (disclosingInfo) { _placeholderHeightLayoutConstraint.constant = 0.0; _placeholderView.hidden = NO; [NSAnimationContext runAnimationGroup:^(NSAnimationContext * _Nonnull context) { context.duration = TOGGLE_INFO_ANIMATION_DURATION; self->_placeholderHeightLayoutConstraint.animator.constant = _moreInfoView.frame.size.height; } completionHandler:^{ self->_placeholderView.hidden = YES; self->_moreInfoView.hidden = NO; }]; } else { _placeholderHeightLayoutConstraint.constant = _moreInfoView.frame.size.height; _moreInfoView.hidden = YES; _placeholderView.hidden = NO; [NSAnimationContext runAnimationGroup:^(NSAnimationContext * _Nonnull context) { context.duration = TOGGLE_INFO_ANIMATION_DURATION; self->_placeholderHeightLayoutConstraint.animator.constant = 0.0; } completionHandler:^{ self->_placeholderView.hidden = YES; }]; } } - (IBAction)finishPrompt:(NSButton *)sender { BOOL automaticUpdateChecksEnabled = ([sender tag] == 1); NSNumber *automaticUpdateDownloading; if ([self allowsAutomaticUpdates]) { automaticUpdateDownloading = @(automaticUpdateChecksEnabled && _automaticallyDownloadUpdates); } else { automaticUpdateDownloading = nil; } SUUpdatePermissionResponse *response = [[SUUpdatePermissionResponse alloc] initWithAutomaticUpdateChecks:automaticUpdateChecksEnabled automaticUpdateDownloading:automaticUpdateDownloading sendSystemProfile:_shouldSendProfile]; _reply(response); [self close]; } - (NSTouchBar *)makeTouchBar { NSTouchBar *touchBar = [[NSTouchBar alloc] init]; touchBar.defaultItemIdentifiers = @[SUUpdatePermissionPromptTouchBarIdentifier,]; touchBar.principalItemIdentifier = SUUpdatePermissionPromptTouchBarIdentifier; touchBar.delegate = self; return touchBar; } - (NSTouchBarItem *)touchBar:(NSTouchBar * __unused)touchBar makeItemForIdentifier:(NSTouchBarItemIdentifier)identifier { if ([identifier isEqualToString:SUUpdatePermissionPromptTouchBarIdentifier]) { NSCustomTouchBarItem* item = [[NSCustomTouchBarItem alloc] initWithIdentifier:identifier]; item.viewController = [[SUTouchBarButtonGroup alloc] initByReferencingButtons:@[_checkButton, _cancelButton]]; return item; } return nil; } @end #endif ================================================ FILE: Sparkle/SUUpdatePermissionPrompt.xib ================================================ <?xml version="1.0" encoding="UTF-8"?> <document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="23727" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES"> <dependencies> <deployment identifier="macosx"/> <plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="23727"/> <capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/> </dependencies> <objects> <customObject id="-2" userLabel="File's Owner" customClass="SUUpdatePermissionPrompt"> <connections> <outlet property="_anonymousInfoDisclosureButton" destination="JW6-0z-Ud3" id="grm-Uj-KJs"/> <outlet property="_anonymousSystemProfileDisclosureInformation" destination="46" id="Ctt-KG-z3I"/> <outlet property="_automaticallyDownloadAndInstallUpdatesButton" destination="BId-kb-eYW" id="FLp-cT-Pth"/> <outlet property="_automaticallyDownloadUpdatesView" destination="37T-Ef-DtX" id="Ojd-zE-6Kd"/> <outlet property="_cancelButton" destination="cFC-wV-H3j" id="B1o-H0-cr1"/> <outlet property="_checkButton" destination="sMh-ha-r7R" id="2Oc-EC-EcG"/> <outlet property="_checkForUpdatesAutomaticallyTextField" destination="IgD-Pj-pc8" id="h2Y-hw-KHf"/> <outlet property="_includeAnonymousSystemProfileButton" destination="a90-Iq-FgS" id="v0L-n4-sGt"/> <outlet property="_infoChoiceView" destination="ov1-yV-Uol" id="RSd-bn-7bu"/> <outlet property="_moreInfoView" destination="39" id="ceJ-4H-FHd"/> <outlet property="_placeholderHeightLayoutConstraint" destination="yis-HR-OIe" id="npL-hg-wRq"/> <outlet property="_placeholderView" destination="AGr-V0-0al" id="UnQ-rl-QSS"/> <outlet property="_promptView" destination="Yrl-Dt-Qvb" id="qbO-fw-1YD"/> <outlet property="_responseView" destination="0CP-x7-YFK" id="AcW-Md-hSP"/> <outlet property="_stackView" destination="qNw-uD-4Bg" id="EIt-kn-pJ0"/> <outlet property="window" destination="5" id="126"/> </connections> </customObject> <customObject id="-1" userLabel="First Responder" customClass="FirstResponder"/> <customObject id="-3" userLabel="Application" customClass="NSObject"/> <window allowsToolTipsWhenApplicationIsInactive="NO" autorecalculatesKeyViewLoop="NO" releasedWhenClosed="NO" visibleAtLaunch="NO" animationBehavior="default" id="5" userLabel="Profile Info"> <windowStyleMask key="styleMask" titled="YES"/> <windowCollectionBehavior key="collectionBehavior" fullScreenAuxiliary="YES"/> <windowPositionMask key="initialPositionMask" leftStrut="YES" rightStrut="YES" topStrut="YES" bottomStrut="YES"/> <rect key="contentRect" x="83" y="492" width="488" height="379"/> <rect key="screenRect" x="0.0" y="0.0" width="1470" height="918"/> <view key="contentView" id="6"> <rect key="frame" x="0.0" y="0.0" width="437" height="379"/> <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/> <subviews> <stackView distribution="fill" orientation="vertical" alignment="leading" spacing="0.0" horizontalStackHuggingPriority="249.99998474121094" verticalStackHuggingPriority="249.99998474121094" detachesHiddenViews="YES" translatesAutoresizingMaskIntoConstraints="NO" id="qNw-uD-4Bg"> <rect key="frame" x="0.0" y="0.0" width="437" height="379"/> <constraints> <constraint firstAttribute="width" constant="437" id="gcK-Q4-n9c"/> </constraints> </stackView> </subviews> <constraints> <constraint firstItem="qNw-uD-4Bg" firstAttribute="top" secondItem="6" secondAttribute="top" id="BBZ-1D-MsD"/> <constraint firstAttribute="trailing" secondItem="qNw-uD-4Bg" secondAttribute="trailing" id="HvQ-3i-G7k"/> <constraint firstItem="qNw-uD-4Bg" firstAttribute="leading" secondItem="6" secondAttribute="leading" id="SsP-SG-frl"/> <constraint firstAttribute="bottom" secondItem="qNw-uD-4Bg" secondAttribute="bottom" id="i6M-Nl-VAm"/> </constraints> </view> <point key="canvasLocation" x="264" y="181"/> </window> <arrayController editable="NO" preservesSelection="NO" selectsInsertedObjects="NO" avoidsEmptySelection="NO" id="24" userLabel="Array Controller"> <declaredKeys> <string>visibleKey</string> <string>visibleValue</string> <string>displayValue</string> <string>displayKey</string> </declaredKeys> <connections> <binding destination="-2" name="contentArray" keyPath="systemProfileInformationArray" id="25"/> </connections> </arrayController> <userDefaultsController id="49" userLabel="User Defaults Controller"> <declaredKeys> <string>SUIncludeProfile</string> <string>SUSendProfileInfo</string> </declaredKeys> </userDefaultsController> <view id="Yrl-Dt-Qvb" userLabel="Prompt View"> <rect key="frame" x="0.0" y="0.0" width="437" height="78"/> <autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/> <subviews> <textField verticalHuggingPriority="750" horizontalCompressionResistancePriority="250" translatesAutoresizingMaskIntoConstraints="NO" id="IgD-Pj-pc8"> <rect key="frame" x="104" y="42" width="315" height="16"/> <constraints> <constraint firstAttribute="height" relation="greaterThanOrEqual" constant="16" id="J49-m7-iIa"/> </constraints> <textFieldCell key="cell" sendsActionOnEndEditing="YES" title="Check for updates automatically?" id="gmh-T4-BO0"> <font key="font" metaFont="systemBold"/> <color key="textColor" name="controlTextColor" catalog="System" colorSpace="catalog"/> <color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/> </textFieldCell> </textField> <textField verticalHuggingPriority="750" horizontalCompressionResistancePriority="250" translatesAutoresizingMaskIntoConstraints="NO" id="tgW-Jp-Aia"> <rect key="frame" x="104" y="0.0" width="315" height="34"/> <constraints> <constraint firstAttribute="height" relation="greaterThanOrEqual" constant="34" id="HfG-Lg-91D"/> <constraint firstAttribute="width" constant="311" id="lNZ-C1-ceR"/> </constraints> <textFieldCell key="cell" sendsActionOnEndEditing="YES" title="DO NOT LOCALIZE" id="cfa-j0-Ya4"> <font key="font" metaFont="smallSystem"/> <color key="textColor" name="controlTextColor" catalog="System" colorSpace="catalog"/> <color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/> </textFieldCell> <connections> <binding destination="-2" name="value" keyPath="promptDescription" id="dvR-Lp-2jl"/> </connections> </textField> <imageView translatesAutoresizingMaskIntoConstraints="NO" id="NBn-FN-XAx"> <rect key="frame" x="23" y="-4" width="64" height="64"/> <constraints> <constraint firstAttribute="width" constant="64" id="KbC-i0-ZyN"/> <constraint firstAttribute="height" constant="64" id="Osy-kb-JTR"/> </constraints> <imageCell key="cell" refusesFirstResponder="YES" alignment="left" animates="YES" imageScaling="axesIndependently" image="NSApplicationIcon" id="clc-hq-1NY"/> <connections> <binding destination="-2" name="value" keyPath="icon" id="JD9-MU-vwB"/> </connections> </imageView> </subviews> <constraints> <constraint firstAttribute="trailing" secondItem="tgW-Jp-Aia" secondAttribute="trailing" constant="20" symbolic="YES" id="Fwc-c2-DDZ"/> <constraint firstAttribute="trailing" secondItem="IgD-Pj-pc8" secondAttribute="trailing" constant="20" symbolic="YES" id="HXU-lh-GWa"/> <constraint firstItem="IgD-Pj-pc8" firstAttribute="top" secondItem="Yrl-Dt-Qvb" secondAttribute="top" constant="20" symbolic="YES" id="Wl8-N0-skJ"/> <constraint firstItem="NBn-FN-XAx" firstAttribute="leading" secondItem="Yrl-Dt-Qvb" secondAttribute="leading" constant="23" id="Xku-5M-xMC"/> <constraint firstItem="tgW-Jp-Aia" firstAttribute="leading" secondItem="NBn-FN-XAx" secondAttribute="trailing" constant="19" id="fPB-MW-Oc2"/> <constraint firstItem="NBn-FN-XAx" firstAttribute="top" secondItem="Yrl-Dt-Qvb" secondAttribute="top" constant="18" id="fad-TA-xIj"/> <constraint firstItem="tgW-Jp-Aia" firstAttribute="top" secondItem="IgD-Pj-pc8" secondAttribute="bottom" constant="8" id="nN4-jN-Fdn"/> <constraint firstItem="IgD-Pj-pc8" firstAttribute="leading" secondItem="NBn-FN-XAx" secondAttribute="trailing" constant="19" id="s7w-Aw-JQY"/> <constraint firstAttribute="bottom" secondItem="tgW-Jp-Aia" secondAttribute="bottom" id="uS1-FX-kSX"/> </constraints> <point key="canvasLocation" x="-241" y="-37"/> </view> <customView hidden="YES" id="39" userLabel="MoreInfoView"> <rect key="frame" x="0.0" y="0.0" width="434" height="205"/> <autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/> <subviews> <textField verticalHuggingPriority="750" horizontalCompressionResistancePriority="250" translatesAutoresizingMaskIntoConstraints="NO" id="46" userLabel="SystemProfileInfo"> <rect key="frame" x="104" y="123" width="312" height="70"/> <constraints> <constraint firstAttribute="height" relation="greaterThanOrEqual" constant="70" id="Csk-S6-dh4"/> <constraint firstAttribute="width" constant="308" id="iAG-SY-yVp"/> </constraints> <textFieldCell key="cell" sendsActionOnEndEditing="YES" id="183"> <font key="font" metaFont="smallSystem"/> <string key="title">Anonymous system profile information is used to help us plan future development work. Please contact us if you have any questions about this. This is the information that would be sent:</string> <color key="textColor" name="controlTextColor" catalog="System" colorSpace="catalog"/> <color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/> </textFieldCell> </textField> <scrollView autohidesScrollers="YES" horizontalLineScroll="16" horizontalPageScroll="0.0" verticalLineScroll="16" verticalPageScroll="0.0" hasHorizontalScroller="NO" usesPredominantAxisScrolling="NO" translatesAutoresizingMaskIntoConstraints="NO" id="40" userLabel="ScrollView"> <rect key="frame" x="103" y="0.0" width="314" height="115"/> <clipView key="contentView" id="sbp-rk-wxX"> <rect key="frame" x="1" y="1" width="312" height="113"/> <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/> <subviews> <tableView focusRingType="none" verticalHuggingPriority="750" allowsExpansionToolTips="YES" columnAutoresizingStyle="lastColumnOnly" alternatingRowBackgroundColors="YES" multipleSelection="NO" autosaveColumns="NO" typeSelect="NO" rowHeight="14" id="41"> <rect key="frame" x="0.0" y="0.0" width="312" height="113"/> <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/> <size key="intercellSpacing" width="3" height="2"/> <color key="backgroundColor" name="controlBackgroundColor" catalog="System" colorSpace="catalog"/> <color key="gridColor" name="gridColor" catalog="System" colorSpace="catalog"/> <tableColumns> <tableColumn editable="NO" width="128" minWidth="40" maxWidth="1000" id="42"> <tableHeaderCell key="headerCell" lineBreakMode="truncatingTail" borderStyle="border" alignment="left"> <color key="textColor" name="headerTextColor" catalog="System" colorSpace="catalog"/> <color key="backgroundColor" white="0.33333298560000002" alpha="1" colorSpace="calibratedWhite"/> </tableHeaderCell> <textFieldCell key="dataCell" controlSize="small" selectable="YES" alignment="left" title="Text Cell" id="43"> <font key="font" metaFont="smallSystem"/> <color key="textColor" name="controlTextColor" catalog="System" colorSpace="catalog"/> <color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/> </textFieldCell> <tableColumnResizingMask key="resizingMask" resizeWithTable="YES" userResizable="YES"/> <connections> <binding destination="24" name="value" keyPath="arrangedObjects.displayKey" id="174"/> </connections> </tableColumn> <tableColumn editable="NO" width="137" minWidth="40" maxWidth="1000" id="44"> <tableHeaderCell key="headerCell" lineBreakMode="truncatingTail" borderStyle="border" alignment="left"> <color key="textColor" name="headerTextColor" catalog="System" colorSpace="catalog"/> <color key="backgroundColor" white="0.33333298560000002" alpha="1" colorSpace="calibratedWhite"/> </tableHeaderCell> <textFieldCell key="dataCell" controlSize="small" selectable="YES" alignment="left" title="Text Cell" id="45"> <font key="font" metaFont="smallSystem"/> <color key="textColor" name="controlTextColor" catalog="System" colorSpace="catalog"/> <color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/> </textFieldCell> <tableColumnResizingMask key="resizingMask" resizeWithTable="YES" userResizable="YES"/> <connections> <binding destination="24" name="value" keyPath="arrangedObjects.displayValue" id="173"/> </connections> </tableColumn> </tableColumns> <connections> <outlet property="delegate" destination="-2" id="oca-Wx-0pN"/> </connections> </tableView> </subviews> </clipView> <constraints> <constraint firstAttribute="height" constant="115" id="g4H-Z1-0xq"/> </constraints> <scroller key="horizontalScroller" hidden="YES" wantsLayer="YES" verticalHuggingPriority="750" controlSize="small" horizontal="YES" id="185"> <rect key="frame" x="-100" y="-100" width="345" height="11"/> <autoresizingMask key="autoresizingMask"/> </scroller> <scroller key="verticalScroller" hidden="YES" wantsLayer="YES" verticalHuggingPriority="750" controlSize="small" horizontal="NO" id="184"> <rect key="frame" x="-22" y="1" width="11" height="125"/> <autoresizingMask key="autoresizingMask"/> </scroller> </scrollView> </subviews> <constraints> <constraint firstItem="46" firstAttribute="top" secondItem="39" secondAttribute="top" constant="12" id="0rj-EA-SIj"/> <constraint firstItem="40" firstAttribute="trailing" secondItem="46" secondAttribute="trailing" constant="3" id="5Ze-wu-AsK"/> <constraint firstItem="40" firstAttribute="leading" secondItem="46" secondAttribute="leading" constant="-3" id="Qg9-kp-Lzl"/> <constraint firstItem="46" firstAttribute="leading" secondItem="39" secondAttribute="leading" constant="106" id="TMg-61-Tbj"/> <constraint firstAttribute="trailing" secondItem="46" secondAttribute="trailing" constant="20" id="fYg-If-EVL"/> <constraint firstAttribute="bottom" secondItem="40" secondAttribute="bottom" id="o4Y-3j-NmQ"/> <constraint firstItem="40" firstAttribute="top" secondItem="46" secondAttribute="bottom" constant="8" symbolic="YES" id="seb-Gl-jtw"/> </constraints> <point key="canvasLocation" x="-621" y="194"/> </customView> <customView id="0CP-x7-YFK" userLabel="Response View"> <rect key="frame" x="0.0" y="0.0" width="437" height="55"/> <autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/> <subviews> <customView translatesAutoresizingMaskIntoConstraints="NO" id="ilj-QP-33p" userLabel="View"> <rect key="frame" x="0.0" y="0.0" width="437" height="55"/> <subviews> <button tag="1" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="sMh-ha-r7R"> <rect key="frame" x="260" y="13" width="164" height="32"/> <buttonCell key="cell" type="push" title="Check Automatically" bezelStyle="rounded" alignment="center" state="on" borderStyle="border" tag="1" inset="2" id="OhZ-1K-DmA"> <behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/> <font key="font" metaFont="system"/> <string key="keyEquivalent" base64-UTF8="YES"> DQ </string> </buttonCell> <constraints> <constraint firstAttribute="width" relation="greaterThanOrEqual" constant="150" id="gon-Nu-M8r"/> </constraints> <connections> <action selector="finishPrompt:" target="-2" id="sms-z7-2Ij"/> </connections> </button> <button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="cFC-wV-H3j"> <rect key="frame" x="145" y="13" width="115" height="32"/> <buttonCell key="cell" type="push" title="Don’t Check" bezelStyle="rounded" alignment="center" borderStyle="border" inset="2" id="cCJ-V0-aTi"> <behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/> <font key="font" metaFont="system"/> <string key="keyEquivalent" base64-UTF8="YES"> Gw </string> </buttonCell> <constraints> <constraint firstAttribute="width" relation="greaterThanOrEqual" constant="101" id="Fta-eR-U3P"/> </constraints> <connections> <action selector="finishPrompt:" target="-2" id="QJG-fs-Y9u"/> </connections> </button> </subviews> <constraints> <constraint firstAttribute="bottom" secondItem="cFC-wV-H3j" secondAttribute="bottom" constant="20" id="B76-mf-2m9"/> <constraint firstItem="cFC-wV-H3j" firstAttribute="top" secondItem="ilj-QP-33p" secondAttribute="top" constant="15" id="Fyr-gv-3TM"/> <constraint firstItem="sMh-ha-r7R" firstAttribute="leading" secondItem="cFC-wV-H3j" secondAttribute="trailing" constant="14" id="Gse-h5-ctH"/> <constraint firstAttribute="width" constant="437" id="KXc-Kv-G8j"/> <constraint firstAttribute="trailing" secondItem="sMh-ha-r7R" secondAttribute="trailing" constant="20" symbolic="YES" id="PWc-tb-dWV"/> <constraint firstItem="sMh-ha-r7R" firstAttribute="centerY" secondItem="cFC-wV-H3j" secondAttribute="centerY" id="dmL-eu-rzf"/> <constraint firstItem="cFC-wV-H3j" firstAttribute="leading" relation="greaterThanOrEqual" secondItem="ilj-QP-33p" secondAttribute="leading" constant="20" symbolic="YES" id="l3J-Va-WaM"/> </constraints> </customView> </subviews> <constraints> <constraint firstItem="ilj-QP-33p" firstAttribute="top" secondItem="0CP-x7-YFK" secondAttribute="top" id="5In-8H-mUY"/> <constraint firstAttribute="bottom" secondItem="ilj-QP-33p" secondAttribute="bottom" id="XNF-yO-yBl"/> <constraint firstItem="ilj-QP-33p" firstAttribute="leading" secondItem="0CP-x7-YFK" secondAttribute="leading" id="d3g-7o-f52"/> <constraint firstAttribute="trailing" secondItem="ilj-QP-33p" secondAttribute="trailing" id="jzr-jk-sQQ"/> </constraints> <point key="canvasLocation" x="-291" y="554"/> </customView> <customView id="ov1-yV-Uol" userLabel="Anonymous Info View"> <rect key="frame" x="0.0" y="0.0" width="437" height="20"/> <autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/> <subviews> <customView translatesAutoresizingMaskIntoConstraints="NO" id="a0k-dW-Jhx" userLabel="View"> <rect key="frame" x="0.0" y="0.0" width="437" height="20"/> <subviews> <button focusRingType="none" translatesAutoresizingMaskIntoConstraints="NO" id="a90-Iq-FgS" userLabel="IncludeInfoButton"> <rect key="frame" x="104" y="-1" width="200" height="16"/> <buttonCell key="cell" type="check" title="Include anonymous system profile" bezelStyle="regularSquare" imagePosition="left" alignment="left" controlSize="small" state="on" focusRingType="none" inset="2" id="gz7-LM-gNf"> <behavior key="behavior" changeContents="YES" doesNotDimImage="YES" lightByContents="YES"/> <font key="font" metaFont="smallSystem"/> </buttonCell> <constraints> <constraint firstAttribute="height" constant="14" id="STF-87-eYy"/> </constraints> <connections> <binding destination="-2" name="value" keyPath="shouldSendProfile" id="HPm-tg-NCZ"> <dictionary key="options"> <integer key="NSNullPlaceholder" value="1"/> <bool key="NSValidatesImmediately" value="YES"/> </dictionary> </binding> </connections> </button> <button focusRingType="none" horizontalHuggingPriority="750" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="JW6-0z-Ud3"> <rect key="frame" x="85" y="-1" width="16" height="16"/> <buttonCell key="cell" type="disclosureTriangle" bezelStyle="disclosure" imagePosition="overlaps" alignment="center" borderStyle="border" focusRingType="none" imageScaling="proportionallyDown" inset="2" id="yd4-Id-v7q"> <behavior key="behavior" pushIn="YES" changeBackground="YES" changeGray="YES" lightByContents="YES"/> <font key="font" metaFont="system"/> </buttonCell> <constraints> <constraint firstAttribute="height" constant="16" id="HMA-GI-C0a"/> <constraint firstAttribute="width" constant="16" id="Qxo-Ga-nVP"/> </constraints> <connections> <action selector="toggleMoreInfo:" target="-2" id="AqY-Mp-X0q"/> <binding destination="-2" name="hidden" keyPath="shouldAskAboutProfile" id="IJh-ox-Lrn"> <dictionary key="options"> <string key="NSValueTransformerName">NSNegateBoolean</string> </dictionary> </binding> </connections> </button> </subviews> <constraints> <constraint firstItem="JW6-0z-Ud3" firstAttribute="leading" secondItem="a0k-dW-Jhx" secondAttribute="leading" constant="85" id="6Y1-s7-g6r"/> <constraint firstItem="a90-Iq-FgS" firstAttribute="centerY" secondItem="JW6-0z-Ud3" secondAttribute="centerY" id="BNJ-xV-faQ"/> <constraint firstItem="a90-Iq-FgS" firstAttribute="leading" secondItem="a0k-dW-Jhx" secondAttribute="leading" constant="105" id="ILg-Qu-R6M"/> <constraint firstItem="a90-Iq-FgS" firstAttribute="top" secondItem="a0k-dW-Jhx" secondAttribute="top" constant="6" id="Mh6-5L-xqM"/> <constraint firstAttribute="trailing" relation="greaterThanOrEqual" secondItem="a90-Iq-FgS" secondAttribute="trailing" constant="20" id="U4G-Eh-HPu"/> <constraint firstItem="a90-Iq-FgS" firstAttribute="leading" relation="greaterThanOrEqual" secondItem="JW6-0z-Ud3" secondAttribute="trailing" constant="4" id="axA-s0-uGg"/> <constraint firstAttribute="width" constant="437" id="iUc-Zs-h9S"/> <constraint firstAttribute="bottom" secondItem="a90-Iq-FgS" secondAttribute="bottom" id="qij-uH-6Yl"/> </constraints> </customView> </subviews> <constraints> <constraint firstItem="a0k-dW-Jhx" firstAttribute="leading" secondItem="ov1-yV-Uol" secondAttribute="leading" id="dE2-R0-ENa"/> <constraint firstItem="a0k-dW-Jhx" firstAttribute="top" secondItem="ov1-yV-Uol" secondAttribute="top" id="ibf-cg-DNR"/> <constraint firstAttribute="trailing" secondItem="a0k-dW-Jhx" secondAttribute="trailing" id="rAn-bJ-Mp6"/> <constraint firstAttribute="bottom" secondItem="a0k-dW-Jhx" secondAttribute="bottom" id="xw5-bj-VOd"/> </constraints> <point key="canvasLocation" x="-135.5" y="242.5"/> </customView> <customView id="37T-Ef-DtX" userLabel="Automatic Download View"> <rect key="frame" x="0.0" y="0.0" width="437" height="20"/> <autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/> <subviews> <customView translatesAutoresizingMaskIntoConstraints="NO" id="MEW-2n-kJE" userLabel="View"> <rect key="frame" x="0.0" y="0.0" width="437" height="20"/> <subviews> <button focusRingType="none" translatesAutoresizingMaskIntoConstraints="NO" id="BId-kb-eYW" userLabel="automaticallyDownloadUpdatesButton"> <rect key="frame" x="104" y="-1" width="248" height="16"/> <buttonCell key="cell" type="check" title="Automatically download and install updates" bezelStyle="regularSquare" imagePosition="left" alignment="left" controlSize="small" focusRingType="none" inset="2" id="AUc-33-qGN"> <behavior key="behavior" changeContents="YES" doesNotDimImage="YES" lightByContents="YES"/> <font key="font" metaFont="smallSystem"/> </buttonCell> <constraints> <constraint firstAttribute="height" constant="14" id="kJy-g7-0Un"/> </constraints> <connections> <binding destination="-2" name="value" keyPath="automaticallyDownloadUpdates" id="5Me-7t-mMu"/> </connections> </button> </subviews> <constraints> <constraint firstItem="BId-kb-eYW" firstAttribute="leading" secondItem="MEW-2n-kJE" secondAttribute="leading" constant="105" id="MnW-AM-QzP"/> <constraint firstAttribute="bottom" secondItem="BId-kb-eYW" secondAttribute="bottom" id="QXK-90-kUJ"/> <constraint firstAttribute="trailing" relation="greaterThanOrEqual" secondItem="BId-kb-eYW" secondAttribute="trailing" constant="20" id="Vk3-CH-cXd"/> <constraint firstItem="BId-kb-eYW" firstAttribute="top" secondItem="MEW-2n-kJE" secondAttribute="top" constant="6" id="YrX-aL-LAq"/> <constraint firstAttribute="width" constant="437" id="n4W-Z4-w9i"/> </constraints> </customView> </subviews> <constraints> <constraint firstAttribute="bottom" secondItem="MEW-2n-kJE" secondAttribute="bottom" id="4iq-ZN-n9x"/> <constraint firstItem="MEW-2n-kJE" firstAttribute="top" secondItem="37T-Ef-DtX" secondAttribute="top" id="FCe-KY-rHW"/> <constraint firstItem="MEW-2n-kJE" firstAttribute="leading" secondItem="37T-Ef-DtX" secondAttribute="leading" id="dsv-fP-ffg"/> <constraint firstAttribute="trailing" secondItem="MEW-2n-kJE" secondAttribute="trailing" id="puT-Df-xgH"/> </constraints> <point key="canvasLocation" x="-136" y="181"/> </customView> <customView hidden="YES" translatesAutoresizingMaskIntoConstraints="NO" id="AGr-V0-0al" userLabel="Placeholder View"> <rect key="frame" x="0.0" y="0.0" width="437" height="205"/> <constraints> <constraint firstAttribute="width" constant="437" id="4p7-cJ-U8Z"/> <constraint firstAttribute="height" constant="205" id="yis-HR-OIe"/> </constraints> <point key="canvasLocation" x="-643" y="495"/> </customView> </objects> <resources> <image name="NSApplicationIcon" width="32" height="32"/> </resources> </document> ================================================ FILE: Sparkle/SUUpdatePermissionResponse.h ================================================ // // SUUpdatePermissionResponse.h // Sparkle // // Created by Mayur Pawashe on 2/8/16. // Copyright © 2016 Sparkle Project. All rights reserved. // #import <Foundation/Foundation.h> #if defined(BUILDING_SPARKLE_SOURCES_EXTERNALLY) // Ignore incorrect warning #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wquoted-include-in-framework-header" #import "SUExport.h" #pragma clang diagnostic pop #else #import <Sparkle/SUExport.h> #endif NS_ASSUME_NONNULL_BEGIN /** This class represents a response for permission to check updates. */ SU_EXPORT NS_SWIFT_SENDABLE @interface SUUpdatePermissionResponse : NSObject<NSSecureCoding> /** Initializes a new update permission response instance. @param automaticUpdateChecks Flag to enable automatic update checks. @param sendSystemProfile Flag for if system profile information should be sent to the server hosting the appcast. */ - (instancetype)initWithAutomaticUpdateChecks:(BOOL)automaticUpdateChecks sendSystemProfile:(BOOL)sendSystemProfile; /** Initializes a new update permission response instance. @param automaticUpdateChecks Flag to enable automatic update checks. @param automaticUpdateDownloading Flag to enable automatic downloading and installing of updates. If this is nil, this option will be ignored. @param sendSystemProfile Flag for if system profile information should be sent to the server hosting the appcast. */ - (instancetype)initWithAutomaticUpdateChecks:(BOOL)automaticUpdateChecks automaticUpdateDownloading:(NSNumber * _Nullable)automaticUpdateDownloading sendSystemProfile:(BOOL)sendSystemProfile; /* Use -initWithAutomaticUpdateChecks:sendSystemProfile: instead. */ - (instancetype)init NS_UNAVAILABLE; /** A read-only property indicating if update checks should be done automatically. */ @property (nonatomic, readonly) BOOL automaticUpdateChecks; /** A read-only property indicating if updates should be automatically downloaded and installed. If this property is `nil`, then no user choice was made for this option. If `automaticUpdateChecks` is `NO` then this property should not be `@(YES)`. Set it to `NO` if the user was given the choice of automatically downloading and installing updates, otherwise set it to `nil`. */ @property (nonatomic, readonly, nullable) NSNumber *automaticUpdateDownloading; /** A read-only property indicating if system profile should be sent or not. */ @property (nonatomic, readonly) BOOL sendSystemProfile; @end NS_ASSUME_NONNULL_END ================================================ FILE: Sparkle/SUUpdatePermissionResponse.m ================================================ // // SUUpdatePermissionResponse.m // Sparkle // // Created by Mayur Pawashe on 2/8/16. // Copyright © 2016 Sparkle Project. All rights reserved. // #import "SUUpdatePermissionResponse.h" #include "AppKitPrevention.h" static NSString *SUUpdatePermissionAutomaticUpdateChecksKey = @"SUUpdatePermissionAutomaticUpdateChecks"; static NSString *SUUpdatePermissionAutomaticUpdateDownloadingKey = @"SUUpdatePermissionAutomaticUpdateDownloading"; static NSString *SUUpdatePermissionSendSystemProfileKey = @"SUUpdatePermissionSendSystemProfile"; @implementation SUUpdatePermissionResponse @synthesize automaticUpdateChecks = _automaticUpdateChecks; @synthesize sendSystemProfile = _sendSystemProfile; @synthesize automaticUpdateDownloading = _automaticUpdateDownloading; + (BOOL)supportsSecureCoding { return YES; } - (instancetype)initWithCoder:(NSCoder *)decoder { BOOL automaticUpdateChecks = [decoder decodeBoolForKey:SUUpdatePermissionAutomaticUpdateChecksKey]; NSNumber *automaticUpdateDownloading = [decoder decodeObjectOfClass:[NSNumber class] forKey:SUUpdatePermissionAutomaticUpdateDownloadingKey]; BOOL sendSystemProfile = [decoder decodeBoolForKey:SUUpdatePermissionSendSystemProfileKey]; return [self initWithAutomaticUpdateChecks:automaticUpdateChecks automaticUpdateDownloading:automaticUpdateDownloading sendSystemProfile:sendSystemProfile]; } - (void)encodeWithCoder:(NSCoder *)encoder { [encoder encodeBool:_automaticUpdateChecks forKey:SUUpdatePermissionAutomaticUpdateChecksKey]; if (_automaticUpdateDownloading != nil) { [encoder encodeObject:_automaticUpdateDownloading forKey:SUUpdatePermissionAutomaticUpdateDownloadingKey]; } [encoder encodeBool:_sendSystemProfile forKey:SUUpdatePermissionSendSystemProfileKey]; } - (instancetype)initWithAutomaticUpdateChecks:(BOOL)automaticUpdateChecks automaticUpdateDownloading:(NSNumber * _Nullable)automaticUpdateDownloading sendSystemProfile:(BOOL)sendSystemProfile { self = [super init]; if (self != nil) { _automaticUpdateChecks = automaticUpdateChecks; _automaticUpdateDownloading = automaticUpdateDownloading; _sendSystemProfile = sendSystemProfile; } return self; } - (instancetype)initWithAutomaticUpdateChecks:(BOOL)automaticUpdateChecks sendSystemProfile:(BOOL)sendSystemProfile { return [self initWithAutomaticUpdateChecks:automaticUpdateChecks automaticUpdateDownloading:nil sendSystemProfile:sendSystemProfile]; } @end ================================================ FILE: Sparkle/SUUpdateValidator.h ================================================ // // SUUpdateValidator.h // Sparkle // // Created by Mayur Pawashe on 12/3/16. // Copyright © 2016 Sparkle Project. All rights reserved. // #import <Foundation/Foundation.h> @class SUHost; @class SUSignatures; @class SPUVerifierInformation; NS_ASSUME_NONNULL_BEGIN #ifndef BUILDING_SPARKLE_TESTS SPU_OBJC_DIRECT_MEMBERS #endif @interface SUUpdateValidator : NSObject - (instancetype)init NS_UNAVAILABLE; - (instancetype)initWithDownloadPath:(NSString *)downloadPath signatures:(SUSignatures *)signatures host:(SUHost *)host verifierInformation:(SPUVerifierInformation * _Nullable)verifierInformation; - (BOOL)validateHostHasPublicKeys:(NSError **)error; // This is "pre" validation, before the archive has been extracted - (BOOL)validateDownloadPathWithFallbackOnCodeSigning:(BOOL)fallbackOnCodeSigning error:(NSError **)error; // This is "post" validation, after an archive has been extracted - (BOOL)validateWithUpdateDirectory:(NSString *)updateDirectory error:(NSError **)error; @end NS_ASSUME_NONNULL_END ================================================ FILE: Sparkle/SUUpdateValidator.m ================================================ // // SUUpdateValidator.m // Sparkle // // Created by Mayur Pawashe on 12/3/16. // Copyright © 2016 Sparkle Project. All rights reserved. // #import "SUUpdateValidator.h" #import "SUSignatureVerifier.h" #import "SUCodeSigningVerifier.h" #import "SUInstaller.h" #import "SUHost.h" #import "SULog.h" #import "SUSignatures.h" #import "SUErrors.h" #import "SPUVerifierInformation.h" #include "AppKitPrevention.h" @implementation SUUpdateValidator { SUHost *_host; SUSignatures *_signatures; NSString *_downloadPath; SPUVerifierInformation *_verifierInformation; BOOL _prevalidatedSignature; BOOL _validatedDownloadUsingCodeSigning; } - (instancetype)initWithDownloadPath:(NSString *)downloadPath signatures:(SUSignatures *)signatures host:(SUHost *)host verifierInformation:(SPUVerifierInformation * _Nullable)verifierInformation { self = [super init]; if (self != nil) { _downloadPath = [downloadPath copy]; _signatures = signatures; _host = host; _verifierInformation = verifierInformation; } return self; } - (BOOL)validateHostHasPublicKeys:(NSError * __autoreleasing *)error { SUPublicKeys *publicKeys = _host.publicKeys; if (!publicKeys.hasAnyKeys) { if (error != NULL) { *error = [NSError errorWithDomain:SUSparkleErrorDomain code:SUInsufficientSigningError userInfo:@{ NSLocalizedDescriptionKey: @"Failed to validate update before unarchiving because no (Ed)DSA public key was found in the old app" }]; } return NO; } return YES; } - (BOOL)validateDownloadPathWithFallbackOnCodeSigning:(BOOL)fallbackOnCodeSigning error:(NSError * __autoreleasing *)error { SUPublicKeys *publicKeys = _host.publicKeys; SUSignatures *signatures = _signatures; NSError *dsaVerificationError = nil; if ([SUSignatureVerifier validatePath:_downloadPath withSignatures:signatures withPublicKeys:publicKeys verifierInformation:_verifierInformation error:&dsaVerificationError]) { _prevalidatedSignature = YES; return YES; } NSMutableArray<NSError *> *underlyingErrors = [[NSMutableArray alloc] init]; if (dsaVerificationError != nil) { [underlyingErrors addObject:dsaVerificationError]; } if (fallbackOnCodeSigning) { SULog(SULogLevelError, @"Failed to validate update archive with (Ed)DSA signing. Trying fallback with Apple Developer ID code signing verification: %@", dsaVerificationError); // (Ed)DSA validation failed + signed archives are required + regular app update // As fallback for key rotation, check if the archive is Developer ID signed with a team ID that matches the host NSError *codeSignError = nil; NSURL *downloadURL = [NSURL fileURLWithPath:_downloadPath isDirectory:NO]; if (![SUCodeSigningVerifier codeSignatureIsValidAtDownloadURL:downloadURL andMatchesDeveloperIDTeamFromOldBundleURL:_host.bundle.bundleURL error:&codeSignError]) { SULog(SULogLevelError, @"Failed to validate update archive with Developer ID code signing fallback: %@", codeSignError); if (codeSignError != nil) { [underlyingErrors addObject:codeSignError]; } } else { _prevalidatedSignature = YES; _validatedDownloadUsingCodeSigning = YES; return YES; } } if (error != NULL) { NSMutableDictionary<NSString *, id> *userInfo = [[NSMutableDictionary alloc] init]; userInfo[NSLocalizedDescriptionKey] = [NSString stringWithFormat:@"(Ed)DSA signature validation before unarchiving failed for update %@", _downloadPath]; if (dsaVerificationError != nil) { // This is the primary error userInfo[NSUnderlyingErrorKey] = dsaVerificationError; } if (underlyingErrors.count > 1) { if (@available(macOS 11.3, *)) { userInfo[NSMultipleUnderlyingErrorsKey] = [underlyingErrors copy]; } } *error = [NSError errorWithDomain:SUSparkleErrorDomain code:SUInstallationError userInfo:[userInfo copy]]; } return NO; } - (BOOL)validateWithUpdateDirectory:(NSString *)updateDirectory error:(NSError * __autoreleasing *)error { SUSignatures *signatures = _signatures; NSString *downloadPath = _downloadPath; SUHost *host = _host; #if SPARKLE_BUILD_PACKAGE_SUPPORT BOOL isPackage = NO; #endif // install source could point to a new bundle or a package NSString *installSource = [SUInstaller installSourcePathInUpdateFolder:updateDirectory forHost:host #if SPARKLE_BUILD_PACKAGE_SUPPORT isPackage:&isPackage isGuided:NULL #endif ]; if (installSource == nil) { if (error != NULL) { *error = [NSError errorWithDomain:SUSparkleErrorDomain code:SUValidationError userInfo:@{ NSLocalizedDescriptionKey: @"No suitable install is found in the update. The update will be rejected." }]; } return NO; } NSURL *installSourceURL = [NSURL fileURLWithPath:installSource]; if (!_prevalidatedSignature) { #if SPARKLE_BUILD_PACKAGE_SUPPORT // Check to see if we have a package or bundle to validate if (isPackage) { // If we get here, then the appcast installation type was lying to us.. This error will be caught later when starting the installer. // For package type updates, all we do is check if the EdDSA signature is valid NSError *innerError = nil; SUPublicKeys *publicKeys = host.publicKeys; BOOL validationCheckSuccess = [SUSignatureVerifier validatePath:downloadPath withSignatures:signatures withPublicKeys:publicKeys verifierInformation:_verifierInformation error:&innerError]; if (!validationCheckSuccess) { if (error != NULL) { *error = [NSError errorWithDomain:SUSparkleErrorDomain code:SUValidationError userInfo:@{ NSLocalizedDescriptionKey: @"EdDSA signature validation of the package failed. The update contains an installer package, and valid EdDSA signatures are mandatory for all installer packages. The update will be rejected. Sign the installer with a valid EdDSA key or use an .app bundle update instead.", NSUnderlyingErrorKey: innerError }]; } } return validationCheckSuccess; } else #endif { // For application bundle updates, we check both the EdDSA and Apple code signing signatures return [self validateUpdateForHost:host downloadedToPath:downloadPath newBundleURL:installSourceURL signatures:signatures error:error]; } } #if SPARKLE_BUILD_PACKAGE_SUPPORT else if (isPackage) { // We already prevalidated the package and nothing else needs to be done return YES; } #endif else { // We already validated the download archive // Let's check if the update passes Sparkle's basic update policy and that the update is properly signed // Currently, this case gets hit for binary delta updates and updates requiring SUVerifyUpdateBeforeExtraction NSBundle *newBundle = [NSBundle bundleWithURL:installSourceURL]; SUHost *newHost = [[SUHost alloc] initWithBundle:newBundle]; SUPublicKeys *publicKeys = host.publicKeys; SUPublicKeys *newPublicKeys = newHost.publicKeys; BOOL oldHasAnyDSAKey = NO; BOOL newHasAnyDSAKey = NO; BOOL hostIsCodeSigned = NO; BOOL updateIsCodeSigned = NO; [self getHostIsCodeSigned:&hostIsCodeSigned updateIsCodeSigned:&updateIsCodeSigned hostHasAnyDSAKey:&oldHasAnyDSAKey updateHasAnyDSAKey:&newHasAnyDSAKey migratesDSAKeys:NULL hostPublicKeys:publicKeys updatePublicKeys:newPublicKeys hostBundleURL:host.bundle.bundleURL updateBundleURL:installSourceURL]; if (![self passesBasicUpdatePolicyWithHostIsCodeSigned:hostIsCodeSigned updateIsCodeSigned:updateIsCodeSigned hostHasAnyDSAKey:oldHasAnyDSAKey updateHasAnyDSAKey:newHasAnyDSAKey error:error]) { return NO; } NSError *codeSigningInnerError = nil; if (updateIsCodeSigned && ![SUCodeSigningVerifier codeSignatureIsValidAtBundleURL:installSourceURL error:&codeSigningInnerError]) { if (error != NULL) { NSMutableDictionary *userInfo = [NSMutableDictionary dictionary]; userInfo[NSLocalizedDescriptionKey] = @"The update archive is validly signed, but the app's Apple code signing signature is corrupted. The update will be rejected."; if (codeSigningInnerError != nil) { userInfo[NSUnderlyingErrorKey] = codeSigningInnerError; } *error = [NSError errorWithDomain:SUSparkleErrorDomain code:SUValidationError userInfo:userInfo]; } return NO; } if (_validatedDownloadUsingCodeSigning) { // Old EdDSA key failed on download archive, and Apple Code signing validation was used as a fallback (with SUVerifyUpdateBeforeExtraction set to YES), // which means the developer may be rotating keys. // So we must validate new EdDSA key with the new download. // This is a policy to ensure the next update can be updatable with the new EdDSA key (not a security measure). NSError *validateInnerError = nil; BOOL validationCheckSuccess = [SUSignatureVerifier validatePath:downloadPath withSignatures:signatures withPublicKeys:newPublicKeys verifierInformation:_verifierInformation error:&validateInnerError]; if (!validationCheckSuccess) { if (error != NULL) { NSMutableDictionary *userInfo = [NSMutableDictionary dictionary]; userInfo[NSLocalizedDescriptionKey] = @"(Ed)DSA signature validation failed after using Apple code signing to validate the update archive. The update has a public (Ed)DSA key, but the public key shipped with the update doesn't match the signature. To prevent future problems, the update will be rejected."; if (validateInnerError != nil) { userInfo[NSUnderlyingErrorKey] = validateInnerError; } *error = [NSError errorWithDomain:SUSparkleErrorDomain code:SUValidationError userInfo:userInfo]; } return NO; } } return YES; } } - (void)getHostIsCodeSigned:(BOOL *)outHostIsCodeSigned updateIsCodeSigned:(BOOL *)outUpdateIsCodeSigned hostHasAnyDSAKey:(BOOL *)outHostHasAnyDSAKey updateHasAnyDSAKey:(BOOL *)outUpdateHasAnyDSAKey migratesDSAKeys:(BOOL *)outMigratesDSAKeys hostPublicKeys:(SUPublicKeys *)hostPublicKeys updatePublicKeys:(SUPublicKeys *)updatePublicKeys hostBundleURL:(NSURL *)hostBundleURL updateBundleURL:(NSURL *)updateBundleURL SPU_OBJC_DIRECT { BOOL oldHasLegacyDSAKey = hostPublicKeys.dsaPubKeyStatus != SUSigningInputStatusAbsent; BOOL oldHasEdDSAKey = hostPublicKeys.ed25519PubKeyStatus != SUSigningInputStatusAbsent; BOOL oldHasAnyDSAKey = oldHasLegacyDSAKey || oldHasEdDSAKey; if (outHostHasAnyDSAKey != NULL) { *outHostHasAnyDSAKey = oldHasAnyDSAKey; } BOOL newHasLegacyDSAKey = updatePublicKeys.dsaPubKeyStatus != SUSigningInputStatusAbsent; BOOL newHasEdDSAKey = updatePublicKeys.ed25519PubKeyStatus != SUSigningInputStatusAbsent; BOOL newHasAnyDSAKey = newHasLegacyDSAKey || newHasEdDSAKey; if (outUpdateHasAnyDSAKey != NULL) { *outUpdateHasAnyDSAKey = newHasAnyDSAKey; } BOOL migratesDSAKeys = oldHasLegacyDSAKey && !oldHasEdDSAKey && newHasEdDSAKey && !newHasLegacyDSAKey; if (outMigratesDSAKeys != NULL) { *outMigratesDSAKeys = migratesDSAKeys; } BOOL hostIsCodeSigned = [SUCodeSigningVerifier bundleAtURLIsCodeSigned:hostBundleURL]; if (outHostIsCodeSigned != NULL) { *outHostIsCodeSigned = hostIsCodeSigned; } BOOL updateIsCodeSigned = [SUCodeSigningVerifier bundleAtURLIsCodeSigned:updateBundleURL]; if (outUpdateIsCodeSigned != NULL) { *outUpdateIsCodeSigned = updateIsCodeSigned; } } // This is not essential for security, only a policy - (BOOL)passesBasicUpdatePolicyWithHostIsCodeSigned:(BOOL)hostIsCodeSigned updateIsCodeSigned:(BOOL)updateIsCodeSigned hostHasAnyDSAKey:(BOOL)hostHasAnyDSAKey updateHasAnyDSAKey:(BOOL)updateHasAnyDSAKey error:(NSError * __autoreleasing *)error SPU_OBJC_DIRECT { // Don't allow removal of (Ed)DSA keys if (hostHasAnyDSAKey && !updateHasAnyDSAKey) { if (error != NULL) { *error = [NSError errorWithDomain:SUSparkleErrorDomain code:SUValidationError userInfo:@{ NSLocalizedDescriptionKey: @"A public (Ed)DSA key was found in the old bundle but no public (Ed)DSA key was found in the new update. Sparkle only supports rotation, but not removal of (Ed)DSA keys. Please add an EdDSA key to the new app." }]; } return NO; } // Don't allow removal of code signing if (hostIsCodeSigned && !updateIsCodeSigned) { if (error != NULL) { if (hostHasAnyDSAKey) { *error = [NSError errorWithDomain:SUSparkleErrorDomain code:SUValidationError userInfo:@{ NSLocalizedDescriptionKey: @"The old bundle is code signed but the update is not code signed. Sparkle only supports rotation, but not removal of Apple Code Signing identity. Please code sign the new app. If no Apple Code Signing certificate is available, adhoc signing can be used at minimum." }]; } else { *error = [NSError errorWithDomain:SUSparkleErrorDomain code:SUValidationError userInfo:@{ NSLocalizedDescriptionKey: @"The old bundle is code signed but the update is not code signed. Please code sign the new app with the same signing identity." }]; } } return NO; } return YES; } /** * If the update is a bundle, then it must meet any one of: * * * old and new Ed(DSA) public keys are the same and valid (it allows change of Code Signing identity), or * * * old and new Code Signing identity are the same and valid * */ - (BOOL)validateUpdateForHost:(SUHost *)host downloadedToPath:(NSString *)downloadedPath newBundleURL:(NSURL *)newBundleURL signatures:(SUSignatures *)signatures error:(NSError * __autoreleasing *)error SPU_OBJC_DIRECT { NSBundle *newBundle = [NSBundle bundleWithURL:newBundleURL]; if (newBundle == nil) { if (error != NULL) { *error = [NSError errorWithDomain:SUSparkleErrorDomain code:SUValidationError userInfo:@{ NSLocalizedDescriptionKey: @"No suitable bundle is found in the update. The update will be rejected." }]; } return NO; } SUPublicKeys *publicKeys = host.publicKeys; SUHost *newHost = [[SUHost alloc] initWithBundle:newBundle]; SUPublicKeys *newPublicKeys = newHost.publicKeys; _verifierInformation.actualVersion = newHost.version; BOOL oldHasAnyDSAKey = NO; BOOL newHasAnyDSAKey = NO; BOOL migratesDSAKeys = NO; BOOL hostIsCodeSigned = NO; BOOL updateIsCodeSigned = NO; [self getHostIsCodeSigned:&hostIsCodeSigned updateIsCodeSigned:&updateIsCodeSigned hostHasAnyDSAKey:&oldHasAnyDSAKey updateHasAnyDSAKey:&newHasAnyDSAKey migratesDSAKeys:&migratesDSAKeys hostPublicKeys:publicKeys updatePublicKeys:newPublicKeys hostBundleURL:host.bundle.bundleURL updateBundleURL:newHost.bundle.bundleURL]; if (![self passesBasicUpdatePolicyWithHostIsCodeSigned:hostIsCodeSigned updateIsCodeSigned:updateIsCodeSigned hostHasAnyDSAKey:oldHasAnyDSAKey updateHasAnyDSAKey:newHasAnyDSAKey error:error]) { return NO; } // Security-critical part starts here BOOL passedDSACheck = NO; BOOL passedCodeSigning = NO; NSError *dsaError = nil; if (oldHasAnyDSAKey) { // it's critical to check against the old public key, rather than the new key passedDSACheck = [SUSignatureVerifier validatePath:downloadedPath withSignatures:signatures withPublicKeys:publicKeys verifierInformation:_verifierInformation error:&dsaError]; } NSError *codeSignedError = nil; if (hostIsCodeSigned) { passedCodeSigning = [SUCodeSigningVerifier codeSignatureIsValidAtBundleURL:newHost.bundle.bundleURL andMatchesSignatureAtBundleURL:host.bundle.bundleURL error:&codeSignedError]; } // If code signing passes, and the new DSA key differs from the old, the check ensures that the app author has correctly used DSA keys for the new update, so the app will be updateable in the next version. // Code signing passing ensures the new DSA key can also be trusted for validating the archive. // If code signing doesn't pass, DSA validation failing will be an error either way. if (!passedDSACheck && newHasAnyDSAKey) { NSError *innerError = nil; if (![SUSignatureVerifier validatePath:downloadedPath withSignatures:signatures withPublicKeys:newPublicKeys verifierInformation:_verifierInformation error:&innerError]) { if (error != NULL) { *error = [NSError errorWithDomain:SUSparkleErrorDomain code:SUValidationError userInfo:@{ NSLocalizedDescriptionKey: @"The update has a public (Ed)DSA key, but the public key shipped with the update doesn't match the signature. To prevent future problems, the update will be rejected.", NSUnderlyingErrorKey: innerError }]; } return NO; } } // End of security-critical part // If the new update is code signed but it's not validly code signed, we reject it NSError *innerError = nil; if (passedDSACheck && updateIsCodeSigned && !passedCodeSigning && ![SUCodeSigningVerifier codeSignatureIsValidAtBundleURL:newHost.bundle.bundleURL error:&innerError]) { if (error != NULL) { *error = [NSError errorWithDomain:SUSparkleErrorDomain code:SUValidationError userInfo:@{ NSLocalizedDescriptionKey: @"The update archive has a valid (Ed)DSA signature, but the app is also signed with Code Signing, which is corrupted. The update will be rejected.", NSUnderlyingErrorKey: innerError }]; } return NO; } // Either DSA must be valid, or Apple Code Signing must be valid. // We allow failure of one of them, because this allows key rotation without breaking chain of trust. if (passedDSACheck || passedCodeSigning) { return YES; } // Now this just explains the failure NSString *dsaStatus; if (migratesDSAKeys) { dsaStatus = @"migrates to new EdDSA keys without keeping the old DSA key for transition"; } else if (newHasAnyDSAKey) { dsaStatus = @"has a new (Ed)DSA key that doesn't match the previous one"; } else if (oldHasAnyDSAKey) { dsaStatus = @"removes the (Ed)DSA key"; } else { dsaStatus = @"isn't signed with an EdDSA key"; } if (!hostIsCodeSigned || !updateIsCodeSigned) { NSString *acsStatus = !hostIsCodeSigned ? @"old app hasn't been signed with app Code Signing" : @"new app isn't signed with app Code Signing"; if (error != NULL) { NSMutableDictionary *userInfo = [NSMutableDictionary dictionary]; userInfo[NSLocalizedDescriptionKey] = [NSString stringWithFormat:@"The update archive %@, and the %@. At least one method of signature verification must be valid. The update will be rejected.", dsaStatus, acsStatus]; if (dsaError != nil) { userInfo[NSUnderlyingErrorKey] = dsaError; } *error = [NSError errorWithDomain:SUSparkleErrorDomain code:SUValidationError userInfo:[userInfo copy]]; } } else { if (error != NULL) { NSMutableDictionary *userInfo = [NSMutableDictionary dictionary]; userInfo[NSLocalizedDescriptionKey] = [NSString stringWithFormat:@"The update archive %@, and the app is signed with a new Code Signing identity that doesn't match code signing of the original app. At least one method of signature verification must be valid. The update will be rejected.", dsaStatus]; if (codeSignedError != nil) { userInfo[NSUnderlyingErrorKey] = codeSignedError; } *error = [NSError errorWithDomain:SUSparkleErrorDomain code:SUValidationError userInfo:[userInfo copy]]; } } return NO; } @end ================================================ FILE: Sparkle/SUUpdater.h ================================================ // // SUUpdater.h // Sparkle // // Created by Andy Matuschak on 1/4/06. // Copyright 2006 Andy Matuschak. All rights reserved. // #ifndef SUUPDATER_H #define SUUPDATER_H #import <Foundation/Foundation.h> #if defined(BUILDING_SPARKLE_SOURCES_EXTERNALLY) // Ignore incorrect warning #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wquoted-include-in-framework-header" #import "SUExport.h" #import "SUVersionComparisonProtocol.h" #import "SUVersionDisplayProtocol.h" #import "SUUpdaterDelegate.h" #pragma clang diagnostic pop #else #import <Sparkle/SUExport.h> #import <Sparkle/SUVersionComparisonProtocol.h> #import <Sparkle/SUVersionDisplayProtocol.h> #import <Sparkle/SUUpdaterDelegate.h> #endif @class SUAppcastItem, SUAppcast, NSMenuItem; @protocol SUUpdaterDelegate; /** The legacy API in Sparkle for controlling the update mechanism. This class is now deprecated and acts as a thin wrapper around `SPUUpdater` and `SPUStandardUserDriver`. If you are migrating to Sparkle 2, use `SPUStandardUpdaterController` instead, or `SPUUpdater` if you need more control. */ __deprecated_msg("Deprecated in Sparkle 2. Use SPUStandardUpdaterController instead, or SPUUpdater if you need more control.") SU_EXPORT @interface SUUpdater : NSObject @property (unsafe_unretained, nonatomic) IBOutlet id<SUUpdaterDelegate> delegate; /*! The shared updater for the main bundle. This is equivalent to passing [NSBundle mainBundle] to SUUpdater::updaterForBundle: */ + (SUUpdater *)sharedUpdater; /*! The shared updater for a specified bundle. If an updater has already been initialized for the provided bundle, that shared instance will be returned. */ + (SUUpdater *)updaterForBundle:(NSBundle *)bundle; /*! Designated initializer for SUUpdater. If an updater has already been initialized for the provided bundle, that shared instance will be returned. */ - (instancetype)initForBundle:(NSBundle *)bundle; /*! Explicitly checks for updates and displays a progress dialog while doing so. This method is meant for a main menu item. Connect any menu item to this action in Interface Builder, and Sparkle will check for updates and report back its findings verbosely when it is invoked. This will find updates that the user has opted into skipping. */ - (IBAction)checkForUpdates:(id)sender; /*! The menu item validation used for the -checkForUpdates: action */ - (BOOL)validateMenuItem:(NSMenuItem *)menuItem; /*! Checks for updates, but does not display any UI unless an update is found. This is meant for programmatically initiating a check for updates. That is, it will display no UI unless it actually finds an update, in which case it proceeds as usual. If automatic downloading of updates it turned on and allowed, however, this will invoke that behavior, and if an update is found, it will be downloaded in the background silently and will be prepped for installation. This will not find updates that the user has opted into skipping. */ - (void)checkForUpdatesInBackground; /*! A property indicating whether or not to check for updates automatically. Setting this property will persist in the host bundle's user defaults. The update schedule cycle will be reset in a short delay after the property's new value is set. This is to allow reverting this property without kicking off a schedule change immediately */ @property (nonatomic) BOOL automaticallyChecksForUpdates; /*! A property indicating whether or not updates can be automatically downloaded in the background. Note that automatic downloading of updates can be disallowed by the developer. In this case, -automaticallyDownloadsUpdates will return NO regardless of how this property is set. Setting this property will persist in the host bundle's user defaults. */ @property (nonatomic) BOOL automaticallyDownloadsUpdates; /*! A property indicating the current automatic update check interval. Setting this property will persist in the host bundle's user defaults. The update schedule cycle will be reset in a short delay after the property's new value is set. This is to allow reverting this property without kicking off a schedule change immediately */ @property (nonatomic) NSTimeInterval updateCheckInterval; /*! Begins a "probing" check for updates which will not actually offer to update to that version. However, the delegate methods SUUpdaterDelegate::updater:didFindValidUpdate: and SUUpdaterDelegate::updaterDidNotFindUpdate: will be called, so you can use that information in your UI. Updates that have been skipped by the user will not be found. */ - (void)checkForUpdateInformation; /*! The URL of the appcast used to download update information. Setting this property will persist in the host bundle's user defaults. If you don't want persistence, you may want to consider instead implementing SUUpdaterDelegate::feedURLStringForUpdater: or SUUpdaterDelegate::feedParametersForUpdater:sendingSystemProfile: This property must be called on the main thread. */ @property (nonatomic, copy) NSURL *feedURL; /*! The host bundle that is being updated. */ @property (readonly, nonatomic) NSBundle *hostBundle; /*! The bundle this class (SUUpdater) is loaded into. */ @property (nonatomic, readonly) NSBundle *sparkleBundle; /*! The user agent used when checking for and downloading updates. The default implementation can be overridden. */ @property (nonatomic, copy) NSString *userAgentString; /*! The HTTP headers used when checking for and downloading updates. The keys of this dictionary are HTTP header fields (NSString) and values are corresponding values (NSString) */ @property (copy, nonatomic) NSDictionary<NSString *, NSString *> *httpHeaders; /*! A property indicating whether or not the user's system profile information is sent when checking for updates. Setting this property will persist in the host bundle's user defaults. */ @property (nonatomic) BOOL sendsSystemProfile; /*! A property indicating the decryption password used for extracting updates shipped as Apple Disk Images (dmg) */ @property (nonatomic, copy) NSString *decryptionPassword; /*! Returns the date of last update check. \returns \c nil if no check has been performed. */ @property (nonatomic, readonly, copy) NSDate *lastUpdateCheckDate; /*! Appropriately schedules or cancels the update checking timer according to the preferences for time interval and automatic checks. This call does not change the date of the next check, but only the internal NSTimer. */ - (void)resetUpdateCycle; /*! A property indicating whether or not an update is in progress. Note this property is not indicative of whether or not user initiated updates can be performed. Use SUUpdater::validateMenuItem: for that instead. */ @property (nonatomic, readonly) BOOL updateInProgress; @end #endif ================================================ FILE: Sparkle/SUUpdater.m ================================================ // // SUUpdater.m // Sparkle // // Created by Andy Matuschak on 1/4/06. // Copyright 2006 Andy Matuschak. All rights reserved. // #if SPARKLE_BUILD_UI_BITS && SPARKLE_BUILD_LEGACY_SUUPDATER #import "SUUpdater.h" #import "SPUUpdater.h" #import "SPUStandardUserDriver.h" #import "SPUStandardUserDriverDelegate.h" #import "SPUUpdaterDelegate.h" #import "SULog.h" #import <AppKit/AppKit.h> @interface SUUpdater () <SPUUpdaterDelegate, SPUStandardUserDriverDelegate> @end #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdeprecated-implementations" @implementation SUUpdater #pragma clang diagnostic pop { SPUUpdater *_updater; SPUStandardUserDriver *_userDriver; void(^_postponedInstallHandler)(void); void(^_silentInstallHandler)(void); BOOL _delayShowingUserUpdate; BOOL _loggedInstallUpdatesIfAvailableWarning; } @synthesize delegate = _delegate; @synthesize decryptionPassword = _decryptionPassword; static NSMutableDictionary *sharedUpdaters = nil; + (SUUpdater *)sharedUpdater { return [self updaterForBundle:[NSBundle mainBundle]]; } // SUUpdater has a singleton for each bundle. We use the fact that NSBundle instances are also singletons, so we can use them as keys. If you don't trust that you can also use the identifier as key + (SUUpdater *)updaterForBundle:(NSBundle *)bundle { if (bundle == nil) bundle = [NSBundle mainBundle]; id updater = [sharedUpdaters objectForKey:[NSValue valueWithNonretainedObject:bundle]]; if (updater == nil) { updater = [(SUUpdater *)[[self class] alloc] initForBundle:bundle]; } return updater; } // This is the designated initializer for SUUpdater, important for subclasses - (instancetype)initForBundle:(NSBundle *)bundle { self = [super init]; if (bundle == nil) bundle = [NSBundle mainBundle]; id updater = [sharedUpdaters objectForKey:[NSValue valueWithNonretainedObject:bundle]]; if (updater) { self = updater; } else if (self) { if (sharedUpdaters == nil) { sharedUpdaters = [[NSMutableDictionary alloc] init]; } [sharedUpdaters setObject:self forKey:[NSValue valueWithNonretainedObject:bundle]]; // This bundle may not necessarily be the correct application bundle // Unfortunately we won't know the correct application bundle until after the delegate is set // See -[SUUpdater _standardUserDriverRequestsPathToRelaunch] and -[SUUpdater _pathToRelaunchForUpdater:] implemented below which resolves this _userDriver = [[SPUStandardUserDriver alloc] initWithHostBundle:bundle delegate:self]; _updater = [[SPUUpdater alloc] initWithHostBundle:bundle applicationBundle:bundle userDriver:_userDriver delegate:self]; NSError *updaterError = nil; if (![_updater startUpdater:&updaterError]) { SULog(SULogLevelError, @"Error: Failed to start updater with error: %@", updaterError); } } return self; } // This will be used when the updater is instantiated in a nib such as MainMenu - (instancetype)init { return [self initForBundle:[NSBundle mainBundle]]; } - (void)resetUpdateCycle { [_updater resetUpdateCycle]; } - (NSBundle *)hostBundle { return _updater.hostBundle; } - (NSBundle *)sparkleBundle { // Use explicit class to use the correct bundle even when subclassed return [NSBundle bundleForClass:[SUUpdater class]]; } - (BOOL)automaticallyChecksForUpdates { return _updater.automaticallyChecksForUpdates; } - (void)setAutomaticallyChecksForUpdates:(BOOL)automaticallyChecksForUpdates { [_updater setAutomaticallyChecksForUpdates:automaticallyChecksForUpdates]; } - (NSTimeInterval)updateCheckInterval { return _updater.updateCheckInterval; } - (void)setUpdateCheckInterval:(NSTimeInterval)updateCheckInterval { [_updater setUpdateCheckInterval:updateCheckInterval]; } - (NSURL *)feedURL { return _updater.feedURL; } - (void)setFeedURL:(NSURL *)feedURL { [_updater setFeedURL:feedURL]; } - (NSString *)userAgentString { return _updater.userAgentString; } - (void)setUserAgentString:(NSString *)userAgentString { [_updater setUserAgentString:userAgentString]; } - (NSDictionary *)httpHeaders { return _updater.httpHeaders; } - (void)setHttpHeaders:(NSDictionary *)httpHeaders { [_updater setHttpHeaders:httpHeaders]; } - (BOOL)sendsSystemProfile { return _updater.sendsSystemProfile; } - (void)setSendsSystemProfile:(BOOL)sendsSystemProfile { [_updater setSendsSystemProfile:sendsSystemProfile]; } - (BOOL)automaticallyDownloadsUpdates { return _updater.automaticallyDownloadsUpdates; } - (void)setAutomaticallyDownloadsUpdates:(BOOL)automaticallyDownloadsUpdates { [_updater setAutomaticallyDownloadsUpdates:automaticallyDownloadsUpdates]; } - (IBAction)checkForUpdates:(id)__unused sender { [_updater checkForUpdates]; } - (BOOL)validateMenuItem:(NSMenuItem *)item { if ([item action] == @selector(checkForUpdates:)) { return _updater.canCheckForUpdates; } return YES; } - (void)checkForUpdatesInBackground { if (_delayShowingUserUpdate) { // We don't know if SUUpdater delegate will call checkForUpdates: or checkForUpdatesInBackground // to bring a deferred update alert back in 1.x. // So if checkForUpdatesInBackground is called we will bring the update back in focus [self checkForUpdates:nil]; } else { [_updater checkForUpdatesInBackground]; } } - (NSDate *)lastUpdateCheckDate { return _updater.lastUpdateCheckDate; } - (void)checkForUpdateInformation { [_updater checkForUpdateInformation]; } - (BOOL)updateInProgress { // This is not quite true -- we may be able to check / resume an update if one is in progress // But this is a close enough approximation for 1.x updater API return _updater.sessionInProgress; } // Not implemented properly at the moment - leaning towards it not be in the future // because it may be hard to implement properly (without passing a boolean flag everywhere), or // it would require us to maintain support for an additional class used by a very few people thus far // For now, just invoke the regular background update process if this is invoked. Could change our minds on this later. - (void)installUpdatesIfAvailable { if (!_loggedInstallUpdatesIfAvailableWarning) { SULog(SULogLevelError, @"-[%@ installUpdatesIfAvailable] does not function anymore.. Instead a background scheduled update check will be done.", NSStringFromClass([self class])); _loggedInstallUpdatesIfAvailableWarning = YES; } [self checkForUpdatesInBackground]; } - (void)standardUserDriverWillShowModalAlert { if ([_delegate respondsToSelector:@selector(updaterWillShowModalAlert:)]) { [_delegate updaterWillShowModalAlert:self]; } } - (void)standardUserDriverDidShowModalAlert { if ([_delegate respondsToSelector:@selector(updaterDidShowModalAlert:)]) { [_delegate updaterDidShowModalAlert:self]; } } - (_Nullable id <SUVersionDisplay>)standardUserDriverRequestsVersionDisplayer { id <SUVersionDisplay> versionDisplayer = nil; if ([_delegate respondsToSelector:@selector(versionDisplayerForUpdater:)]) { versionDisplayer = [_delegate versionDisplayerForUpdater:self]; } return versionDisplayer; } - (BOOL)updater:(SPUUpdater *)__unused updater mayPerformUpdateCheck:(SPUUpdateCheck)__unused updateCheck error:(NSError *__autoreleasing _Nullable *)error { BOOL updaterMayCheck = YES; if ([_delegate respondsToSelector:@selector(updaterMayCheckForUpdates:)]) { updaterMayCheck = [_delegate updaterMayCheckForUpdates:self]; } return updaterMayCheck; } - (NSArray *)feedParametersForUpdater:(SPUUpdater *)__unused updater sendingSystemProfile:(BOOL)sendingProfile { NSArray *feedParameters; if ([_delegate respondsToSelector:@selector(feedParametersForUpdater:sendingSystemProfile:)]) { feedParameters = [_delegate feedParametersForUpdater:self sendingSystemProfile:sendingProfile]; } else { feedParameters = [NSArray array]; } return feedParameters; } - (NSString *)feedURLStringForUpdater:(SPUUpdater *)__unused updater { // Be really careful not to call [self feedURL] here. That might lead us into infinite recursion. NSString *feedURL = nil; if ([_delegate respondsToSelector:@selector(feedURLStringForUpdater:)]) { feedURL = [_delegate feedURLStringForUpdater:self]; } return feedURL; } - (BOOL)updaterShouldPromptForPermissionToCheckForUpdates:(SPUUpdater *)__unused updater { BOOL shouldPrompt = YES; if ([_delegate respondsToSelector:@selector(updaterShouldPromptForPermissionToCheckForUpdates:)]) { shouldPrompt = [_delegate updaterShouldPromptForPermissionToCheckForUpdates:self]; } return shouldPrompt; } - (void)updater:(SPUUpdater *)__unused updater didFinishLoadingAppcast:(SUAppcast *)appcast { if ([_delegate respondsToSelector:@selector(updater:didFinishLoadingAppcast:)]) { [_delegate updater:self didFinishLoadingAppcast:appcast]; } } - (SUAppcastItem *)bestValidUpdateInAppcast:(SUAppcast *)appcast forUpdater:(SPUUpdater *)__unused updater { SUAppcastItem *bestValidUpdate = nil; if ([_delegate respondsToSelector:@selector(bestValidUpdateInAppcast:forUpdater:)]) { bestValidUpdate = [_delegate bestValidUpdateInAppcast:appcast forUpdater:self]; } return bestValidUpdate; } - (void)updater:(SPUUpdater *)__unused updater didFindValidUpdate:(SUAppcastItem *)item { if ([_delegate respondsToSelector:@selector(updater:didFindValidUpdate:)]) { [_delegate updater:self didFindValidUpdate:item]; } } - (void)updaterDidNotFindUpdate:(SPUUpdater *)__unused updater { if ([_delegate respondsToSelector:@selector(updaterDidNotFindUpdate:)]) { [_delegate updaterDidNotFindUpdate:self]; } } - (void)updater:(SPUUpdater *)__unused updater userDidMakeChoice:(SPUUserUpdateChoice)choice forUpdate:(SUAppcastItem *)updateItem state:(SPUUserUpdateState *)__unused state { // This delegate callback matches 1.x behavior (even though -standardUserDriverWillFinishUpdateSession might be a better place for it) if ([_delegate respondsToSelector:@selector(updater:didDismissUpdateAlertPermanently:forItem:)]) { [_delegate updater:self didDismissUpdateAlertPermanently:(choice == SPUUserUpdateChoiceSkip) forItem:updateItem]; } if (choice == SPUUserUpdateChoiceSkip && [_delegate respondsToSelector:@selector(updater:userDidSkipThisVersion:)]) { [_delegate updater:self userDidSkipThisVersion:updateItem]; } } - (BOOL)standardUserDriverShouldHandleShowingScheduledUpdate:(SUAppcastItem *)update andInImmediateFocus:(BOOL)immediateFocus { if ([_delegate respondsToSelector:@selector(updaterShouldShowUpdateAlertForScheduledUpdate:forItem:)]) { // If the delegate returns NO and tries to show the update before // -standardUserDriverWillHandleShowingUpdate:forUpdate:state: is called, this is technically // a violation. However it is also unlikely to happen. return [_delegate updaterShouldShowUpdateAlertForScheduledUpdate:self forItem:update]; } else { return YES; } } - (void)standardUserDriverWillHandleShowingUpdate:(BOOL)handleShowingUpdate forUpdate:(SUAppcastItem *)update state:(SPUUserUpdateState *)state { if (!handleShowingUpdate) { _delayShowingUserUpdate = YES; } } - (void)standardUserDriverWillFinishUpdateSession { _delayShowingUserUpdate = NO; } - (void)updater:(SPUUpdater *)__unused updater willDownloadUpdate:(SUAppcastItem *)item withRequest:(NSMutableURLRequest *)request { if ([_delegate respondsToSelector:@selector(updater:willDownloadUpdate:withRequest:)]) { [_delegate updater:self willDownloadUpdate:item withRequest:request]; } } - (void)updater:(SPUUpdater *)__unused updater didDownloadUpdate:(SUAppcastItem *)item { if ([_delegate respondsToSelector:@selector(updater:didDownloadUpdate:)]) { [_delegate updater:self didDownloadUpdate:item]; } } - (void)updater:(SPUUpdater *)__unused updater failedToDownloadUpdate:(SUAppcastItem *)item error:(NSError *)error { if ([_delegate respondsToSelector:@selector(updater:failedToDownloadUpdate:error:)]) { [_delegate updater:self failedToDownloadUpdate:item error:error]; } } - (void)userDidCancelDownload:(SPUUpdater *)__unused updater { if ([_delegate respondsToSelector:@selector(userDidCancelDownload:)]) { [_delegate userDidCancelDownload:self]; } } - (void)updater:(SPUUpdater *)updater willExtractUpdate:(SUAppcastItem *)item { if ([_delegate respondsToSelector:@selector(updater:willExtractUpdate:)]) { [_delegate updater:self willExtractUpdate:item]; } } - (void)updater:(SPUUpdater *)updater didExtractUpdate:(SUAppcastItem *)item { if ([_delegate respondsToSelector:@selector(updater:didExtractUpdate:)]) { [_delegate updater:self didExtractUpdate:item]; } } - (void)updater:(SPUUpdater *)__unused updater willInstallUpdate:(SUAppcastItem *)item { if ([_delegate respondsToSelector:@selector(updater:willInstallUpdate:)]) { [_delegate updater:self willInstallUpdate:item]; } } - (void)installPostponedUpdate { if (_postponedInstallHandler != nil) { _postponedInstallHandler(); _postponedInstallHandler = nil; } } - (BOOL)updater:(SPUUpdater *)__unused updater shouldPostponeRelaunchForUpdate:(SUAppcastItem *)item untilInvokingBlock:(void (^)(void))installHandler { BOOL shouldPostponeRelaunch = NO; if ([_delegate respondsToSelector:@selector(updater:shouldPostponeRelaunchForUpdate:untilInvoking:)]) { NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:[[self class] instanceMethodSignatureForSelector:@selector(installPostponedUpdate)]]; [invocation setSelector:@selector(installPostponedUpdate)]; // This invocation will retain self, but this instance is kept alive forever by our singleton pattern anyway [invocation setTarget:self]; _postponedInstallHandler = installHandler; shouldPostponeRelaunch = [_delegate updater:self shouldPostponeRelaunchForUpdate:item untilInvoking:invocation]; } else if ([_delegate respondsToSelector:@selector(updater:shouldPostponeRelaunchForUpdate:)]) { // This API should really take a block, but not fixing a 1.x mishap now shouldPostponeRelaunch = [_delegate updater:self shouldPostponeRelaunchForUpdate:item]; } return shouldPostponeRelaunch; } - (BOOL)updaterShouldRelaunchApplication:(SPUUpdater *)__unused updater { BOOL shouldRestart = YES; if ([_delegate respondsToSelector:@selector(updaterShouldRelaunchApplication:)]) { shouldRestart = [_delegate updaterShouldRelaunchApplication:self]; } return shouldRestart; } - (void)updaterWillRelaunchApplication:(SPUUpdater *)__unused updater { if ([_delegate respondsToSelector:@selector(updaterWillRelaunchApplication:)]) { [_delegate updaterWillRelaunchApplication:self]; } } #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdeprecated-implementations" - (id<SUVersionComparison>)versionComparatorForUpdater:(SPUUpdater *)__unused updater { id<SUVersionComparison> versionComparator; if ([_delegate respondsToSelector:@selector(versionComparatorForUpdater:)]) { versionComparator = [_delegate versionComparatorForUpdater:self]; } return versionComparator; } #pragma clang diagnostic pop // Private SPUUpdater API that allows us to defer providing an application path to relaunch - (NSString * _Nullable)_pathToRelaunchForUpdater:(SPUUpdater *)__unused updater { NSString *relaunchPath = nil; if ([_delegate respondsToSelector:@selector(pathToRelaunchForUpdater:)]) { relaunchPath = [_delegate pathToRelaunchForUpdater:self]; } return relaunchPath; } - (NSString *)decryptionPasswordForUpdater:(SPUUpdater *)__unused updater { return _decryptionPassword; } - (void)finishSilentInstallation { if (_silentInstallHandler != nil) { _silentInstallHandler(); _silentInstallHandler = nil; } } - (BOOL)updater:(SPUUpdater *)__unused updater willInstallUpdateOnQuit:(SUAppcastItem *)item immediateInstallationBlock:(void (^)(void))immediateInstallHandler { BOOL installationHandledByDelegate = NO; if ([_delegate respondsToSelector:@selector((updater:willInstallUpdateOnQuit:immediateInstallationBlock:))]) { [_delegate updater:self willInstallUpdateOnQuit:item immediateInstallationBlock:immediateInstallHandler]; // We have to assume they will handle the installation since they implement this method // Not ideal, but this is why this delegate callback is deprecated installationHandledByDelegate = YES; } else if ([_delegate respondsToSelector:@selector(updater:willInstallUpdateOnQuit:immediateInstallationInvocation:)]) { NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:[[self class] instanceMethodSignatureForSelector:@selector(finishSilentInstallation)]]; // This invocation will retain self, but this instance is kept alive forever by our singleton pattern anyway [invocation setTarget:self]; _silentInstallHandler = immediateInstallHandler; [_delegate updater:self willInstallUpdateOnQuit:item immediateInstallationInvocation:invocation]; // We have to assume they will handle the installation since they implement this method // Not ideal, but this is why this delegate callback is deprecated installationHandledByDelegate = YES; } return installationHandledByDelegate; } - (void)updater:(SPUUpdater *)__unused updater didAbortWithError:(NSError *)error { if ([_delegate respondsToSelector:@selector(updater:didAbortWithError:)]) { [_delegate updater:self didAbortWithError:error]; } } @end #endif ================================================ FILE: Sparkle/SUUpdaterDelegate.h ================================================ // // SUUpdaterDelegate.h // Sparkle // // Created by Mayur Pawashe on 3/12/16. // Copyright © 2016 Sparkle Project. All rights reserved. // #import <Foundation/Foundation.h> #if defined(BUILDING_SPARKLE_SOURCES_EXTERNALLY) // Ignore incorrect warning #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wquoted-include-in-framework-header" #import "SUExport.h" #pragma clang diagnostic pop #else #import <Sparkle/SUExport.h> #endif @protocol SUVersionComparison, SUVersionDisplay; @class SUUpdater, SUAppcast, SUAppcastItem; NS_ASSUME_NONNULL_BEGIN // ----------------------------------------------------------------------------- // SUUpdater Notifications for events that might be interesting to more than just the delegate // The updater will be the notification object // ----------------------------------------------------------------------------- SU_EXPORT extern NSString *const SUUpdaterDidFinishLoadingAppCastNotification; SU_EXPORT extern NSString *const SUUpdaterDidFindValidUpdateNotification; SU_EXPORT extern NSString *const SUUpdaterDidNotFindUpdateNotification; SU_EXPORT extern NSString *const SUUpdaterWillRestartNotification; #define SUUpdaterWillRelaunchApplicationNotification SUUpdaterWillRestartNotification; #define SUUpdaterWillInstallUpdateNotification SUUpdaterWillRestartNotification; // Key for the SUAppcastItem object in the SUUpdaterDidFindValidUpdateNotification userInfo SU_EXPORT extern NSString *const SUUpdaterAppcastItemNotificationKey; // Key for the SUAppcast object in the SUUpdaterDidFinishLoadingAppCastNotification userInfo SU_EXPORT extern NSString *const SUUpdaterAppcastNotificationKey; // ----------------------------------------------------------------------------- // SUUpdater Delegate: // ----------------------------------------------------------------------------- /*! Provides methods to control the behavior of an SUUpdater object. */ __deprecated_msg("Deprecated in Sparkle 2. See SPUUpdaterDelegate instead") @protocol SUUpdaterDelegate <NSObject> @optional /*! Returns whether to allow Sparkle to pop up. For example, this may be used to prevent Sparkle from interrupting a setup assistant. \param updater The SUUpdater instance. */ - (BOOL)updaterMayCheckForUpdates:(SUUpdater *)updater; /*! Returns additional parameters to append to the appcast URL's query string. This is potentially based on whether or not Sparkle will also be sending along the system profile. \param updater The SUUpdater instance. \param sendingProfile Whether the system profile will also be sent. \return An array of dictionaries with keys: "key", "value", "displayKey", "displayValue", the latter two being specifically for display to the user. */ - (NSArray<NSDictionary<NSString *, NSString *> *> *)feedParametersForUpdater:(SUUpdater *)updater sendingSystemProfile:(BOOL)sendingProfile; /*! Returns a custom appcast URL. Override this to dynamically specify the entire URL. An alternative may be to use SUUpdaterDelegate::feedParametersForUpdater:sendingSystemProfile: and let the server handle what kind of feed to provide. \param updater The SUUpdater instance. */ - (nullable NSString *)feedURLStringForUpdater:(SUUpdater *)updater; /*! Returns whether Sparkle should prompt the user about automatic update checks. Use this to override the default behavior. \param updater The SUUpdater instance. */ - (BOOL)updaterShouldPromptForPermissionToCheckForUpdates:(SUUpdater *)updater; /*! Called after Sparkle has downloaded the appcast from the remote server. Implement this if you want to do some special handling with the appcast once it finishes loading. \param updater The SUUpdater instance. \param appcast The appcast that was downloaded from the remote server. */ - (void)updater:(SUUpdater *)updater didFinishLoadingAppcast:(SUAppcast *)appcast; /*! Returns the item in the appcast corresponding to the update that should be installed. If you're using special logic or extensions in your appcast, implement this to use your own logic for finding a valid update, if any, in the given appcast. \param appcast The appcast that was downloaded from the remote server. \param updater The SUUpdater instance. */ - (nullable SUAppcastItem *)bestValidUpdateInAppcast:(SUAppcast *)appcast forUpdater:(SUUpdater *)updater; /*! Called when a valid update is found by the update driver. \param updater The SUUpdater instance. \param item The appcast item corresponding to the update that is proposed to be installed. */ - (void)updater:(SUUpdater *)updater didFindValidUpdate:(SUAppcastItem *)item; /*! Called when a valid update is not found. \param updater The SUUpdater instance. */ - (void)updaterDidNotFindUpdate:(SUUpdater *)updater; /*! Called just before the scheduled update driver prompts the user to install an update. \param updater The SUUpdater instance. \return YES to allow the update prompt to be shown (the default behavior), or NO to suppress it. */ - (BOOL)updaterShouldShowUpdateAlertForScheduledUpdate:(SUUpdater *)updater forItem:(SUAppcastItem *)item; /*! Called after the user dismisses the update alert. \param updater The SUUpdater instance. \param permanently YES if the alert will not appear again for this update; NO if it may reappear. */ - (void)updater:(SUUpdater *)updater didDismissUpdateAlertPermanently:(BOOL)permanently forItem:(SUAppcastItem *)item; /*! Called immediately before downloading the specified update. \param updater The SUUpdater instance. \param item The appcast item corresponding to the update that is proposed to be downloaded. \param request The mutable URL request that will be used to download the update. */ - (void)updater:(SUUpdater *)updater willDownloadUpdate:(SUAppcastItem *)item withRequest:(NSMutableURLRequest *)request; /*! Called immediately after successful download of the specified update. \param updater The SUUpdater instance. \param item The appcast item corresponding to the update that has been downloaded. */ - (void)updater:(SUUpdater *)updater didDownloadUpdate:(SUAppcastItem *)item; /*! Called after the specified update failed to download. \param updater The SUUpdater instance. \param item The appcast item corresponding to the update that failed to download. \param error The error generated by the failed download. */ - (void)updater:(SUUpdater *)updater failedToDownloadUpdate:(SUAppcastItem *)item error:(NSError *)error; /*! Called when the user clicks the cancel button while and update is being downloaded. \param updater The SUUpdater instance. */ - (void)userDidCancelDownload:(SUUpdater *)updater; /*! Called immediately before extracting the specified downloaded update. \param updater The SUUpdater instance. \param item The appcast item corresponding to the update that is proposed to be extracted. */ - (void)updater:(SUUpdater *)updater willExtractUpdate:(SUAppcastItem *)item; /*! Called immediately after extracting the specified downloaded update. \param updater The SUUpdater instance. \param item The appcast item corresponding to the update that has been extracted. */ - (void)updater:(SUUpdater *)updater didExtractUpdate:(SUAppcastItem *)item; /*! Called immediately before installing the specified update. \param updater The SUUpdater instance. \param item The appcast item corresponding to the update that is proposed to be installed. */ - (void)updater:(SUUpdater *)updater willInstallUpdate:(SUAppcastItem *)item; /*! Called when an update is skipped by the user. \param updater The updater instance. \param item The appcast item corresponding to the update that the user skipped. */ - (void)updater:(SUUpdater *)updater userDidSkipThisVersion:(SUAppcastItem *)item; /*! Returns whether the relaunch should be delayed in order to perform other tasks. This is not called if the user didn't relaunch on the previous update, in that case it will immediately restart. This may also not be called if the application is not going to relaunch after it terminates. \param updater The SUUpdater instance. \param item The appcast item corresponding to the update that is proposed to be installed. \param invocation The invocation that must be completed with `[invocation invoke]` before continuing with the relaunch. \return \c YES to delay the relaunch until \p invocation is invoked. */ - (BOOL)updater:(SUUpdater *)updater shouldPostponeRelaunchForUpdate:(SUAppcastItem *)item untilInvoking:(NSInvocation *)invocation; /*! Returns whether the relaunch should be delayed in order to perform other tasks. This is not called if the user didn't relaunch on the previous update, in that case it will immediately restart. This method acts as a simpler alternative to SUUpdaterDelegate::updater:shouldPostponeRelaunchForUpdate:untilInvoking: avoiding usage of NSInvocation, which is not available in Swift environments. \param updater The SUUpdater instance. \param item The appcast item corresponding to the update that is proposed to be installed. \return \c YES to delay the relaunch. */ - (BOOL)updater:(SUUpdater *)updater shouldPostponeRelaunchForUpdate:(SUAppcastItem *)item; /*! Returns whether the application should be relaunched at all. Some apps \b cannot be relaunched under certain circumstances. This method can be used to explicitly prevent a relaunch. \param updater The SUUpdater instance. */ - (BOOL)updaterShouldRelaunchApplication:(SUUpdater *)updater; /*! Called immediately before relaunching. \param updater The SUUpdater instance. */ - (void)updaterWillRelaunchApplication:(SUUpdater *)updater; /*! Called immediately after relaunching. SUUpdater delegate must be set before applicationDidFinishLaunching: to catch this event. \param updater The SUUpdater instance. */ - (void)updaterDidRelaunchApplication:(SUUpdater *)updater; /*! Returns an object that compares version numbers to determine their arithmetic relation to each other. This method allows you to provide a custom version comparator. If you don't implement this method or return \c nil, the standard version comparator will be used. Note that the standard version comparator may be used during installation for preventing a downgrade, even if you provide a custom comparator here. \sa SUStandardVersionComparator \param updater The SUUpdater instance. */ - (nullable id<SUVersionComparison>)versionComparatorForUpdater:(SUUpdater *)updater; /*! Returns an object that formats version numbers for display to the user. If you don't implement this method or return \c nil, the standard version formatter will be used. \sa SUUpdateAlert \param updater The SUUpdater instance. */ - (nullable id <SUVersionDisplay>)versionDisplayerForUpdater:(SUUpdater *)updater; /*! Returns the path to the application which is used to relaunch after the update is installed. The installer also waits for the termination of the application at this path. The default is the path of the host bundle. \param updater The SUUpdater instance. */ - (nullable NSString *)pathToRelaunchForUpdater:(SUUpdater *)updater; /*! Called before an updater shows a modal alert window, to give the host the opportunity to hide attached windows that may get in the way. \param updater The SUUpdater instance. */ - (void)updaterWillShowModalAlert:(SUUpdater *)updater; /*! Called after an updater shows a modal alert window, to give the host the opportunity to hide attached windows that may get in the way. \param updater The SUUpdater instance. */ - (void)updaterDidShowModalAlert:(SUUpdater *)updater; /*! Called when an update is scheduled to be silently installed on quit. This is after an update has been automatically downloaded in the background. (i.e. SUUpdater::automaticallyDownloadsUpdates is YES) \param updater The SUUpdater instance. \param item The appcast item corresponding to the update that is proposed to be installed. \param invocation Can be used to trigger an immediate silent install and relaunch. */ - (void)updater:(SUUpdater *)updater willInstallUpdateOnQuit:(SUAppcastItem *)item immediateInstallationInvocation:(NSInvocation *)invocation; /*! Called when an update is scheduled to be silently installed on quit. This is after an update has been automatically downloaded in the background. (i.e. SUUpdater::automaticallyDownloadsUpdates is YES) This method acts as a more modern alternative to SUUpdaterDelegate::updater:willInstallUpdateOnQuit:immediateInstallationInvocation: using a block instead of NSInvocation, which is not available in Swift environments. \param updater The SUUpdater instance. \param item The appcast item corresponding to the update that is proposed to be installed. \param installationBlock Can be used to trigger an immediate silent install and relaunch. */ - (void)updater:(SUUpdater *)updater willInstallUpdateOnQuit:(SUAppcastItem *)item immediateInstallationBlock:(void (^)(void))installationBlock; /*! Calls after an update that was scheduled to be silently installed on quit has been canceled. \param updater The SUUpdater instance. \param item The appcast item corresponding to the update that was proposed to be installed. \deprecated This method is no longer invoked. The installer will try to its best ability to install the update. */ - (void)updater:(SUUpdater *)updater didCancelInstallUpdateOnQuit:(SUAppcastItem *)item __deprecated; /*! 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 NS_ASSUME_NONNULL_END ================================================ FILE: Sparkle/SUVersionComparisonProtocol.h ================================================ // // SUVersionComparisonProtocol.h // Sparkle // // Created by Andy Matuschak on 12/21/07. // Copyright 2007 Andy Matuschak. All rights reserved. // #ifndef SUVERSIONCOMPARISONPROTOCOL_H #define SUVERSIONCOMPARISONPROTOCOL_H #import <Foundation/Foundation.h> #if defined(BUILDING_SPARKLE_SOURCES_EXTERNALLY) // Ignore incorrect warning #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wquoted-include-in-framework-header" #import "SUExport.h" #pragma clang diagnostic pop #else #import <Sparkle/SUExport.h> #endif NS_ASSUME_NONNULL_BEGIN /** Provides version comparison facilities for Sparkle. */ @protocol SUVersionComparison /** An abstract method to compare two version strings. Should return NSOrderedAscending if b > a, NSOrderedDescending if b < a, and NSOrderedSame if they are equivalent. */ - (NSComparisonResult)compareVersion:(NSString *)versionA toVersion:(NSString *)versionB; // *** MAY BE CALLED ON NON-MAIN THREAD! @end NS_ASSUME_NONNULL_END #endif ================================================ FILE: Sparkle/SUVersionDisplayProtocol.h ================================================ // // SUVersionDisplayProtocol.h // EyeTV // // Created by Uli Kusterer on 08.12.09. // Copyright 2009 Elgato Systems GmbH. All rights reserved. // #import <Foundation/Foundation.h> #if defined(BUILDING_SPARKLE_SOURCES_EXTERNALLY) // Ignore incorrect warning #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wquoted-include-in-framework-header" #import "SUExport.h" #pragma clang diagnostic pop #else #import <Sparkle/SUExport.h> #endif @class SUAppcastItem; NS_ASSUME_NONNULL_BEGIN /** Applies special display formatting to version numbers of the bundle to update and the update before presenting them to the user. */ SU_EXPORT @protocol SUVersionDisplay <NSObject> /** Formats an update's version string and bundle's version string for display. This method is used to format both the display version of the update and the display version of the bundle to update. The display versions returned by this method are then used for presenting to the user when a new update is available, or when the user cannot download/install the latest update for a specific reason, or when the user has a newer version installed than the latest known version in the update feed. On input, the `update.displayVersionString` and `*inOutBundleDisplayVersion` may be the same, but the `update.versionString` and `bundleVersion` will differ. To differentiate between these display versions, you may choose to return different display version strings for the update and bundle. @param update The update to format the update display version from. You can query `update.displayVersionString` and `update.versionString` to retrieve the update's version information. @param inOutBundleDisplayVersion On input, the display version string (or `CFBundleShortVersionString`) of the bundle to update. On output, this is the display version string of the bundle to show to the user. @param bundleVersion The version (or CFBundleVersion) of the bundle to update. @return A new display version string of the `update.displayVersionString` to show to the user. */ - (NSString *)formatUpdateDisplayVersionFromUpdate:(SUAppcastItem *)update andBundleDisplayVersion:(NSString * _Nonnull __autoreleasing * _Nonnull)inOutBundleDisplayVersion withBundleVersion:(NSString *)bundleVersion; @optional /** Formats a bundle's version string for display. This method is used to format the display version of the bundle. This method may be used when no new update is available and the user is already on the latest known version. In this case, no new update version is shown to the user. This method is optional. If it's not implemented, Sparkle will default to using the `bundleDisplayVersion` passed to this method. @param bundleDisplayVersion The display version string (or `CFBundleShortVersionString`) of the bundle to update. @param bundleVersion The version (or `CFBundleVersion`) of the bundle to update. @param matchingUpdate The update in the feed that corresponds to the current bundle, or `nil` if no matching update item could be found in the feed. @return A new display version string of the bundle to show to the user. */ - (NSString *)formatBundleDisplayVersion:(NSString *)bundleDisplayVersion withBundleVersion:(NSString *)bundleVersion matchingUpdate:(SUAppcastItem * _Nullable)matchingUpdate; /** Formats two version strings. Both versions are provided so that important distinguishing information can be displayed while also leaving out unnecessary/confusing parts. */ - (void)formatVersion:(NSString *_Nonnull*_Nonnull)inOutVersionA andVersion:(NSString *_Nonnull*_Nonnull)inOutVersionB __deprecated_msg("Please use -formatUpdateDisplayVersionFromUpdate:andBundleDisplayVersion:withBundleVersion:"); @end NS_ASSUME_NONNULL_END ================================================ FILE: Sparkle/SUWKWebView.h ================================================ // // SUWKWebView.h // Sparkle // // Created by Mayur Pawashe on 12/30/20. // Copyright © 2020 Sparkle Project. All rights reserved. // #if SPARKLE_BUILD_UI_BITS #import <Foundation/Foundation.h> #import "SUReleaseNotesView.h" NS_ASSUME_NONNULL_BEGIN SPU_OBJC_DIRECT_MEMBERS @interface SUWKWebView : NSObject <SUReleaseNotesView> - (instancetype)initWithColorStyleSheetLocation:(NSURL *)colorStyleSheetLocation fontFamily:(NSString *)fontFamily fontPointSize:(int)fontPointSize javaScriptEnabled:(BOOL)javaScriptEnabled customAllowedURLSchemes:(NSArray<NSString *> *)customAllowedURLSchemes allowsLoadingExternalReferences:(BOOL)allowsLoadingExternalReferences installedVersion:(NSString *)installedVersion; @end NS_ASSUME_NONNULL_END #endif ================================================ FILE: Sparkle/SUWKWebView.m ================================================ // // SUWKWebView.m // Sparkle // // Created by Mayur Pawashe on 12/30/20. // Copyright © 2020 Sparkle Project. All rights reserved. // #if SPARKLE_BUILD_UI_BITS #import "SUWKWebView.h" #import "SUReleaseNotesCommon.h" #import "SULog.h" #import "SUErrors.h" #import <WebKit/WebKit.h> @interface WKWebView (Private) - (void)_setDrawsBackground:(BOOL)drawsBackground; - (void)_setDrawsTransparentBackground:(BOOL)drawsTransparentBackground; @end @interface SUWKWebView () <WKNavigationDelegate> @end @implementation SUWKWebView { WKWebView *_webView; WKNavigation *_currentNavigation; NSArray<NSString *> *_customAllowedURLSchemes; void (^_completionHandler)(NSError * _Nullable); BOOL _drawsWebViewBackground; BOOL _allowsLoadingExternalReferences; } static WKUserScript *makeUserScriptWithInjectedStyleSource(NSString *styleSource) { // We must remove newlines when inserting the style source in this interpolated string below NSString *strippedStyleSource = [styleSource stringByReplacingOccurrencesOfString:@"\n" withString:@""]; NSString *scriptSource = [NSString stringWithFormat: @"var style = document.createElement('style');\n" @"style.innerHTML = '%@'\n" @"var head = document.head;\n" @"if (head.firstChild) {" @"\tdocument.head.insertBefore(style, document.head.firstChild);\n" @"} else {\n" @"\tdocument.head.appendChild(style)\n" @"}", strippedStyleSource]; return [[WKUserScript alloc] initWithSource:scriptSource injectionTime:WKUserScriptInjectionTimeAtDocumentEnd forMainFrameOnly:YES]; } static WKUserScript *makeUserScriptForExposingCurrentRelease(NSString *releaseString) { // Check that release string can be safely injected NSMutableCharacterSet *allowedCharacterSet = [NSMutableCharacterSet alphanumericCharacterSet]; [allowedCharacterSet addCharactersInString:@"_.- "]; if ([releaseString rangeOfCharacterFromSet:allowedCharacterSet.invertedSet].location != NSNotFound) { SULog(SULogLevelDefault, @"warning: App version '%@' has characters unsafe for injection. The version number will not be exposed to the release notes CSS. Only [a-zA-Z0-9._- ] is allowed.", releaseString); return nil; } // This script adds the `sparkle-installed-version` class to all elements which have a matching `data-sparkle-version` attribute NSString *scriptSource = [NSString stringWithFormat: @"document.querySelectorAll(\'[data-sparkle-version=\"%@\"]\')\n" @".forEach(installedVersionElement =>\n" @"installedVersionElement.classList.add('sparkle-installed-version')\n" @");", releaseString]; return [[WKUserScript alloc] initWithSource:scriptSource injectionTime:WKUserScriptInjectionTimeAtDocumentEnd forMainFrameOnly:YES]; } - (instancetype)initWithColorStyleSheetLocation:(NSURL *)colorStyleSheetLocation fontFamily:(NSString *)fontFamily fontPointSize:(int)fontPointSize javaScriptEnabled:(BOOL)javaScriptEnabled customAllowedURLSchemes:(NSArray<NSString *> *)customAllowedURLSchemes allowsLoadingExternalReferences:(BOOL)allowsLoadingExternalReferences installedVersion:(NSString *)installedVersion { self = [super init]; if (self != nil) { // Synchronize with web view defaulting to drawing background to avoid unnecessary invocations in -setDrawsBackground: _drawsWebViewBackground = YES; _allowsLoadingExternalReferences = allowsLoadingExternalReferences; WKWebViewConfiguration *configuration = [[WKWebViewConfiguration alloc] init]; // Note: this javaScriptEnabled property is deprecated in favor of another webpage preference property, // that involves implementing a delegate method that is only available on macOS 11.. to get it properly working. // To simplify things, just rely on deprecated property for now. // Future reader: if you change how JS is disabled, please be sure to test that JS code is properly disabled in HTML release notes. #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdeprecated-declarations" configuration.preferences.javaScriptEnabled = javaScriptEnabled; #pragma clang diagnostic pop configuration.preferences.javaScriptCanOpenWindowsAutomatically = NO; NSError *colorStyleContentsError = nil; NSString *colorStyleContents = [NSString stringWithContentsOfURL:colorStyleSheetLocation encoding:NSUTF8StringEncoding error:&colorStyleContentsError]; WKUserContentController *userContentController = [[WKUserContentController alloc] init]; NSString *fontStyleContents = [NSString stringWithFormat:@"body { font-family: %@; font-size: %dpx; }", fontFamily, fontPointSize]; NSString *finalStyleContents; if (colorStyleContents == nil) { SULog(SULogLevelError, @"Failed to load style contents from %@ with %@", colorStyleSheetLocation, colorStyleContentsError); finalStyleContents = fontStyleContents; } else { finalStyleContents = [NSString stringWithFormat:@"%@ %@", fontStyleContents, colorStyleContents]; } // Note: we can still execute javascript via WKUserScript even if javascript is otherwise disabled from the web content // In fact, we must execute javascript to properly inject our default CSS style into the DOM // Legacy WebView has exposed methods for custom stylesheets and default fonts, // but WKWebView seems to forgo that type of API surface in favor of user scripts like this WKUserScript *userScriptWithInjectedStyleSource = makeUserScriptWithInjectedStyleSource(finalStyleContents); if (userScriptWithInjectedStyleSource == nil) { SULog(SULogLevelError, @"Failed to create script for injecting style"); } else { [userContentController addUserScript:userScriptWithInjectedStyleSource]; } WKUserScript *userScriptForExposingCurrentRelease = makeUserScriptForExposingCurrentRelease(installedVersion); if (userScriptForExposingCurrentRelease == nil) { SULog(SULogLevelDefault, @"warning: Failed to create script for injecting version %@", installedVersion); } else { [userContentController addUserScript:userScriptForExposingCurrentRelease]; } configuration.userContentController = userContentController; _webView = [[WKWebView alloc] initWithFrame:NSZeroRect configuration:configuration]; _webView.navigationDelegate = self; _customAllowedURLSchemes = customAllowedURLSchemes; } return self; } - (NSView *)view { return _webView; } static void SPULoadWebContent(BOOL allowsExternalReferences, WKUserContentController *userContentController, void (^loadHTMLContent)(void)) { if (allowsExternalReferences) { loadHTMLContent(); return; } // Block loading all external resources for signed appcasts & signed release notes NSString *encodedContentRuleList = @"[{\"trigger\": { \"url-filter\": \".*\" }, \"action\": { \"type\": \"block\" } }]"; [WKContentRuleListStore.defaultStore compileContentRuleListForIdentifier:@"sparkle-updater" encodedContentRuleList:encodedContentRuleList completionHandler:^(WKContentRuleList *contentRuleList, NSError *contentRuleListError) { dispatch_async(dispatch_get_main_queue(), ^{ if (contentRuleList == nil) { SULog(SULogLevelError, @"Error: failed to load content rule list for WKWebView with error: %@", contentRuleListError); } else { [userContentController addContentRuleList:contentRuleList]; } loadHTMLContent(); }); }]; } - (void)loadString:(NSString *)htmlString baseURL:(NSURL * _Nullable)baseURL completionHandler:(void (^)(NSError * _Nullable))completionHandler { _completionHandler = [completionHandler copy]; SPULoadWebContent(_allowsLoadingExternalReferences, _webView.configuration.userContentController, ^{ self->_currentNavigation = [self->_webView loadHTMLString:htmlString baseURL:baseURL]; }); } - (void)loadData:(NSData *)data MIMEType:(NSString *)MIMEType textEncodingName:(NSString *)textEncodingName baseURL:(NSURL *)baseURL completionHandler:(void (^)(NSError * _Nullable))completionHandler { _completionHandler = [completionHandler copy]; SPULoadWebContent(_allowsLoadingExternalReferences, _webView.configuration.userContentController, ^{ self->_currentNavigation = [self->_webView loadData:data MIMEType:MIMEType characterEncodingName:textEncodingName baseURL:baseURL]; }); } - (void)setDrawsBackground:(BOOL)drawsBackground { if (_drawsWebViewBackground != drawsBackground) { // Unfortunately we have to rely on a private API // FB7539179: https://github.com/feedback-assistant/reports/issues/81 | https://bugs.webkit.org/show_bug.cgi?id=155550 // But it seems like others are already relying on it, passed App Review, and apps couldn't be broken due to compatibility // Note: before we were using _setDrawsTransparentBackground < macOS 10.12 if ([_webView respondsToSelector:@selector(_setDrawsBackground:)]) { [_webView _setDrawsBackground:drawsBackground]; } _drawsWebViewBackground = drawsBackground; } } - (void)stopLoading { _completionHandler = nil; [_webView stopLoading]; } - (void)webView:(WKWebView *)webView didFinishNavigation:(WKNavigation *)navigation { if (navigation == _currentNavigation) { if (_completionHandler != nil) { _completionHandler(nil); _completionHandler = nil; } _currentNavigation = nil; } } - (void)webView:(WKWebView *)webView didFailNavigation:(WKNavigation *)navigation withError:(NSError *)error { if (navigation == _currentNavigation) { if (_completionHandler != nil) { _completionHandler(error); _completionHandler = nil; } _currentNavigation = nil; } } - (void)webViewWebContentProcessDidTerminate:(WKWebView *)webView { if (_currentNavigation != nil) { if (_completionHandler != nil) { _completionHandler([NSError errorWithDomain:SUSparkleErrorDomain code:SUWebKitTerminationError userInfo:nil]); _completionHandler = nil; } _currentNavigation = nil; } } - (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler { NSURLRequest *request = navigationAction.request; NSURL *requestURL = request.URL; BOOL isAboutBlank = NO; BOOL safeURL = SUReleaseNotesIsSafeURL(requestURL, _customAllowedURLSchemes, &isAboutBlank); // Do not allow redirects to dangerous protocols such as file:// if (!safeURL) { SULog(SULogLevelDefault, @"Blocked display of %@ URL which may be dangerous", requestURL.scheme); decisionHandler(WKNavigationActionPolicyCancel); } else { // Ensure we're finished loading if (_completionHandler == nil) { if (!isAboutBlank) { [[NSWorkspace sharedWorkspace] openURL:requestURL]; } decisionHandler(WKNavigationActionPolicyCancel); } else { decisionHandler(WKNavigationActionPolicyAllow); } } } @end #endif ================================================ FILE: Sparkle/Sparkle-Info.plist ================================================ <?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <plist version="1.0"> <dict> <key>CFBundleDevelopmentRegion</key> <string>en</string> <key>CFBundleExecutable</key> <string>${EXECUTABLE_NAME}</string> <key>CFBundleIconFile</key> <string></string> <key>CFBundleIdentifier</key> <string>$(PRODUCT_BUNDLE_IDENTIFIER)</string> <key>CFBundleInfoDictionaryVersion</key> <string>6.0</string> <key>CFBundleName</key> <string>${PRODUCT_NAME}</string> <key>CFBundlePackageType</key> <string>FMWK</string> <key>CFBundleShortVersionString</key> <string>$(MARKETING_VERSION)</string> <key>CFBundleSignature</key> <string>????</string> <key>CFBundleVersion</key> <string>${CURRENT_PROJECT_VERSION}</string> <key>NSPrincipalClass</key> <string></string> </dict> </plist> ================================================ FILE: Sparkle/Sparkle.h ================================================ // // Sparkle.h // Sparkle // // Created by Andy Matuschak on 3/16/06. (Modified by CDHW on 23/12/07) // Copyright 2006 Andy Matuschak. All rights reserved. // #ifndef SPARKLE_H #define SPARKLE_H // This list should include the shared headers. It doesn't matter if some of them aren't shared (unless // there are name-space collisions) so we can list all of them to start with: #import <Sparkle/SUExport.h> #import <Sparkle/SUAppcast.h> #import <Sparkle/SUAppcastItem.h> #import <Sparkle/SUStandardVersionComparator.h> #import <Sparkle/SPUUpdater.h> #import <Sparkle/SPUUpdaterDelegate.h> #import <Sparkle/SPUUpdaterSettings.h> #import <Sparkle/SUVersionComparisonProtocol.h> #import <Sparkle/SUVersionDisplayProtocol.h> #import <Sparkle/SUErrors.h> #import <Sparkle/SPUUpdatePermissionRequest.h> #import <Sparkle/SUUpdatePermissionResponse.h> #import <Sparkle/SPUUserDriver.h> #import <Sparkle/SPUDownloadData.h> // UI bits #import <Sparkle/SPUStandardUpdaterController.h> #import <Sparkle/SPUStandardUserDriver.h> #import <Sparkle/SPUStandardUserDriverDelegate.h> // Deprecated bits #import <Sparkle/SUUpdater.h> #import <Sparkle/SUUpdaterDelegate.h> #endif ================================================ FILE: Sparkle/Sparkle.private.modulemap ================================================ // // Sparkle.private.modulemap // Sparkle // // Created on 4/30/25. // Copyright © 2025 Sparkle Project. All rights reserved. // framework module Sparkle_Private { // Nothing exported here } explicit module Sparkle_Private.SPUStandardUserDriver { header "SPUStandardUserDriver+Private.h" export * } explicit module Sparkle_Private.SPUGentleUserDriverReminders { header "SPUGentleUserDriverReminders.h" export * } explicit module Sparkle_Private.SPUUserAgent { header "SPUUserAgent+Private.h" export * } explicit module Sparkle_Private.SUAppcastItem { header "SUAppcastItem+Private.h" header "SPUAppcastItemStateResolver.h" export * } explicit module Sparkle_Private.SUInstallerLauncher { header "SUInstallerLauncher+Private.h" header "SPUInstallationType.h" export * } ================================================ FILE: Sparkle/ar.lproj/Sparkle.strings ================================================ /* No comment provided by engineer. */ "%@ %@ is currently the newest version available." = "الإصدار %2$@ هو أحدث إصدار متوفر حاليًا لتطبيق %1$@"; /* The download progress in a unit of bytes, e.g. 100 KB */ "%@ downloaded" = "تم تنزيل %@"; /* The download progress in units of bytes, e.g. 100 KB of 1,0 MB */ "%@ of %@" = "%1$@ من %2$@"; /* Description text for SUUpdateAlert when the update has already been downloaded and ready to install. */ "%1$@ %2$@ has been downloaded and is ready to use! Would you like to install it and relaunch %1$@ now?" = "تم تنزيل %1$@ %2$@ وهو جاهز للاستخدام، هل ترغب بتثبيت التحديث وإعادة تشغيل %1$@ الآن؟"; /* No comment provided by engineer. */ "A new version of %@ is available!" = "يتوفر إصدار جديد من تطبيق %@"; /* No comment provided by engineer. */ "A new version of %@ is ready to install!" = "الإصدار الجديد من تطبيق %@ جاهز للتثبيت"; /* No comment provided by engineer. */ "An error occurred in retrieving update information. Please try again later." = "حال خطأ دون الحصول على معلومات حول التحديث، الرجاء المحاولة لاحقًا."; /* No comment provided by engineer. */ "An error occurred while downloading the update. Please try again later." = "حدث خطأ أثناء تنزيل التحديث، الرجاء المحاولة لاحقًا."; /* No comment provided by engineer. */ "An error occurred while extracting the archive. Please try again later." = "حدث خطأ أثناء استخراج الأرشيف، الرجاء المحاولة لاحقًا."; /* No comment provided by engineer. */ "An error occurred while parsing the update feed." = "حدث خطأ أثناء تحليل المعلومات المزوّدَة حول التحديث."; /* No comment provided by engineer. */ "Cancel" = "إلغاء"; /* No comment provided by engineer. */ "Cancel Update" = "إلغاء التحديث"; /* No comment provided by engineer. */ "Install and Relaunch" = "تثبيت وإعادة تشغيل التطبيق"; /* No comment provided by engineer. */ "OK" = "موافق"; /* No comment provided by engineer. */ "Ready to Install" = "جاهز للتثبيت"; /* No comment provided by engineer. */ "Should %1$@ automatically check for updates? You can always check for updates manually from the %1$@ menu." = "هل ترغب في أن يتحقق %1$@ من وجود تحديثات تلقائيًا؟ يمكنك التحقق من وجود تحديثات يدويًا في أي وقت من قائمة %1$@."; /* No comment provided by engineer. */ "Update Error!" = "حدث خطأ أثناء التحديث"; /* No comment provided by engineer. */ "Updating %@" = "تحديث %@"; /* No comment provided by engineer. */ "Use Finder to copy %1$@ to the Applications folder, relaunch it from there, and try again." = "انقل %1$@ إلى مجلد التطبيقات وأعد تشغيله من هناك ثم حاول مجددًا."; /* Software Update title/label */ "Software Update" = "محدث البرنامج"; /* Remind Me Later choice for update alert */ "Remind Me Later" = "تذكيري لاحقًا"; /* Skip This Version choice for update alert */ "Skip This Version" = "تخطي هذا الإصدار"; /* Install Update choice for update alert */ "Install Update" = "تثبيت التحديث"; /* Automatically download and install updates in the future option for update alert */ "Automatically download and install updates in the future" = "تنزيل التحديثات وتثبيتها تلقائيًا في المستقبل"; /* Check for updates automatically response for update permission dialog */ "Check Automatically" = "التحقق تلقائيًا"; /* Don't check for updates automatically response for update permission dialog */ "Don’t Check" = "عدم التحقق"; /* Title question for update permission dialog */ "Check for updates automatically?" = "هل تريد أن يتم التحقق من وجود تحديثات تلقائيًا؟"; /* Include anonymous system profile choice for update permission dialog */ "Include anonymous system profile" = "تضمين تقرير عن النظام دون ذكر معلومات عن المستخدم"; /* Automatically download and install updates choice for update permission dialog */ "Automatically download and install updates" = "تنزيل التحديثات وتثبيتها تلقائيًا في المست"; /* Anonymous system profile information disclosure for update permission dialog */ "Anonymous system profile information is used to help us plan future development work. Please contact us if you have any questions about this.\n\nThis is the information that would be sent:" = "Anonymous system profile information is used to help us plan future development work. Please contact us if you have any questions about this.\n\nThis is the information that would be sent:"; ================================================ FILE: Sparkle/ca.lproj/Sparkle.strings ================================================ /* No comment provided by engineer. */ "%@ %@ is currently the newest version available." = "%@ %@ és la versió disponible més actual."; /* No comment provided by engineer. */ "%@ %@ is currently the newest version available.\n(You are currently running version %@.)" = "%@ %@ és la versió disponible més actual.\n(You are currently running version %@.)"; /* Description text for SUUpdateAlert when the update is downloadable. */ "%@ %@ is now available—you have %@. Would you like to download it now?" = "%@ %@ està disponible (ara teniu %@). Voleu actualitzar?"; /* The download progress in units of bytes, e.g. 100 KB of 1,0 MB */ "%@ of %@" = "%1$@ de %2$@"; /* No comment provided by engineer. */ "%1$@ can’t be updated because it was opened from a read-only or a temporary location." = "%1$@ no es pot actualitzar quan funciona des d'un disc d'imatge."; /* No comment provided by engineer. */ "A new version of %@ is available!" = "Hi ha una nova versió de %@ disponible!"; /* No comment provided by engineer. */ "An error occurred in retrieving update information. Please try again later." = "Hi ha hagut un error obtenint la informació d'actualització. Torneu a provar-ho més tard."; /* No comment provided by engineer. */ "An error occurred while extracting the archive. Please try again later." = "Hi ha hagut un error al extreure l'arxiu? Torneu a provar-ho més tard."; /* No comment provided by engineer. */ "Cancel" = "Cancel·la"; /* Take care not to overflow the status window. */ "Downloading update…" = "Descarregant l'actualització…"; /* Take care not to overflow the status window. */ "Extracting update…" = "Extraient l'actualització…"; /* No comment provided by engineer. */ "Install and Relaunch" = "Instal·la i reinicia"; /* Alternate title for 'Remind Me Later' button when downloaded updates can be resumed */ "Install on Quit" = "Reinicia el programa més tard"; /* Take care not to overflow the status window. */ "Installing update…" = "Instal·lant l'actualització…"; /* No comment provided by engineer. */ "OK" = "D'acord"; /* No comment provided by engineer. */ "Ready to Install" = "A punt per instal·lar"; /* No comment provided by engineer. */ "Should %1$@ automatically check for updates? You can always check for updates manually from the %1$@ menu." = "Voleu que %1$@ comprovi si hi ha noves actualitzacions al iniciar? Si no, podeu inciar la comprovació manualment des del menú %1$@."; /* No comment provided by engineer. */ "Update Error!" = "Error d'actualització!"; /* No comment provided by engineer. */ "Updating %@" = "Actualitzant %@"; /* No comment provided by engineer. */ "Use Finder to copy %1$@ to the Applications folder, relaunch it from there, and try again." = "Moveu %1$@ al vostre directori Aplicacions, reinicieu-lo, i torneu a provar-ho."; /* Status message shown when the user checks for updates but is already current or the feed doesn't contain any updates. */ "You’re up to date!" = "Ja esteu al dia!"; /* Software Update title/label */ "Software Update" = "Actualització del programari"; /* Remind Me Later choice for update alert */ "Remind Me Later" = "Recorda-m'ho més tard"; /* Skip This Version choice for update alert */ "Skip This Version" = "Omet aquesta versió"; /* Install Update choice for update alert */ "Install Update" = "Instal·la l'actualització"; /* Automatically download and install updates in the future option for update alert */ "Automatically download and install updates in the future" = "Descarrega i instal·la les actualitzacions automàticament en el futur"; ================================================ FILE: Sparkle/cs.lproj/Sparkle.strings ================================================ /* No comment provided by engineer. */ "%@ %@ is currently the newest version available." = "%1$@ %2$@ je nejnovější dostupná verze"; /* No comment provided by engineer. */ "%@ %@ is currently the newest version available.\n(You are currently running version %@.)" = "%1$@ %2$@ je nejnovější dostupná verze.\n(Aktuálně používáte verzi %3$@.)"; /* Description text for SUUpdateAlert when the critical update is downloadable. */ "%@ %@ is now available—you have %@. This is an important update; would you like to download it now?" = "Verze %1$@ %2$@ je nyní k dispozici – nainstalována je %3$@. Tato aktualizace je důležitá; přejete si ji nyní stáhnout?"; /* Description text for SUUpdateAlert when the update is downloadable. */ "%@ %@ is now available—you have %@. Would you like to download it now?" = "Je k dispozici %1$@ %2$@ - nainstalována je %3$@. Přejete si nyní stáhnout aktualizaci?"; /* Description text for SUUpdateAlert when the update informational with no download. */ "%@ %@ is now available—you have %@. Would you like to learn more about this update on the web?" = "%1$@ %2$@ je nyní k dispozici – nainstalována je %3$@. Přejete si zobrazit o této aktualizaci další informace?"; /* The download progress in a unit of bytes, e.g. 100 KB */ "%@ downloaded" = "staženo %@"; /* No comment provided by engineer. */ "%@ is now updated to version %@!" = "Aplikace %1$@ je nyní aktualizována na verzi %2$@! "; /* No comment provided by engineer. */ "%@ is now updated!" = "%@ je nyní aktuální!"; /* The download progress in units of bytes, e.g. 100 KB of 1,0 MB */ "%@ of %@" = "%1$@ z %2$@"; /* Description text for SUUpdateAlert when the critical update has already been downloaded and ready to install. */ "%1$@ %2$@ has been downloaded and is ready to use! This is an important update; would you like to install it and relaunch %1$@ now?" = "Aplikace %1$@ %2$@ byla stažena a je připravena k použití po příštím spuštění! Toto je důležitá aktualizace; přejete si aplikaci %1$@ nyní nainstalovat a znovu spustit?"; /* Description text for SUUpdateAlert when the update has already been downloaded and ready to install. */ "%1$@ %2$@ has been downloaded and is ready to use! Would you like to install it and relaunch %1$@ now?" = "Aplikace %1$@ %2$@ byla stažena a je připravena k použití po příštím spuštění! Přejete si aplikaci %1$@ nyní nainstalovat a znovu spustit?"; /* No comment provided by engineer. */ "%1$@ %2$@ is available but your macOS version is too new for this update. This update only supports up to macOS %3$@." = "%1$@ %2$@ je k dispozici, ale vaše verze macOS je pro tuto aktualizaci příliš nová. Aktualizace podporuje nejvýše macOS %3$@."; /* No comment provided by engineer. */ "%1$@ %2$@ is available but your macOS version is too old to install it. At least macOS %3$@ is required." = "%1$@ %2$@ je k dispozici, ale vaše verze macOS je pro tuto aktualizaci příliš stará. Je potřeba alespoň macOS %3$@."; /* No comment provided by engineer. */ "%1$@ can’t be updated if it’s running from the location it was downloaded to." = "Aplikace %1$@ nemůže být aktualizována pokud je spuštěna z umístění, ve kterém je stažena."; /* No comment provided by engineer. */ "%1$@ can’t be updated because it was opened from a read-only or a temporary location." = "Aplikace %1$@ nemůže být aktualizována, protože je spuštěna z dočasného umístění nebo z umístění, na které nelze zapisovat."; /* No comment provided by engineer. */ "A new version of %@ is available!" = "Je dostupná nová verze aplikace %@!"; /* No comment provided by engineer. */ "A new version of %@ is ready to install!" = "Nová verze %@ je připravena k instalaci!"; /* No comment provided by engineer. */ "An error occurred in retrieving update information. Please try again later." = "Při získávání údajů o aktualizaci se vyskytla chyba. Zkuste to prosím později."; /* No comment provided by engineer. */ "An error occurred while connecting to the installer. Please try again later." = "Při připojování k instalátoru se vyskytla chyba. Zkuste to prosím později."; /* No comment provided by engineer. */ "An error occurred while downloading the update. Please try again later." = "Při stahování souboru se vyskytla chyba. Zkuste to prosím později."; /* No comment provided by engineer. */ "An error occurred while extracting the archive. Please try again later." = "Při rozbalování archivu se vyskytla chyba. Zkuste to prosím později."; /* No comment provided by engineer. */ "An error occurred while launching the installer. Please try again later." = "Při spouštění instalátoru se vyskytla chyba. Zkuste to prosím později."; /* No comment provided by engineer. */ "An error occurred while parsing the update feed." = "Při zpracování údajů o aktualizaci se vyskytla chyba. Zkuste to prosím později."; /* No comment provided by engineer. */ "An error occurred while running the updater. Please try again later." = "Při provádění aktualizace se vyskytla chyba. Zkuste to prosím později."; /* No comment provided by engineer. */ "An error occurred while starting the installer. Please try again later." = "Při spouštění instalátoru se vyskytla chyba. Zkuste to prosím později."; /* No comment provided by engineer. */ "An important update to %@ is ready to install" = "Důležitá aktualizace %@ je připravena k instalaci"; /* No comment provided by engineer. */ "Cancel" = "Zrušit"; /* No comment provided by engineer. */ "Cancel Update" = "Zrušit aktualizaci"; /* Take care not to overflow the status window. */ "Downloading update…" = "Stahuje se aktualizace…"; /* Take care not to overflow the status window. */ "Extracting update…" = "Rozbaluje se aktualizace…"; /* No comment provided by engineer. */ "Failed to resume installing update." = "Nepodařilo se pokračovat v aktualizaci."; /* No comment provided by engineer. */ "Checking for updates…" = "Hledání aktualizací…"; /* No comment provided by engineer. */ "Install and Relaunch" = "Instalovat a znovu spustit"; /* Alternate title for 'Remind Me Later' button when downloaded updates can be resumed */ "Install on Quit" = "Instalovat a ukončit"; /* Take care not to overflow the status window. */ "Installing update…" = "Instaluje se aktualizace…"; /* Alternate title for 'Install Update' button when there's no download in RSS feed. */ "Learn More…" = "Další informace…"; /* No comment provided by engineer. */ "OK" = "OK"; /* No comment provided by engineer. */ "Quit %1$@, move it into your Applications folder, relaunch it from there and try again." = "Ukončete %1$@, spusťte %1$@ ze složky Aplikace a spusťte aktualizaci znovu."; /* No comment provided by engineer. */ "Ready to Install" = "Připraveno k instalaci"; /* No comment provided by engineer. */ "Should %1$@ automatically check for updates? You can always check for updates manually from the %1$@ menu." = "Přejete si, aby aplikace %1$@ automaticky vyhledávala aktualizace? Tuto volbu můžete kdykoliv změnit v nabídce %1$@."; /* No comment provided by engineer. */ "The update is improperly signed and could not be validated. Please try again later or contact the app developer." = "Tato aktualizace není správně podepsaná a nelze ji ověřit. Zkuste to prosím později nebo kontaktujte vývojáře aplikace."; /* No comment provided by engineer. */ "Unable to Check For Updates" = "Hledání aktualizací se nezdařilo"; /* No comment provided by engineer. */ "Update Error!" = "Chyba při aktualizaci!"; /* No comment provided by engineer. */ "Update Installed" = "Aktualizace dokončena"; /* No comment provided by engineer. */ "Updating %@" = "Aktualizuje se %@"; /* No comment provided by engineer. */ "Use Finder to copy %1$@ to the Applications folder, relaunch it from there, and try again." = "Přesuňte %1$@ do složky Aplikace a spusťte ji z tohoto umístění znovu."; /* No comment provided by engineer. */ "Version History" = "Historie verzí"; /* Status message shown when the user checks for updates but is already current or the feed doesn't contain any updates. */ "You’re up to date!" = "Vaše verze je aktuální!"; /* No comment provided by engineer. */ "Your macOS version is too new" = "Vaše verze macOS je příliš nová"; /* No comment provided by engineer. */ "Your macOS version is too old" = "Vaše verze macOS je příliš stará"; /* Software Update title/label */ "Software Update" = "Aktualizace aplikace"; /* Remind Me Later choice for update alert */ "Remind Me Later" = "Připomenout později"; /* Skip This Version choice for update alert */ "Skip This Version" = "Přeskočit tuto verzi"; /* Install Update choice for update alert */ "Install Update" = "Instalovat aktualizaci"; /* Automatically download and install updates in the future option for update alert */ "Automatically download and install updates in the future" = "V budoucnu stahovat a instalovat aktualizace automaticky"; /* Check for updates automatically response for update permission dialog */ "Check Automatically" = "Automaticky vyhledávat"; /* Don't check for updates automatically response for update permission dialog */ "Don’t Check" = "Nevyhledávat"; /* Title question for update permission dialog */ "Check for updates automatically?" = "Vyhledávat aktualizace automaticky?"; /* Include anonymous system profile choice for update permission dialog */ "Include anonymous system profile" = "Odeslat anonymní systémový profil"; /* Automatically download and install updates choice for update permission dialog */ "Automatically download and install updates" = "Stahovat a instalovat aktualizace automaticky"; /* Anonymous system profile information disclosure for update permission dialog */ "Anonymous system profile information is used to help us plan future development work. Please contact us if you have any questions about this.\n\nThis is the information that would be sent:" = "Informace z anonymního systémového profilu pomáhají vývojářům lépe plánovat budoucí vývoj aplikace.\nBudete-li mít nějaký dotaz, obraťte se na nás.\n\nToto jsou informace, které budou odeslány:"; ================================================ FILE: Sparkle/da.lproj/Sparkle.strings ================================================ /* No comment provided by engineer. */ "%@ %@ is currently the newest version available." = "%1$@ %2$@ er den aktuelle version."; /* No comment provided by engineer. */ "%@ %@ is currently the newest version available.\n(You are currently running version %@.)" = "%1$@ %2$@ er den aktuelle version.\n(Du kører lige nu version %3$@.)"; /* Description text for SUUpdateAlert when the update is downloadable. */ "%@ %@ is now available—you have %@. Would you like to download it now?" = "%1$@ %2$@ er tilgængelig! Du har %3$@. Skal den hentes nu?"; /* Description text for SUUpdateAlert when the update informational with no download. */ "%@ %@ is now available—you have %@. Would you like to learn more about this update on the web?" = "%1$@ %2$@ er tilgængelig--du har %3$@. Vil du lære mere om denne opdatering på web?"; /* The download progress in a unit of bytes, e.g. 100 KB */ "%@ downloaded" = "%@ hentet"; /* The download progress in units of bytes, e.g. 100 KB of 1,0 MB */ "%@ of %@" = "%1$@ af %2$@"; /* Description text for SUUpdateAlert when the update has already been downloaded and ready to install. */ "%1$@ %2$@ has been downloaded and is ready to use! Would you like to install it and relaunch %1$@ now?" = "%1$@ %2$@ er hentet og klar til brug! Vil du installere og genstarte %1$@ nu?"; /* No comment provided by engineer. */ "%1$@ can’t be updated because it was opened from a read-only or a temporary location." = "%1$@ kan ikke opdateres når det køres fra en kun læsbar enhed."; /* No comment provided by engineer. */ "A new version of %@ is available!" = "En ny version af %@ er tilgængelig!"; /* No comment provided by engineer. */ "A new version of %@ is ready to install!" = "En ny version af %@ er klar til installering!"; /* No comment provided by engineer. */ "An error occurred in retrieving update information. Please try again later." = "Kunne ikke modtage informationer om opdateringer. Kontroller at du har forbindelse til internettet eller prøv igen senere."; /* No comment provided by engineer. */ "An error occurred while downloading the update. Please try again later." = "Opdateringen kunne ikke hentes. Prøv igen senere."; /* No comment provided by engineer. */ "An error occurred while extracting the archive. Please try again later." = "Arkivet kunne ikke udpakkes. Prøv igen senere."; /* No comment provided by engineer. */ "An error occurred while parsing the update feed." = "En fejl opstod under læsning af opdaterings-feed."; /* No comment provided by engineer. */ "Cancel" = "Annuller"; /* No comment provided by engineer. */ "Cancel Update" = "Annuller opdatering"; /* No comment provided by engineer. */ "Checking for updates…" = "Søger efter opdateringer…"; /* Take care not to overflow the status window. */ "Downloading update…" = "Henter opdatering…"; /* Take care not to overflow the status window. */ "Extracting update…" = "Udpakker arkiver…"; /* No comment provided by engineer. */ "Install and Relaunch" = "Installer og genstart"; /* Take care not to overflow the status window. */ "Installing update…" = "Installerer opdatering…"; /* Alternate title for 'Install Update' button when there's no download in RSS feed. */ "Learn More…" = "Læs mere…"; /* No comment provided by engineer. */ "OK" = "OK"; /* No comment provided by engineer. */ "Ready to Install" = "Klar til installering"; /* No comment provided by engineer. */ "Should %1$@ automatically check for updates? You can always check for updates manually from the %1$@ menu." = "Skal %1$@ søge efter opdateringer automatisk? Du kan altid søge efter opdateringer manuelt fra programmets menu."; /* No comment provided by engineer. */ "Update Error!" = "Der opstod en fejl!"; /* No comment provided by engineer. */ "Updating %@" = "Opdaterer %@"; /* No comment provided by engineer. */ "Use Finder to copy %1$@ to the Applications folder, relaunch it from there, and try again." = "Flyt %1$@ til mappen Programmer, genstart derfra og prøv igen."; /* Status message shown when the user checks for updates but is already current or the feed doesn't contain any updates. */ "You’re up to date!" = "Der er ingen opdateringer"; /* Software Update title/label */ "Software Update" = "Software Update"; /* Remind Me Later choice for update alert */ "Remind Me Later" = "Påmind mig senere"; /* Skip This Version choice for update alert */ "Skip This Version" = "Spring over"; /* Install Update choice for update alert */ "Install Update" = "Installer"; /* Automatically download and install updates in the future option for update alert */ "Automatically download and install updates in the future" = "Hent og installer opdateringer automatisk i fremtiden"; /* Check for updates automatically response for update permission dialog */ "Check Automatically" = "Søg automatisk"; /* Don't check for updates automatically response for update permission dialog */ "Don’t Check" = "Søg ikke"; /* Title question for update permission dialog */ "Check for updates automatically?" = "Søg efter opdateringer automatisk?"; /* Include anonymous system profile choice for update permission dialog */ "Include anonymous system profile" = "Vedhæft anonym systemprofil"; /* Automatically download and install updates choice for update permission dialog */ "Automatically download and install updates" = "Hent og installer opdateringer automatisk"; /* Anonymous system profile information disclosure for update permission dialog */ "Anonymous system profile information is used to help us plan future development work. Please contact us if you have any questions about this.\n\nThis is the information that would be sent:" = "Anonymous system profile information is used to help us plan future development work. Please contact us if you have any questions about this.\n\nThis is the information that would be sent:"; ================================================ FILE: Sparkle/de.lproj/Sparkle.strings ================================================ /* No comment provided by engineer. */ "%@ %@ is currently the newest version available." = "%1$@ %2$@ ist zurzeit die neueste verfügbare Version."; /* No comment provided by engineer. */ "%@ %@ is currently the newest version available.\n(You are currently running version %@.)" = "%1$@ %2$@ ist zurzeit die neueste verfügbare Version.\n(Die derzeit installierte Version ist %3$@.)"; /* Description text for SUUpdateAlert when the critical update is downloadable. */ "%@ %@ is now available—you have %@. This is an important update; would you like to download it now?" = "%1$@ %2$@ ist verfügbar – du verwendest Version %3$@. Dies ist ein wichtiges Update. Möchtest du die neue Version jetzt laden?"; /* Description text for SUUpdateAlert when the update is downloadable. */ "%@ %@ is now available—you have %@. Would you like to download it now?" = "%1$@ %2$@ ist verfügbar – du verwendest Version %3$@. Möchtest du die neue Version jetzt laden?"; /* Description text for SUUpdateAlert when the update informational with no download. */ "%@ %@ is now available—you have %@. Would you like to learn more about this update on the web?" = "%1$@ %2$@ ist verfügbar – du verwendest Version %3$@. Möchtest du mehr über dieses Update erfahren?"; /* The download progress in a unit of bytes, e.g. 100 KB */ "%@ downloaded" = "%@ geladen"; /* No comment provided by engineer. */ "%@ is now updated to version %@!" = "%@ wurde auf Version %@ aktualisiert!"; /* No comment provided by engineer. */ "%@ is now updated!" = "%@ wurde aktualisiert!"; /* The download progress in units of bytes, e.g. 100 KB of 1,0 MB */ "%@ of %@" = "%1$@ von %2$@"; /* Description text for SUUpdateAlert when the critical update has already been downloaded and ready to install. */ "%1$@ %2$@ has been downloaded and is ready to use! This is an important update; would you like to install it and relaunch %1$@ now?" = "%1$@ %2$@ wurde geladen und steht zur Installation bereit! Dies ist ein wichtiges Update. Möchtest du %1$@ jetzt aktualisieren?"; /* Description text for SUUpdateAlert when the update has already been downloaded and ready to install. */ "%1$@ %2$@ has been downloaded and is ready to use! Would you like to install it and relaunch %1$@ now?" = "%1$@ %2$@ wurde geladen und steht zur Installation bereit! Möchtest du %1$@ jetzt aktualisieren?"; /* No comment provided by engineer. */ "%1$@ %2$@ is available but your macOS version is too new for this update. This update only supports up to macOS %3$@." = "%1$@ %2$@ ist verfügbar, aber deine Version von macOS ist zu neu für dieses Update. Das Update unterstützt nur bis macOS %3$@."; /* No comment provided by engineer. */ "%1$@ %2$@ is available but your macOS version is too old to install it. At least macOS %3$@ is required." = "%1$@ %2$@ ist verfügbar, aber deine Version von macOS ist zu alt, um das Update zu installieren. Es wird mindestens macOS %3$@ benötigt."; /* No comment provided by engineer. */ "%1$@ can’t be updated if it’s running from the location it was downloaded to." = "„%1$@“ kann nicht aktualisiert werden, wenn das Programm von dem Ort ausgeführt wird, an den es heruntergeladen wurde."; /* No comment provided by engineer. */ "%1$@ can’t be updated because it was opened from a read-only or a temporary location." = "%1$@ kann nicht aktualisiert werden, da es aus dem Downloads-Ordner, oder von einer DMG-Datei oder einem Laufwerk ohne Schreibzugriff gestartet wurde."; /* No comment provided by engineer. */ "A new version of %@ is available!" = "Eine neue Version von %@ ist verfügbar!"; /* No comment provided by engineer. */ "A new version of %@ is ready to install!" = "Eine neue Version von %@ steht zur Installation bereit!"; /* No comment provided by engineer. */ "An error occurred in retrieving update information. Please try again later." = "Beim Laden der Updateinformationen ist ein Fehler aufgetreten. Bitte versuche es später erneut."; /* No comment provided by engineer. */ "An error occurred while connecting to the installer. Please try again later." = "Beim Verbinden mit dem Installationsprogramm ist ein Fehler aufgetreten. Bitte versuche es später erneut."; /* No comment provided by engineer. */ "An error occurred while downloading the update. Please try again later." = "Beim Laden des Updates ist ein Fehler aufgetreten. Bitte versuche es später erneut."; /* No comment provided by engineer. */ "An error occurred while extracting the archive. Please try again later." = "Beim Entpacken des Archivs ist ein Fehler aufgetreten. Bitte versuche es später erneut."; /* No comment provided by engineer. */ "An error occurred while launching the installer. Please try again later." = "Beim Starten des Installationsprogramms ist ein Fehler aufgetreten. Bitte versuche es später erneut."; /* No comment provided by engineer. */ "An error occurred while parsing the update feed." = "Beim Lesen der Updateinformationen ist ein Fehler aufgetreten."; /* No comment provided by engineer. */ "An error occurred while running the updater. Please try again later." = "Beim Ausführen des Aktualisierungsprogramms ist ein Fehler aufgetreten. Bitte versuche es später erneut."; /* No comment provided by engineer. */ "An error occurred while starting the installer. Please try again later." = "Beim Starten des Installationsprogramms ist ein Fehler aufgetreten. Bitte versuche es später erneut."; /* No comment provided by engineer. */ "An important update to %@ is ready to install" = "Ein wichtiges Update von %@ steht zur Installation bereit"; /* No comment provided by engineer. */ "Application Name" = "App-Name"; /* No comment provided by engineer. */ "Application Version" = "App-Version"; /* No comment provided by engineer. */ "Cancel" = "Abbrechen"; /* No comment provided by engineer. */ "Cancel Update" = "Aktualisierung abbrechen"; /* No comment provided by engineer. */ "Checking for updates…" = "Nach Updates suchen …"; /* No comment provided by engineer. */ "CPU is 64-Bit?" = "CPU ist 64-Bit?"; /* No comment provided by engineer. */ "CPU Speed (MHz)" = "CPU-Geschwindigkeit (MHz)"; /* No comment provided by engineer. */ "CPU Subtype" = "CPU-Untertyp"; /* No comment provided by engineer. */ "CPU Type" = "CPU-Typ"; /* Take care not to overflow the status window. */ "Downloading update…" = "Update laden …"; /* Take care not to overflow the status window. */ "Extracting update…" = "Update entpacken …"; /* No comment provided by engineer. */ "Failed to resume installing update." = "Das Fortsetzen der Installation ist fehlgeschlagen."; /* No comment provided by engineer. */ "Install and Relaunch" = "Installieren und App neu starten"; /* Alternate title for 'Remind Me Later' button when downloaded updates can be resumed */ "Install on Quit" = "Installieren beim Beenden"; /* Take care not to overflow the status window. */ "Installing update…" = "Update installieren …"; /* Alternate title for 'Install Update' button when there's no download in RSS feed. */ "Learn More…" = "Mehr erfahren …"; /* No comment provided by engineer. */ "Mac Model" = "Mac-Modell"; /* No comment provided by engineer. */ "Memory (MB)" = "Speicher (MB)"; /* No comment provided by engineer. */ "No" = "Nein"; /* No comment provided by engineer. */ "Number of CPUs" = "Anzahl der CPUs"; /* No comment provided by engineer. */ "OK" = "OK"; /* No comment provided by engineer. */ "OS Version" = "OS-Version"; /* No comment provided by engineer. */ "Preferred Language" = "Bevorzugte Sprache"; /* No comment provided by engineer. */ "Quit %1$@, move it into your Applications folder, relaunch it from there and try again." = "Beende „%1$@“, bewege es in den Ordner „Programme“, starte das Programm von dort erneut und versuche es noch einmal."; /* No comment provided by engineer. */ "Ready to Install" = "Bereit zum Installieren"; /* No comment provided by engineer. */ "Should %1$@ automatically check for updates? You can always check for updates manually from the %1$@ menu." = "Soll %1$@ automatisch nach Updates suchen? Du kannst im %1$@-Menü auch manuell nach Updates suchen."; /* No comment provided by engineer. */ "The installation failed due to not having permission to write the new update." = "Die Installation ist fehlgeschlagen aufgrund fehlender Berechtigung zum Schreiben des neuen Updates."; /* No comment provided by engineer. */ "The update is improperly signed and could not be validated. Please try again later or contact the app developer." = "Das Update ist nicht ordnungsgemäß signiert und konnte nicht überprüft werden. Bitte versuche es später erneut oder kontaktiere den App-Entwickler."; /* No comment provided by engineer. */ "Unable to Check For Updates" = "Suche nach Updates ist fehlgeschlagen"; /* No comment provided by engineer. */ "Update Error!" = "Fehler beim Aktualisieren!"; /* No comment provided by engineer. */ "Update Installed" = "Update installiert"; /* No comment provided by engineer. */ "Updating %@" = "Aktualisierung von %@"; /* No comment provided by engineer. */ "Use Finder to copy %1$@ to the Applications folder, relaunch it from there, and try again." = "Kopiere %1$@ in den Programmeordner, starte es von dort aus und versuche es erneut zu aktualisieren."; /* No comment provided by engineer. */ "Version History" = "Versionsverlauf"; /* No comment provided by engineer. */ "Yes" = "Ja"; /* No comment provided by engineer. */ "You may need to allow modifications from %1$@ in System Settings under Privacy & Security and App Management to install future updates." = "Du musst möglicherweise in den Systemeinstellungen unter „Datenschutz & Sicherheit“ und „App-Verwaltung“ Änderungen von %1$@ erlauben, um künftige Updates zu installieren."; /* Status message shown when the user checks for updates but is already current or the feed doesn't contain any updates. */ "You’re up to date!" = "Du bist auf dem neuesten Stand!"; /* No comment provided by engineer. */ "Your macOS version is too new" = "Deine Version von macOS ist zu neu"; /* No comment provided by engineer. */ "Your macOS version is too old" = "Deine Version von macOS ist zu alt"; /* Software Update title/label */ "Software Update" = "Softwareupdate"; /* Remind Me Later choice for update alert */ "Remind Me Later" = "Später erinnern"; /* Skip This Version choice for update alert */ "Skip This Version" = "Diese Version überspringen"; /* Install Update choice for update alert */ "Install Update" = "Installieren"; /* Automatically download and install updates in the future option for update alert */ "Automatically download and install updates in the future" = "Updates in Zukunft automatisch laden und installieren"; /* Check for updates automatically response for update permission dialog */ "Check Automatically" = "Automatisch suchen"; /* Don't check for updates automatically response for update permission dialog */ "Don’t Check" = "Nicht suchen"; /* Title question for update permission dialog */ "Check for updates automatically?" = "Automatisch nach Updates suchen?"; /* Include anonymous system profile choice for update permission dialog */ "Include anonymous system profile" = "Anonymisiertes Systemprofil übertragen"; /* Automatically download and install updates choice for update permission dialog */ "Automatically download and install updates" = "Updates automatisch laden und installieren"; /* Anonymous system profile information disclosure for update permission dialog */ "Anonymous system profile information is used to help us plan future development work. Please contact us if you have any questions about this.\n\nThis is the information that would be sent:" = "Das anonymisierte Systemprofil unterstützt uns bei der zukünftigen Entwicklung. Bitte kontaktiere uns, wenn du Fragen hierzu hast.\n\nDiese Informationen würden an uns gesendet werden:"; ================================================ FILE: Sparkle/el.lproj/Sparkle.strings ================================================ /* No comment provided by engineer. */ "%@ %@ is currently the newest version available." = "Το %1$@ %2$@ είναι η τελευταία διαθέσιμη έκδοση."; /* No comment provided by engineer. */ "%@ %@ is currently the newest version available.\n(You are currently running version %@.)" = "Το %1$@ %2$@ είναι η τελευταία διαθέσιμη έκδοση.\n(Η παρούσα εκδοσή σας είναι η %3$@.)"; /* Description text for SUUpdateAlert when the update is downloadable. */ "%@ %@ is now available—you have %@. Would you like to download it now?" = "Το %1$@ %2$@ είναι η νέα έκδοση--έχετε την %3$@. Θέλετε να την κατεβάσετε τώρα;"; /* The download progress in a unit of bytes, e.g. 100 KB */ "%@ downloaded" = "%@ λήφθησαν"; /* The download progress in units of bytes, e.g. 100 KB of 1,0 MB */ "%@ of %@" = "%1$@ από %2$@"; /* Description text for SUUpdateAlert when the update has already been downloaded and ready to install. */ "%1$@ %2$@ has been downloaded and is ready to use! Would you like to install it and relaunch %1$@ now?" = "Το %1$@ %2$@ κατέβηκε. Είναι έτοιμο προς εγκατάσταση! Θέλετε να εγκαταστήσετε και να επανεκκινήσετε το %1$@ τώρα;"; /* No comment provided by engineer. */ "%1$@ can’t be updated because it was opened from a read-only or a temporary location." = "Το %1$@ δεν μπορεί να ενημερωθεί όσο τρέχει από έναν δίσκο ανάγνωσης-μόνο ή έναν οπτικό δίσκο."; /* No comment provided by engineer. */ "A new version of %@ is available!" = "Μία νέα έκδοση του %@ είναι διαθέσιμη!"; /* No comment provided by engineer. */ "A new version of %@ is ready to install!" = "Μία νέα έκδοση του %@ είναι έτοιμη για εγκατάσταση!"; /* No comment provided by engineer. */ "An error occurred in retrieving update information. Please try again later." = "Υπήρξε ένα σφάλμα κατά την ανάκτηση των πληροφοριών ενημέρωσης. Παρακαλώ δοκιμάστε αργότερα."; /* No comment provided by engineer. */ "An error occurred while downloading the update. Please try again later." = "Υπήρξε ένα σφάλμα κατά την λήψη της ενημέρωσης. Παρακαλώ δοκιμάστε αργότερα."; /* No comment provided by engineer. */ "An error occurred while extracting the archive. Please try again later." = "Υπήρξε ένα σφάλμα κατά την εξαγωγή του αρχείου. Παρακαλώ δοκιμάστε αργότερα."; /* No comment provided by engineer. */ "An error occurred while parsing the update feed." = "Υπήρξε ένα σφάλμα κατά την ανάλυση του feed ενημερώσεων."; /* No comment provided by engineer. */ "Cancel" = "Ακύρωση"; /* No comment provided by engineer. */ "Cancel Update" = "Ακύρωση Ενημέρωσης"; /* No comment provided by engineer. */ "Checking for updates…" = "Έλεγχος για ενημερώσεις…"; /* Take care not to overflow the status window. */ "Downloading update…" = "Λήψη ενημέρωσης…"; /* Take care not to overflow the status window. */ "Extracting update…" = "Εξαγωγή ενημέρωσης…"; /* No comment provided by engineer. */ "Install and Relaunch" = "Εγκατάσταση και Επανεκκίνηση"; /* Take care not to overflow the status window. */ "Installing update…" = "Εγκατάσταση ενημέρωσης…"; /* No comment provided by engineer. */ "OK" = "OK"; /* No comment provided by engineer. */ "Ready to Install" = "Έτοιμο προς Εγκατάσταση"; /* No comment provided by engineer. */ "Should %1$@ automatically check for updates? You can always check for updates manually from the %1$@ menu." = "Θέλετε το %1$@ να ελέγχει αυτόματα για ενημερώσεις; Μπορείτε να ελέγχετε πάντα για επιθυμητές ενημερώσεις από το μενού %1$@."; /* No comment provided by engineer. */ "Update Error!" = "Σφάλμα Ενημέρωσης!"; /* No comment provided by engineer. */ "Updating %@" = "Ενημέρωση %@"; /* No comment provided by engineer. */ "Use Finder to copy %1$@ to the Applications folder, relaunch it from there, and try again." = "Αντιγράψτε το %1$@ στον φάκελο εφαρμογών συστήματος, επανεκκινήστε το από εκεί και ξαναδοκιμάστε."; /* Status message shown when the user checks for updates but is already current or the feed doesn't contain any updates. */ "You’re up to date!" = "Είστε ενημερωμένοι!"; /* Software Update title/label */ "Software Update" = "Ενημέρωση προγράμματος"; /* Remind Me Later choice for update alert */ "Remind Me Later" = "Υπενθύμιση Αργότερα"; /* Skip This Version choice for update alert */ "Skip This Version" = "Παράλειψη Έκδοσης"; /* Install Update choice for update alert */ "Install Update" = "Εγκατάσταση Ενημέρωσης"; /* Automatically download and install updates in the future option for update alert */ "Automatically download and install updates in the future" = "Αυτόματη λήψη και εγκατάσταση ενημερώσεων στο μέλλον"; /* Check for updates automatically response for update permission dialog */ "Check Automatically" = "Αυτόματος Ελεγχος"; /* Don't check for updates automatically response for update permission dialog */ "Don’t Check" = "Κανένας έλεγχος"; /* Title question for update permission dialog */ "Check for updates automatically?" = "Αυτόματος έλεγχος για ενημερώσεις;"; /* Include anonymous system profile choice for update permission dialog */ "Include anonymous system profile" = "Συμπερίληψη του ανώνυμου προφίλ του συστήματός σας"; /* Automatically download and install updates choice for update permission dialog */ "Automatically download and install updates" = "Αυτόματη λήψη και εγκατάσταση ενημερώσεων"; /* Anonymous system profile information disclosure for update permission dialog */ "Anonymous system profile information is used to help us plan future development work. Please contact us if you have any questions about this.\n\nThis is the information that would be sent:" = "Οι ανώνυμες πληροφορίες του προφίλ του συστήματός σας, μας βοηθούν στο σχεδιασμό της μελλοντικής ανάπτυξης του προγράμματος. Παρακαλώ επικοινωνήστε μαζί μας άν έχετε ερωτήσεις.\n\nΑυτές είναι οι πληροφορίες που θα σταλούν σε εμάς:"; ================================================ FILE: Sparkle/es.lproj/Sparkle.strings ================================================ /* No comment provided by engineer. */ "%@ %@ is currently the newest version available." = "%1$@ %2$@ es la versión más nueva disponible."; /* No comment provided by engineer. */ "%@ %@ is currently the newest version available.\n(You are currently running version %@.)" = "%1$@ %2$@ es la versión más nueva disponible.\n(Actualmente estás corriendo la versión %3$@.)"; /* Description text for SUUpdateAlert when the update is downloadable. */ "%@ %@ is now available—you have %@. Would you like to download it now?" = "%1$@ %2$@ ya está disponible (tienes la %3$@). ¿Quisieras descargarla ahora?"; /* Description text for SUUpdateAlert when the update informational with no download. */ "%@ %@ is now available—you have %@. Would you like to learn more about this update on the web?" = "%1$@ %2$@ ya está disponible--tú tienes la %3$@. ¿Quisieras aprender más acerca de esta actualización en la red?"; /* The download progress in a unit of bytes, e.g. 100 KB */ "%@ downloaded" = "%@ descargado"; /* The download progress in units of bytes, e.g. 100 KB of 1,0 MB */ "%@ of %@" = "%1$@ de %2$@"; /* Description text for SUUpdateAlert when the critical update has already been downloaded and ready to install. */ "%1$@ %2$@ has been downloaded and is ready to use! This is an important update; would you like to install it and relaunch %1$@ now?" = "¡%1$@ %2$@ se ha descargado y está lista para usarse! Ésta es una actualización importante. ¿Te gustaría instalarla y volver a abrir %1$@ ahora?"; /* Description text for SUUpdateAlert when the update has already been downloaded and ready to install. */ "%1$@ %2$@ has been downloaded and is ready to use! Would you like to install it and relaunch %1$@ now?" = "¡%1$@ %2$@ se ha descargado y está lista para usarse! ¿Te gustaría instalarla y volver a abrir %1$@ ahora?"; /* No comment provided by engineer. */ "%1$@ can’t be updated because it was opened from a read-only or a temporary location." = "%1$@ no se puede actualizar porque fue abierta desde una ubicación temporal o de solo lectura."; /* No comment provided by engineer. */ "A new version of %@ is available!" = "¡Una nueva versión de %@ está disponible!"; /* No comment provided by engineer. */ "A new version of %@ is ready to install!" = "¡Una nueva versión de %@ está lista para instalarse!"; /* No comment provided by engineer. */ "An error occurred in retrieving update information. Please try again later." = "Ocurrió un error al recopilar información sobre la actualización. Por favor inténtalo de nuevo más tarde."; /* No comment provided by engineer. */ "An error occurred while downloading the update. Please try again later." = "Ocurrió un error al descargar la actualización. Por favor inténtalo de nuevo más tarde."; /* No comment provided by engineer. */ "An error occurred while extracting the archive. Please try again later." = "Ocurrió un error al extraer el archivo. Por favor inténtalo de nuevo más tarde."; /* No comment provided by engineer. */ "An error occurred while parsing the update feed." = "Ocurrió un error al analizar la información de la actualización."; /* No comment provided by engineer. */ "An important update to %@ is ready to install" = "Una actualización importante para %@ está lista para instalarse"; /* No comment provided by engineer. */ "Cancel" = "Cancelar"; /* No comment provided by engineer. */ "Cancel Update" = "Cancelar actualización"; /* No comment provided by engineer. */ "Checking for updates…" = "Buscando actualizaciones…"; /* Take care not to overflow the status window. */ "Downloading update…" = "Descargando actualización…"; /* Take care not to overflow the status window. */ "Extracting update…" = "Extrayendo actualización…"; /* No comment provided by engineer. */ "Install and Relaunch" = "Instalar y volver a abrir"; /* Take care not to overflow the status window. */ "Installing update…" = "Instalando actualización…"; /* Alternate title for 'Install Update' button when there's no download in RSS feed. */ "Learn More…" = "Más información…"; /* No comment provided by engineer. */ "OK" = "Aceptar"; /* No comment provided by engineer. */ "Ready to Install" = "Listo para instalarse"; /* No comment provided by engineer. */ "Should %1$@ automatically check for updates? You can always check for updates manually from the %1$@ menu." = "¿Debería %1$@ buscar actualizaciones automáticamente? Siempre puedes buscar actualizaciones manualmente desde el menú %1$@."; /* No comment provided by engineer. */ "Update Error!" = "¡Error de actualización!"; /* No comment provided by engineer. */ "Updating %@" = "Actualizando %@"; /* No comment provided by engineer. */ "Use Finder to copy %1$@ to the Applications folder, relaunch it from there, and try again." = "Usa Finder para copiar %1$@ a la carpeta Aplicaciones, vuélvela a abrir desde ahí e inténtalo de nuevo."; /* No comment provided by engineer. */ "Version History" = "Historial de versiones"; /* Status message shown when the user checks for updates but is already current or the feed doesn't contain any updates. */ "You’re up to date!" = "¡Está actualizado!"; /* Software Update title/label */ "Software Update" = "Actualización de software"; /* Remind Me Later choice for update alert */ "Remind Me Later" = "Recordármelo"; /* Skip This Version choice for update alert */ "Skip This Version" = "No instalar esta versión"; /* Install Update choice for update alert */ "Install Update" = "Instalar actualización"; /* Automatically download and install updates in the future option for update alert */ "Automatically download and install updates in the future" = "Descargar e instalar actualizaciones automáticamente"; /* Check for updates automatically response for update permission dialog */ "Check Automatically" = "Comprobar automáticamente"; /* Don't check for updates automatically response for update permission dialog */ "Don’t Check" = "No comprobar"; /* Title question for update permission dialog */ "Check for updates automatically?" = "¿Comprobar si hay actualizaciones automáticamente?"; /* Include anonymous system profile choice for update permission dialog */ "Include anonymous system profile" = "Incluir perfil de sistema anónimo"; /* Automatically download and install updates choice for update permission dialog */ "Automatically download and install updates" = "Descargar e instalar actualizaciones automáticamente"; /* Anonymous system profile information disclosure for update permission dialog */ "Anonymous system profile information is used to help us plan future development work. Please contact us if you have any questions about this.\n\nThis is the information that would be sent:" = "La información de perfil de sistema anónimo se usa para ayudarnos a planear el trabajo de desarrollo futuro. Por favor, póngase en contacto con nosotros si tiene preguntas sobre esto.\n\nEsta es la información que nos enviaría:"; ================================================ FILE: Sparkle/fa.lproj/Sparkle.strings ================================================ /* No comment provided by engineer. */ "%@ %@ is currently the newest version available." = "%1$@ %2$@ در حال حاضر جدیدترین نسخهٔ موجود است"; /* No comment provided by engineer. */ "%@ %@ is currently the newest version available.\n(You are currently running version %@.)" = "%1$@ %2$@ در حال حاضر جدیدترین نسخه است.\n(شما در حال اجرای نسخهٔ %3$@ هستید.)"; /* Description text for SUUpdateAlert when the update is downloadable. */ "%@ %@ is now available—you have %@. Would you like to download it now?" = "%1$@ %2$@ اکنون در دسترس است-- نسخهٔ شما: %3$@. اکنون می‌خواهید آن را دریافت کنید؟"; /* Description text for SUUpdateAlert when the update informational with no download. */ "%@ %@ is now available—you have %@. Would you like to learn more about this update on the web?" = "%1$@ %2$@ اکنون در دسترس است-- نسخهٔ شما: %3$@. می‌خواهید در مورد آن بیشتر یاد بگیرید؟"; /* The download progress in a unit of bytes, e.g. 100 KB */ "%@ downloaded" = "دانلود شده است %@"; /* The download progress in units of bytes, e.g. 100 KB of 1,0 MB */ "%@ of %@" = "%1$@ از %2$@"; /* Description text for SUUpdateAlert when the critical update has already been downloaded and ready to install. */ "%1$@ %2$@ has been downloaded and is ready to use! This is an important update; would you like to install it and relaunch %1$@ now?" = "%1$@ %2$@ بارگیری شده و آمادهٔ استفاده است! این به‌روزرسانی مهمی است، آیا اکنون مایل به نصب و بازکردن دوبارهٔ %1$@ هستید؟"; /* Description text for SUUpdateAlert when the update has already been downloaded and ready to install. */ "%1$@ %2$@ has been downloaded and is ready to use! Would you like to install it and relaunch %1$@ now?" = "%1$@ %2$@ بارگیری شده و آمادهٔ استفاده است! آیا اکنون مایل به نصب و بازکردن دوبارهٔ %1$@ هستید؟"; /* No comment provided by engineer. */ "%1$@ can’t be updated if it’s running from the location it was downloaded to." = "%1$@ نمی‌تواند به‌روزرسانی شود، اگر از محلی که در آن بارگیری شده باز شود"; /* No comment provided by engineer. */ "%1$@ can’t be updated because it was opened from a read-only or a temporary location." = "%1$@ نمی‌تواند به‌روزرسانی شود، زیرا از محلی با سطح دسترسی فقط-خواندن یا موقت باز شده است"; /* No comment provided by engineer. */ "A new version of %@ is available!" = "نسخه‌ای جدید از %@ در دسترس است"; /* No comment provided by engineer. */ "A new version of %@ is ready to install!" = "نسخه‌ای جدید از %@ برای نصب آماده است!"; /* No comment provided by engineer. */ "An error occurred in retrieving update information. Please try again later." = "خطایی هنگام دستیابی به اطلاعات به‌روزرسانی رخ داد. لطفاً دوباره امتحان کنید."; /* No comment provided by engineer. */ "An error occurred while downloading the update. Please try again later." = "خطایی در هنگام بارگیری به‌روزرسانی رخ داد. لطفاً دوباره امتحان کنید."; /* No comment provided by engineer. */ "An error occurred while extracting the archive. Please try again later." = "خطایی در بازکردن آرشیو رخ داد. لطفاً دوباره امتحان کنید. "; /* No comment provided by engineer. */ "An error occurred while parsing the update feed." = "خطایی در بررسی به‌روزرسانی رخ داد"; /* No comment provided by engineer. */ "An important update to %@ is ready to install" = "یک به‌روزرسانی مهم آمادهٔ نصب است"; /* No comment provided by engineer. */ "Cancel" = "لغو"; /* No comment provided by engineer. */ "Cancel Update" = "لغو به‌روزرسانی"; /* No comment provided by engineer. */ "Checking for updates…" = "بررسی وجود به‌روزرسانی"; /* Take care not to overflow the status window. */ "Downloading update…" = "در حال بارگیری به‌روزرسانی"; /* Take care not to overflow the status window. */ "Extracting update…" = "در حال بازکردن بستهٔ به‌روزرسانی"; /* No comment provided by engineer. */ "Install and Relaunch" = "نصب و بازکردن دوباره"; /* Take care not to overflow the status window. */ "Installing update…" = "در حال نصب به‌روزرسانی"; /* Alternate title for 'Install Update' button when there's no download in RSS feed. */ "Learn More…" = "یادگیری بیشتر…"; /* No comment provided by engineer. */ "OK" = "پذیرفتن"; /* No comment provided by engineer. */ "Quit %1$@, move it into your Applications folder, relaunch it from there and try again." = "از %1$@ خارج شوید آن را به پوشهٔ برنامه انتقال دهید سپس آن را دوباره باز کرده و دوباره امتحان کنید."; /* No comment provided by engineer. */ "Ready to Install" = "آماده برای نصب"; /* No comment provided by engineer. */ "Should %1$@ automatically check for updates? You can always check for updates manually from the %1$@ menu." = "آیا می‌خواهید بررسی به‌روزرسانی‌ها %1$@ به صورت خودکار انجام شود؟ همیشه می‌توانید به‌روزرسانی‌های %1$@ را به صورت دستی بررسی کنید."; /* No comment provided by engineer. */ "Update Error!" = "خطای به‌روزرسانی!"; /* No comment provided by engineer. */ "Updating %@" = "%@ در حال به‌روزرسانی"; /* No comment provided by engineer. */ "Use Finder to copy %1$@ to the Applications folder, relaunch it from there, and try again." = "از Finder برای کپی کردن %1$@ به پوشهٔ برنامه استفاده کنید، سپس %1$@ را دوباره باز کرده و مجدداً امتحان کنید."; /* Status message shown when the user checks for updates but is already current or the feed doesn't contain any updates. */ "You’re up to date!" = "برنامهٔ شما به‌روز است"; ================================================ FILE: Sparkle/fi.lproj/Sparkle.strings ================================================ /* No comment provided by engineer. */ "%@ %@ is currently the newest version available." = "%1$@ %2$@ on uusin saatavilla oleva versio."; /* No comment provided by engineer. */ "%@ %@ is currently the newest version available.\n(You are currently running version %@.)" = "%1$@ %2$@ on uusin saatavilla oleva versio.\n(Asennettu versio on %3$@.)"; /* Description text for SUUpdateAlert when the update is downloadable. */ "%@ %@ is now available—you have %@. Would you like to download it now?" = "%1$@ %2$@ on nyt saatavilla (nykyinen versiosi on %3$@). Haluatko ladata päivityksen nyt?"; /* The download progress in units of bytes, e.g. 100 KB of 1,0 MB */ "%@ of %@" = "%1$@ / %2$@"; /* No comment provided by engineer. */ "A new version of %@ is available!" = "Uusi versio ohjelmasta %@ on saatavilla!"; /* No comment provided by engineer. */ "An error occurred in retrieving update information. Please try again later." = "Päivitystietojen haussa tapahtui virhe. Yritä myöhemmin uudelleen."; /* No comment provided by engineer. */ "An error occurred while extracting the archive. Please try again later." = "Pakkauksen purkamisessa tapahtui virhe. Yritä myöhemmin uudelleen."; /* No comment provided by engineer. */ "Cancel" = "Kumoa"; /* No comment provided by engineer. */ "Cancel Update" = "Peruuta päivitykset"; /* No comment provided by engineer. */ "Checking for updates…" = "Tarkistetaan päivityksiä…"; /* Take care not to overflow the status window. */ "Downloading update…" = "Haetaan päivitystä…"; /* Take care not to overflow the status window. */ "Extracting update…" = "Puretaan päivitystä…"; /* No comment provided by engineer. */ "Install and Relaunch" = "Asenna ja käynnistä ohjelma uudelleen"; /* Take care not to overflow the status window. */ "Installing update…" = "Päivitys asennetaan…"; /* No comment provided by engineer. */ "OK" = "OK"; /* No comment provided by engineer. */ "Ready to Install" = "Valmiina asentamaan"; /* No comment provided by engineer. */ "Update Error!" = "Virhe päivityksessä"; /* No comment provided by engineer. */ "Updating %@" = "Päivitetään %@"; /* Status message shown when the user checks for updates but is already current or the feed doesn't contain any updates. */ "You’re up to date!" = "Sinulla on viimeisin versio!"; /* Software Update title/label */ "Software Update" = "Ohjelmiston pävitys"; /* Remind Me Later choice for update alert */ "Remind Me Later" = "Muistuta myöhemmin"; /* Skip This Version choice for update alert */ "Skip This Version" = "Ohita tämä versio"; /* Install Update choice for update alert */ "Install Update" = "Asenna päivitys"; /* Automatically download and install updates in the future option for update alert */ "Automatically download and install updates in the future" = "Hae ja asenna päivitykset jatkossa automaattisesti"; /* Check for updates automatically response for update permission dialog */ "Check Automatically" = "Tarkista automaattisesti"; /* Don't check for updates automatically response for update permission dialog */ "Don’t Check" = "Älä tarkista"; /* Title question for update permission dialog */ "Check for updates automatically?" = "Tarkista päivitykset käynnistyksen yhteydessä?"; /* Include anonymous system profile choice for update permission dialog */ "Include anonymous system profile" = "Sisällytä nimetön järjestelmäprofiili"; /* Automatically download and install updates choice for update permission dialog */ "Automatically download and install updates" = "Hae ja asenna päivitykset automaattisesti"; /* Anonymous system profile information disclosure for update permission dialog */ "Anonymous system profile information is used to help us plan future development work. Please contact us if you have any questions about this.\n\nThis is the information that would be sent:" = "Anonymous system profile information is used to help us plan future development work. Please contact us if you have any questions about this.\n\nThis is the information that would be sent:"; ================================================ FILE: Sparkle/fr.lproj/Sparkle.strings ================================================ /* No comment provided by engineer. */ "%@ %@ is currently the newest version available." = "%1$@ %2$@ est la version la plus récente disponible."; /* No comment provided by engineer. */ "%@ %@ is currently the newest version available.\n(You are currently running version %@.)" = "%1$@ %2$@ est la version la plus récente disponible.\n(Vous utilisez actuellement la version %3$@)"; /* Description text for SUUpdateAlert when the update is downloadable. */ "%@ %@ is now available—you have %@. Would you like to download it now?" = "%1$@ %2$@ est disponible ; vous utilisez la version %3$@. Voulez-vous le télécharger maintenant ?"; /* Description text for SUUpdateAlert when the update informational with no download. */ "%@ %@ is now available—you have %@. Would you like to learn more about this update on the web?" = "%1$@ %2$@ est disponible ; vous utilisez la version %3$@. Voulez-vous en savoir plus sur cette mise à jour sur Internet ?"; /* The download progress in a unit of bytes, e.g. 100 KB */ "%@ downloaded" = "%@ téléchargé"; /* The download progress in units of bytes, e.g. 100 KB of 1,0 MB */ "%@ of %@" = "%1$@ sur %2$@"; /* Description text for SUUpdateAlert when the critical update has already been downloaded and ready to install. */ "%1$@ %2$@ has been downloaded and is ready to use! This is an important update; would you like to install it and relaunch %1$@ now?" = "%1$@ %2$@ a été téléchargé. C’est une mise à jour importante ; voulez-vous l’installer et relancer %1$@ maintenant ?"; /* Description text for SUUpdateAlert when the update has already been downloaded and ready to install. */ "%1$@ %2$@ has been downloaded and is ready to use! Would you like to install it and relaunch %1$@ now?" = "%1$@ %2$@ a été téléchargé. Voulez-vous l’installer et relancer %1$@ maintenant ?"; /* No comment provided by engineer. */ "%1$@ can’t be updated if it’s running from the location it was downloaded to." = "%1$@ ne peut pas être mis à jour s’il est exécuté depuis le dossier dans lequel il a été téléchargé."; /* No comment provided by engineer. */ "%1$@ can’t be updated because it was opened from a read-only or a temporary location." = "%1$@ ne peut pas être mis à jour quand il fonctionne à partir d’un volume en lecture seule, comme une image disque ou un lecteur optique."; /* No comment provided by engineer. */ "A new version of %@ is available!" = "Une nouvelle version de %@ est disponible !"; /* No comment provided by engineer. */ "A new version of %@ is ready to install!" = "Une nouvelle version de %@ est prête à être installée !"; /* No comment provided by engineer. */ "An error occurred in retrieving update information. Please try again later." = "Une erreur est survenue en récupérant les informations de mise à jour. Veuillez réessayer plus tard."; /* No comment provided by engineer. */ "An error occurred while downloading the update. Please try again later." = "Une erreur est survenue pendant le téléchargement de la mise à jour. Veuillez réessayer plus tard."; /* No comment provided by engineer. */ "An error occurred while extracting the archive. Please try again later." = "Une erreur est survenue pendant l’extraction des données de l’archive. Veuillez réessayer plus tard."; /* No comment provided by engineer. */ "An error occurred while parsing the update feed." = "Une erreur est survenue pendant l’analyse de la mise à jour."; /* No comment provided by engineer. */ "An important update to %@ is ready to install" = "Une mise à jour importante de %@ est prête à être installée"; /* No comment provided by engineer. */ "Cancel" = "Annuler"; /* No comment provided by engineer. */ "Cancel Update" = "Annuler la mise à jour"; /* No comment provided by engineer. */ "Checking for updates…" = "Recherche de mises à jour…"; /* Take care not to overflow the status window. */ "Downloading update…" = "Téléchargement de la mise à jour…"; /* Take care not to overflow the status window. */ "Extracting update…" = "Extraction de la mise à jour…"; /* No comment provided by engineer. */ "Install and Relaunch" = "Installer et relancer"; /* Take care not to overflow the status window. */ "Installing update…" = "Installation de la mise à jour…"; /* Alternate title for 'Install Update' button when there's no download in RSS feed. */ "Learn More…" = "En savoir plus…"; /* No comment provided by engineer. */ "OK" = "OK"; /* No comment provided by engineer. */ "Quit %1$@, move it into your Applications folder, relaunch it from there and try again." = "Quittez %1$@ et déplacez-le dans le dossier Applications, relancez-le à partir de ce dossier et essayez à nouveau."; /* No comment provided by engineer. */ "Ready to Install" = "Prêt pour l’installation"; /* No comment provided by engineer. */ "Should %1$@ automatically check for updates? You can always check for updates manually from the %1$@ menu." = "%1$@ doit-il rechercher automatiquement les mises à jour ? La mise à jour est toujours possible manuellement depuis le menu %1$@."; /* No comment provided by engineer. */ "Update Error!" = "Erreur pendant la mise à jour !"; /* No comment provided by engineer. */ "Updating %@" = "Mise à jour de %@"; /* No comment provided by engineer. */ "Use Finder to copy %1$@ to the Applications folder, relaunch it from there, and try again." = "Déplacez %1$@ dans votre dossier Applications, relancez-le à partir de là et réessayez."; /* No comment provided by engineer. */ "Version History" = "Historique des versions"; /* Status message shown when the user checks for updates but is already current or the feed doesn't contain any updates. */ "You’re up to date!" = "Votre logiciel est à jour !"; /* Software Update title/label */ "Software Update" = "Mise à jour logiciel"; /* Remind Me Later choice for update alert */ "Remind Me Later" = "Pas maintenant"; /* Skip This Version choice for update alert */ "Skip This Version" = "Ignorer cette version"; /* Install Update choice for update alert */ "Install Update" = "Installer"; /* Automatically download and install updates in the future option for update alert */ "Automatically download and install updates in the future" = "Télécharger et installer automatiquement les mises à jour"; /* Check for updates automatically response for update permission dialog */ "Check Automatically" = "Vérifier automatiquement"; /* Don't check for updates automatically response for update permission dialog */ "Don’t Check" = "Ne pas vérifier"; /* Title question for update permission dialog */ "Check for updates automatically?" = "Rechercher automatiquement les mises à jour ?"; /* Include anonymous system profile choice for update permission dialog */ "Include anonymous system profile" = "Avec transmission anonyme de mon profil système"; /* Automatically download and install updates choice for update permission dialog */ "Automatically download and install updates" = "Télécharger et installer automatiquement les mises à jour"; /* Anonymous system profile information disclosure for update permission dialog */ "Anonymous system profile information is used to help us plan future development work. Please contact us if you have any questions about this.\n\nThis is the information that would be sent:" = "Les informations anonymes de profil système nous aident à planifier les futurs développements. Contactez-nous pour toute question à ce sujet.\n\nCi-dessous figurent les informations qui seront transmises :"; ================================================ FILE: Sparkle/he.lproj/Sparkle.strings ================================================ /* Description text for SUUpdateAlert when the critical update has already been downloaded and ready to install. */ "%1$@ %2$@ has been downloaded and is ready to use! This is an important update; would you like to install it and relaunch %1$@ now?" = "%1$@ %2$@ הורד והוא מוכן לשימוש! זהו עדכון חשוב; האם ברצונך להתקין אותו ולהפעיל מחדש את %1$@ כעת?"; /* Description text for SUUpdateAlert when the update has already been downloaded and ready to install. */ "%1$@ %2$@ has been downloaded and is ready to use! Would you like to install it and relaunch %1$@ now?" = "%1$@ %2$@ הורד והוא מוכן לשימוש! האם ברצונך להתקין אותו ולהפעיל מחדש את %1$@ כעת?"; /* No comment provided by engineer. */ "%1$@ %2$@ is available but your macOS version is too new for this update. This update only supports up to macOS %3$@." = "%1$@ %2$@ זמין, אך גרסת ה-macOS שלך חדשה מדי עבור עדכון זה. עדכון זה תומך רק עד macOS %3$@."; /* No comment provided by engineer. */ "%1$@ %2$@ is available but your macOS version is too old to install it. At least macOS %3$@ is required." = "%1$@ %2$@ זמין אך גרסת ה-macOS שלך ישנה מדי. נדרשת macOS %3$@ לפחות."; /* No comment provided by engineer. */ "%1$@ can’t be updated if it’s running from the location it was downloaded to." = "לא ניתן לעדכן את %1$@ כשהוא פועל מהמיקום אליו הוא הורד."; /* No comment provided by engineer. */ "%1$@ can’t be updated because it was opened from a read-only or a temporary location." = "לא ניתן לעדכן את %1$@ מכיוון שהוא נפתח ממיקום זמני או לקריאה בלבד."; /* No comment provided by engineer. */ "%@ %@ is currently the newest version available." = "%@ %@ היא הגרסה החדשה ביותר שזמינה כרגע."; /* No comment provided by engineer. */ "%@ %@ is currently the newest version available.\n(You are currently running version %@.)" = "%@ %@ היא הגרסה החדשה ביותר שזמינה.\n(אתה מפעיל כעת את גרסה %@.)"; /* Description text for SUUpdateAlert when the critical update is downloadable. */ "%@ %@ is now available—you have %@. This is an important update; would you like to download it now?" = "%@ %@ זמין - אתה משתמש ב-%@. זהו עדכון חשוב; האם ברצונך להוריד אותו כעת?"; /* Description text for SUUpdateAlert when the update is downloadable. */ "%@ %@ is now available—you have %@. Would you like to download it now?" = "%@ %@ זמין - אתה משתמש ב-%@. האם ברצונך להוריד אותו כעת?"; /* Description text for SUUpdateAlert when the update informational with no download. */ "%@ %@ is now available—you have %@. Would you like to learn more about this update on the web?" = "%@ %@ זמין - אתה משתמש ב-%@. האם ברצונך ללמוד עוד על עדכון זה?"; /* The download progress in a unit of bytes, e.g. 100 KB */ "%@ downloaded" = "%@ הורדו"; /* No comment provided by engineer. */ "%@ is now updated to version %@!" = "%@ מעודכן כעת לגרסה %@!"; /* No comment provided by engineer. */ "%@ is now updated!" = "%@ מעודכן כעת!"; /* The download progress in units of bytes, e.g. 100 KB of 1,0 MB */ "%@ of %@" = "%@ מתוך %@"; /* No comment provided by engineer. */ "A new version of %@ is available!" = "גרסה חדשה של %@ זמינה!"; /* No comment provided by engineer. */ "A new version of %@ is ready to install!" = "גרסה חדשה של %@ מוכנה להתקנה!"; /* No comment provided by engineer. */ "An error occurred in retrieving update information. Please try again later." = "אירעה שגיאה בקבלת מידע העדכון. אנא נסה שוב מאוחר יותר."; /* No comment provided by engineer. */ "An error occurred while connecting to the installer. Please try again later." = "אירעה שגיאה בעת התחברות למתקין. אנא נסה שוב מאוחר יותר."; /* No comment provided by engineer. */ "An error occurred while downloading the update. Please try again later." = "אירעה שגיאה בעת הורדת העדכון. אנא נסה שוב מאוחר יותר."; /* No comment provided by engineer. */ "An error occurred while extracting the archive. Please try again later." = "אירעה שגיאה בעת חילוץ העדכון. אנא נסה שוב מאוחר יותר."; /* No comment provided by engineer. */ "An error occurred while launching the installer. Please try again later." = "אירעה שגיאה בעת הפעלת המתקין. אנא נסה שוב מאוחר יותר."; /* No comment provided by engineer. */ "An error occurred while parsing the update feed." = "אירעה שגיאה במהלך ניתוח רשימת העדכונים."; /* No comment provided by engineer. */ "An error occurred while running the updater. Please try again later." = "אירעה שגיאה בעת הפעלת המעדכן. אנא נסה שוב מאוחר יותר."; /* No comment provided by engineer. */ "An error occurred while starting the installer. Please try again later." = "אירעה שגיאה בעת הפעלת המתקין. בבקשה נסה שוב מאוחר יותר."; /* No comment provided by engineer. */ "An important update to %@ is ready to install" = "עדכון חשוב ל-%@ מוכן להתקנה"; /* No comment provided by engineer. */ "Cancel" = "ביטול"; /* No comment provided by engineer. */ "Cancel Update" = "בטל את העדכון"; /* No comment provided by engineer. */ "Checking for updates…" = "מחפש עדכונים…"; /* Take care not to overflow the status window. */ "Downloading update…" = "מוריד את העדכון…"; /* Take care not to overflow the status window. */ "Extracting update…" = "מחלץ את העדכון…"; /* No comment provided by engineer. */ "Failed to resume installing update." = "חידוש התקנת העדכון נכשל."; /* No comment provided by engineer. */ "Install and Relaunch" = "עדכן והפעל מחדש"; /* Alternate title for 'Remind Me Later' button when downloaded updates can be resumed */ "Install on Quit" = "התקן בעת סגירת היישום"; /* Take care not to overflow the status window. */ "Installing update…" = "מתקין את העדכון"; /* Alternate title for 'Install Update' button when there's no download in RSS feed. */ "Learn More…" = "למד עוד…"; /* No comment provided by engineer. */ "OK" = "אישור"; /* No comment provided by engineer. */ "Quit %1$@, move it into your Applications folder, relaunch it from there and try again." = "צא מ-%1$@, העבר אותו לתיקיית היישומים שלך, הפעל אותו מחדש משם ונסה שוב."; /* No comment provided by engineer. */ "Ready to Install" = "מוכן להתקנה"; /* No comment provided by engineer. */ "Should %1$@ automatically check for updates? You can always check for updates manually from the %1$@ menu." = "האם %1$@ צריך לבדוק באופן אוטומטי אם יש עדכונים? תוכל תמיד לחפש עדכונים באופן ידני מהתפריט של %1$@."; /* No comment provided by engineer. */ "The update is improperly signed and could not be validated. Please try again later or contact the app developer." = "העדכון חתום בצורה לא נכונה ולא ניתן היה לאמת אותו. נסה שוב מאוחר יותר או צור קשר עם מפתח היישום."; /* No comment provided by engineer. */ "Unable to Check For Updates" = "לא ניתן לחפש עדכונים"; /* No comment provided by engineer. */ "Update Error!" = "העדכון נכשל!"; /* No comment provided by engineer. */ "Update Installed" = "העדכון הותקן בהצלחה"; /* No comment provided by engineer. */ "Updating %@" = "עדכון %@"; /* No comment provided by engineer. */ "Use Finder to copy %1$@ to the Applications folder, relaunch it from there, and try again." = "השתמש ב-Finder כדי להעתיק את %1$@ לתיקיית היישומים, הפעל אותו מחדש משם ונסה שוב."; /* No comment provided by engineer. */ "Version History" = "היסטוריית גרסאות"; /* No comment provided by engineer. */ "Your macOS version is too new" = "גרסת ה-macOS שלך חדשה מדי"; /* No comment provided by engineer. */ "Your macOS version is too old" = "גרסת ה-macOS שלך ישנה מדי"; /* Status message shown when the user checks for updates but is already current or the feed doesn't contain any updates. */ "You’re up to date!" = "לא נמצאו עדכונים חדשים!"; /* System profile key for OS version */ "OS Version" = "גרסת מערכת ההפעלה"; /* System profile key for CPU Type */ "CPU Type" = "סוג מעבד"; /* System profile key for CPU is 64-bit? */ "CPU is 64-Bit?" = "מעבד 64-bit?"; /* System profile yes value for CPU is 64-bit? */ "Yes" = "כן"; /* System profile no value for CPU is 64-bit? */ "No" = "לא"; /* System profile key for CPU Subtype */ "CPU Subtype" = "תת-סוג מעבד"; /* System profile key for Mac Model */ "Mac Model" = "דגם ה-Mac"; /* System profile key for Number of CPUs */ "Number of CPUs" = "מספר ליבות מעבד"; /* System profile key for Preferred Language */ "Preferred Language" = "שפה מועדפת"; /* System profile key for Application Name */ "Application Name" = "שם היישום"; /* System profile key for Application Version */ "Application Version" = "גרסת היישום"; /* System profile key for CPU Speed (MHz) */ "CPU Speed (MHz)" = "מהירות מעבד (MHz)"; /* System profile key for Memory (MB) */ "Memory (MB)" = "זיכרון (MB)"; /* Software Update title/label */ "Software Update" = "עדכון תוכנה"; /* Remind Me Later choice for update alert */ "Remind Me Later" = "הזכר לי מאוחר יותר"; /* Skip This Version choice for update alert */ "Skip This Version" = "דלג על גרסה זו"; /* Install Update choice for update alert */ "Install Update" = "התקן את העדכון"; /* Automatically download and install updates in the future option for update alert */ "Automatically download and install updates in the future" = "הורד והתקן עדכונים בעתיד באופן אוטומטי"; /* Check for updates automatically response for update permission dialog */ "Check Automatically" = "חפש עדכונים אוטומטית"; /* Don't check for updates automatically response for update permission dialog */ "Don’t Check" = "אל תחפש"; /* Title question for update permission dialog */ "Check for updates automatically?" = "לחפש עדכונים באופן אוטומטי?"; /* Include anonymous system profile choice for update permission dialog */ "Include anonymous system profile" = "כלול מידע מערכת אנונימי"; /* Automatically download and install updates choice for update permission dialog */ "Automatically download and install updates" = "הורד והתקן עדכונים באופן אוטומטי"; /* Anonymous system profile information disclosure for update permission dialog */ "Anonymous system profile information is used to help us plan future development work. Please contact us if you have any questions about this.\n\nThis is the information that would be sent:" = "מידע מערכת אנונימי משמש כדי לעזור לנו לתכנן את עבודת הפיתוח העתידית. אנא צור איתנו קשר אם יש לך שאלות בנושא.\n\nזהו המידע שיישלח:"; ================================================ FILE: Sparkle/hr.lproj/Sparkle.strings ================================================ /* No comment provided by engineer. */ "%@ %@ is currently the newest version available." = "%1$@ %2$@ je trenutačno najnovija dostupna verzija."; /* No comment provided by engineer. */ "%@ %@ is currently the newest version available.\n(You are currently running version %@.)" = "%1$@ %2$@ je trenutačno najnovija dostupna verzija.\n(Trenutačno pokrećeš verziju %3$@.)"; /* Description text for SUUpdateAlert when the update is downloadable. */ "%@ %@ is now available—you have %@. Would you like to download it now?" = "%1$@ %2$@ je sada dostupna. Ti koristiš verziju %3$@. Želiš li je sada preuzeti?"; /* Description text for SUUpdateAlert when the update informational with no download. */ "%@ %@ is now available—you have %@. Would you like to learn more about this update on the web?" = "%1$@ %2$@ je sada dostupna. Ti koristiš verziju %3$@. Želiš li saznati više o ovoj nadogradnji na našim web-stranicama?"; /* The download progress in a unit of bytes, e.g. 100 KB */ "%@ downloaded" = "%@ preuzeto"; /* The download progress in units of bytes, e.g. 100 KB of 1,0 MB */ "%@ of %@" = "%1$@ od %2$@"; /* Description text for SUUpdateAlert when the critical update has already been downloaded and ready to install. */ "%1$@ %2$@ has been downloaded and is ready to use! This is an important update; would you like to install it and relaunch %1$@ now?" = "%1$@ %2$@ je preuzeta i spremna za upotrebu! Ovo je važna nadogradnja. Želiš li je sada instalirati, te ponovo pokrenuti %1$@?"; /* Description text for SUUpdateAlert when the update has already been downloaded and ready to install. */ "%1$@ %2$@ has been downloaded and is ready to use! Would you like to install it and relaunch %1$@ now?" = "%1$@ %2$@ je preuzeta i spremna za upotrebu! Želiš li je sada instalirati, te ponovo pokrenuti %1$@?"; /* No comment provided by engineer. */ "%1$@ can’t be updated because it was opened from a read-only or a temporary location." = "%1$@ nije moguće aktualizirati, jer je pokrenuta s lokacije bez korisničkih prava pisanja, npr. dmg."; /* No comment provided by engineer. */ "A new version of %@ is available!" = "Dostupna je nova verzija za %@!"; /* No comment provided by engineer. */ "A new version of %@ is ready to install!" = "Nova verzija za %@ je spremna za instaliranje!"; /* No comment provided by engineer. */ "An error occurred in retrieving update information. Please try again later." = "Dogodila se greška u dohvaćanju informacija o nadogradnji. Pokušaj kasnije ponovo."; /* No comment provided by engineer. */ "An error occurred while downloading the update. Please try again later." = "Dogodila se greška prilikom preuzimanja nadogradnje. Pokušaj kasnije ponovo."; /* No comment provided by engineer. */ "An error occurred while extracting the archive. Please try again later." = "Dogodila se greška prilikom raspakiranja arhive. Pokušaj kasnije ponovo."; /* No comment provided by engineer. */ "An error occurred while parsing the update feed." = "Dogodila se greška prilikom obrađivanja nadogradnje."; /* No comment provided by engineer. */ "An important update to %@ is ready to install" = "Važna nadogradnja za %@ je spremna za instaliranje"; /* No comment provided by engineer. */ "Cancel" = "Odustani"; /* No comment provided by engineer. */ "Cancel Update" = "Prekini aktualiziranje"; /* No comment provided by engineer. */ "Checking for updates…" = "Provjera nadogradnji …"; /* Take care not to overflow the status window. */ "Downloading update…" = "Preuzimanje nadogradnje …"; /* Take care not to overflow the status window. */ "Extracting update…" = "Raspakiravanje nadogradnje …"; /* No comment provided by engineer. */ "Install and Relaunch" = "Instaliraj i ponovo pokreni"; /* Take care not to overflow the status window. */ "Installing update…" = "Instaliranje nadogradnje …"; /* Alternate title for 'Install Update' button when there's no download in RSS feed. */ "Learn More…" = "Saznaj više …"; /* No comment provided by engineer. */ "OK" = "OK"; /* No comment provided by engineer. */ "Ready to Install" = "Spremno za instaliranje"; /* No comment provided by engineer. */ "Should %1$@ automatically check for updates? You can always check for updates manually from the %1$@ menu." = "Želiš li da %1$@ automatski provjerava nadogradnje? Nadogradnje možeš i ručno provjeriti u %1$@ izborniku."; /* No comment provided by engineer. */ "Update Error!" = "Greška prilikom aktualiziranja!"; /* No comment provided by engineer. */ "Updating %@" = "%@ se aktualizira"; /* No comment provided by engineer. */ "Use Finder to copy %1$@ to the Applications folder, relaunch it from there, and try again." = "Kopiraj %1$@ u mapu Aplikacije, pokreni je odande i pokušaj ponovo."; /* Status message shown when the user checks for updates but is already current or the feed doesn't contain any updates. */ "You’re up to date!" = "Koristiš najnoviju verziju!"; /* Software Update title/label */ "Software Update" = "Aktualiziranje softvera"; /* Remind Me Later choice for update alert */ "Remind Me Later" = "Podsjeti me kasnije"; /* Skip This Version choice for update alert */ "Skip This Version" = "Zanemari ovu verziju"; /* Install Update choice for update alert */ "Install Update" = "Instaliraj nadogradnju"; /* Automatically download and install updates in the future option for update alert */ "Automatically download and install updates in the future" = "Ubuduće preuzmi i instaliraj nadogradnje automatski"; /* Check for updates automatically response for update permission dialog */ "Check Automatically" = "Provjeri automatski"; /* Don't check for updates automatically response for update permission dialog */ "Don’t Check" = "Nemoj provjeravati"; /* Title question for update permission dialog */ "Check for updates automatically?" = "Automatski provjeriti nadogradnje?"; /* Include anonymous system profile choice for update permission dialog */ "Include anonymous system profile" = "Uključi anonimizirane podatke o profilu sustava"; /* Automatically download and install updates choice for update permission dialog */ "Automatically download and install updates" = "Preuzmi i instaliraj nadogradnje automatski"; /* Anonymous system profile information disclosure for update permission dialog */ "Anonymous system profile information is used to help us plan future development work. Please contact us if you have any questions about this.\n\nThis is the information that would be sent:" = "Anonimizirani podaci profila susatava pomažu nam planirati budući razvoj. Kontaktiraj nas, ako imaš pitanja o tome.\n\nŠalju se sljedeći podaci:"; ================================================ FILE: Sparkle/hu.lproj/Sparkle.strings ================================================ /* No comment provided by engineer. */ "%@ %@ is currently the newest version available." = "A %1$@ %2$@ a jelenleg elérhető legfrissebb verzió."; /* The download progress in a unit of bytes, e.g. 100 KB */ "%@ downloaded" = "%@ letöltve"; /* The download progress in units of bytes, e.g. 100 KB of 1,0 MB */ "%@ of %@" = "%1$@ / %2$@"; /* Description text for SUUpdateAlert when the critical update has already been downloaded and ready to install. */ "%1$@ %2$@ has been downloaded and is ready to use! This is an important update; would you like to install it and relaunch %1$@ now?" = "A %1$@ %2$@ verziója letöltődött és készen áll a telepítésre! Ez egy fontos frissítés; szeretné most telepíteni, majd újraindítani a %1$@ alkalmazást?"; /* Description text for SUUpdateAlert when the update has already been downloaded and ready to install. */ "%1$@ %2$@ has been downloaded and is ready to use! Would you like to install it and relaunch %1$@ now?" = "A %1$@ %2$@ verziója letöltődött és készen áll a telepítésre! Szeretné most telepíteni, majd újraindítani a %1$@ alkalmazást?"; /* No comment provided by engineer. */ "A new version of %@ is available!" = "A %@ egy újabb verziója elérhető!"; /* No comment provided by engineer. */ "A new version of %@ is ready to install!" = "A %@ egy újabb verziója készen áll a telepítésre!"; /* No comment provided by engineer. */ "An error occurred in retrieving update information. Please try again later." = "Hiba történt a frissítések lekérdezésekor. Kérjük, próbálja újra később."; /* No comment provided by engineer. */ "An error occurred while downloading the update. Please try again later." = "Hiba történt a frissítés letöltése közben. Kérjük, próbálja újra később."; /* No comment provided by engineer. */ "An error occurred while extracting the archive. Please try again later." = "Hiba történt a frissítés kicsomagolása közben. Kérjük, próbálja újra később."; /* No comment provided by engineer. */ "An error occurred while parsing the update feed." = "Hiba történt az elérhető frissítések beolvasásakor."; /* No comment provided by engineer. */ "An important update to %@ is ready to install" = "A %@ egy fontos frissítése készen áll a telepítésre"; /* No comment provided by engineer. */ "Cancel" = "Mégsem"; /* No comment provided by engineer. */ "Cancel Update" = "Frissítés megszakítása"; /* No comment provided by engineer. */ "Install and Relaunch" = "Telepítés és újraindítás"; /* No comment provided by engineer. */ "OK" = "OK"; /* No comment provided by engineer. */ "Ready to Install" = "A telepítő készen áll"; /* No comment provided by engineer. */ "Should %1$@ automatically check for updates? You can always check for updates manually from the %1$@ menu." = "Szeretné, hogy a %1$@ ellenőrizze az elérhető frissítéseket induláskor? Ha nem, a frissítések keresését kézzel is elindíthatja a %1$@ menüből."; /* No comment provided by engineer. */ "Update Error!" = "Frissítési hiba!"; /* No comment provided by engineer. */ "Updating %@" = "%@ frissítése"; /* No comment provided by engineer. */ "Use Finder to copy %1$@ to the Applications folder, relaunch it from there, and try again." = "Mozgassa a %1$@ alkalmazást az Alkalmazások mappába, indítsa újra onnan, majd próbálja újra a frissítést."; /* Software Update title/label */ "Software Update" = "Szoftverfrissítés"; /* Remind Me Later choice for update alert */ "Remind Me Later" = "Emlékeztessen később"; /* Skip This Version choice for update alert */ "Skip This Version" = "Verzió kihagyása"; /* Install Update choice for update alert */ "Install Update" = "Telepítés"; /* Automatically download and install updates in the future option for update alert */ "Automatically download and install updates in the future" = "A jövőben automatikusan töltse le és telepítse a frissítéseket"; /* Check for updates automatically response for update permission dialog */ "Check Automatically" = "Automatikus keresés"; /* Don't check for updates automatically response for update permission dialog */ "Don’t Check" = "Manuális keresés"; /* Title question for update permission dialog */ "Check for updates automatically?" = "Keresse automatikusan a frissítéseket?"; /* Include anonymous system profile choice for update permission dialog */ "Include anonymous system profile" = "Anonim rendszerinformáció küldése"; /* Automatically download and install updates choice for update permission dialog */ "Automatically download and install updates" = "Automatically download and install updates"; /* Anonymous system profile information disclosure for update permission dialog */ "Anonymous system profile information is used to help us plan future development work. Please contact us if you have any questions about this.\n\nThis is the information that would be sent:" = "Anonymous system profile information is used to help us plan future development work. Please contact us if you have any questions about this.\n\nThis is the information that would be sent:"; ================================================ FILE: Sparkle/is.lproj/Sparkle.strings ================================================ /* No comment provided by engineer. */ "%@ %@ is currently the newest version available." = "%@ %@ er nýjasta útgáfan sem er fáanleg þessa stundina."; /* No comment provided by engineer. */ "%@ %@ is currently the newest version available.\n(You are currently running version %@.)" = "%@ %@ er nýjasta útgáfan sem er fáanleg þessa stundina.\n(You are currently running version %@.)"; /* Description text for SUUpdateAlert when the update is downloadable. */ "%@ %@ is now available—you have %@. Would you like to download it now?" = "Útgafa %2$@ af %1$@ er nú fáanlegt en þú ert með %3$@. Viltu sækja hana núna?"; /* The download progress in units of bytes, e.g. 100 KB of 1,0 MB */ "%@ of %@" = "%@ af %@"; /* No comment provided by engineer. */ "A new version of %@ is available!" = "Ný útgáfa af %@ er fáanleg!"; /* No comment provided by engineer. */ "An error occurred in retrieving update information. Please try again later." = "Villa kom upp við að sækja uppfærsluupplýsingar. Vinsamlegast reynið síðar."; /* No comment provided by engineer. */ "An error occurred while extracting the archive. Please try again later." = "Villa kom upp við afþjöppun skráarsafns. Vinsamlegast reynið síðar."; /* No comment provided by engineer. */ "Cancel" = "Hætta við"; /* Take care not to overflow the status window. */ "Downloading update…" = "Sækja nýja útgáfu…"; /* Take care not to overflow the status window. */ "Extracting update…" = "Afþjappa uppfærslu…"; /* No comment provided by engineer. */ "Install and Relaunch" = "Setja upp og ræsa aftur"; /* Take care not to overflow the status window. */ "Installing update…" = "Set inn uppfærslu…"; /* No comment provided by engineer. */ "OK" = "Í lagi"; /* No comment provided by engineer. */ "Ready to Install" = "Innsetning reiðubúin"; /* No comment provided by engineer. */ "Should %1$@ automatically check for updates? You can always check for updates manually from the %1$@ menu." = "Viltu að forritið %1$@ athugi með uppfærslur þegar það er ræst? Ef ekki getur þú athugað handvirkt af %1$@-valblaðinu."; /* No comment provided by engineer. */ "Update Error!" = "Villa við uppfærslu!"; /* No comment provided by engineer. */ "Updating %@" = "Uppfæri %@"; /* Status message shown when the user checks for updates but is already current or the feed doesn't contain any updates. */ "You’re up to date!" = "Það er allt uppfært hjá þér!"; /* Software Update title/label */ "Software Update" = "Hugbúnaðaruppfærsla"; /* Remind Me Later choice for update alert */ "Remind Me Later" = "Áminntu mig síðar"; /* Skip This Version choice for update alert */ "Skip This Version" = "Sleppa þessari útgáfu"; /* Install Update choice for update alert */ "Install Update" = "Innsetja"; /* Automatically download and install updates in the future option for update alert */ "Automatically download and install updates in the future" = "Sækja og innsetja uppfærslur sjálfkrafa framvegis"; /* Check for updates automatically response for update permission dialog */ "Check Automatically" = "Kanna sjálfkrafa"; /* Don't check for updates automatically response for update permission dialog */ "Don’t Check" = "Ekki kanna"; /* Title question for update permission dialog */ "Check for updates automatically?" = "Athuga sjálfkrafa með uppfærslur?"; /* Include anonymous system profile choice for update permission dialog */ "Include anonymous system profile" = "Innifela nafnlausa kerfisskýrslu"; /* Automatically download and install updates choice for update permission dialog */ "Automatically download and install updates" = "Sækja og innsetja uppfærslur sjálfkrafa"; /* Anonymous system profile information disclosure for update permission dialog */ "Anonymous system profile information is used to help us plan future development work. Please contact us if you have any questions about this.\n\nThis is the information that would be sent:" = "Upplýsingar úr nafnlausum kerfisskýrslum eru notaðar til að hjálpa okkur við framtíðarþróun hugbúnaðarins. Ekki hika við að hafa samband ef spurningar vakna um þetta.\n\nÞetta eru upplýsingarnar sem yrðu sendar:"; ================================================ FILE: Sparkle/it.lproj/Sparkle.strings ================================================ /* Description text for SUUpdateAlert when the critical update has already been downloaded and ready to install. */ "%1$@ %2$@ has been downloaded and is ready to use! This is an important update; would you like to install it and relaunch %1$@ now?" = "%1$@ %2$@ è stato scaricato ed è pronto per essere utilizzato! Questo è un aggiornamento importante; desideri installare e riavviare %1$@ ora?"; /* Description text for SUUpdateAlert when the update has already been downloaded and ready to install. */ "%1$@ %2$@ has been downloaded and is ready to use! Would you like to install it and relaunch %1$@ now?" = "%1$@ %2$@ è stato scaricato ed è pronto per essere utilizzato! Desideri installare e riavviare %1$@ ora?"; /* No comment provided by engineer. */ "%1$@ %2$@ is available but your macOS version is too new for this update. This update only supports up to macOS %3$@." = "%1$@ %2$@ è disponibile ma la tua versione di macOS è troppo recente per questo aggiornamento. Questo aggiornamento supporta solo fino a macOS %3$@."; /* No comment provided by engineer. */ "%1$@ %2$@ is available but your macOS version is too old to install it. At least macOS %3$@ is required." = "%1$@ %2$@ è disponibile ma la tua versione di macOS è troppo vecchia per installarla. È richiesto almeno macOS %3$@."; /* No comment provided by engineer. */ "%1$@ can’t be updated if it’s running from the location it was downloaded to." = "%1$@ non può essere aggiornata se è in esecuzione dalla posizione in cui è stato scaricata."; /* No comment provided by engineer. */ "%1$@ can’t be updated because it was opened from a read-only or a temporary location." = "%1$@ non può essere aggiornata perché è stata aperta da una posizione di sola lettura o temporanea."; /* No comment provided by engineer. */ "%@ %@ is currently the newest version available." = "%1$@ %2$@ è la versione più recente attualmente disponibile."; /* No comment provided by engineer. */ "%@ %@ is currently the newest version available.\n(You are currently running version %@.)" = "%1$@ %2$@ è la versione più recente attualmente disponibile.\n(Attualmente stai utilizzando la versione %3$@)"; /* Description text for SUUpdateAlert when the critical update is downloadable. */ "%@ %@ is now available—you have %@. This is an important update; would you like to download it now?" = "%1$@ %2$@ è disponibile; disponi della versione %3$@. Questo è un aggiornamento importante; desideri eseguire l’aggiornamento ora?"; /* Description text for SUUpdateAlert when the update is downloadable. */ "%@ %@ is now available—you have %@. Would you like to download it now?" = "%1$@ %2$@ è disponibile; disponi della versione %3$@. Desideri eseguire l’aggiornamento ora?"; /* Description text for SUUpdateAlert when the update informational with no download. */ "%@ %@ is now available—you have %@. Would you like to learn more about this update on the web?" = "%1$@ %2$@ è disponibile; disponi della versione %3$@. Desideri ulteriori informazioni su questo aggiornamento sul web?"; /* The download progress in a unit of bytes, e.g. 100 KB */ "%@ downloaded" = "%@ scaricato"; /* No comment provided by engineer. */ "%@ is now updated to version %@!" = "%1$@ ora è aggiornata alla versione %2$@!"; /* No comment provided by engineer. */ "%@ is now updated!" = "%@ ora è aggiornata!"; /* The download progress in units of bytes, e.g. 100 KB of 1,0 MB */ "%@ of %@" = "%1$@ di %2$@"; /* No comment provided by engineer. */ "A new version of %@ is available!" = "È disponibile una nuova versione di %@!"; /* No comment provided by engineer. */ "A new version of %@ is ready to install!" = "Una nuova versione di %@ è pronta per essere installata!"; /* No comment provided by engineer. */ "An error occurred in retrieving update information. Please try again later." = "Si è verificato un errore durante il recupero delle informazioni sull’aggiornamento. Riprova in seguito."; /* No comment provided by engineer. */ "An error occurred while connecting to the installer. Please try again later." = "Si è verificato un errore durante la connessione al programma di installazione. Riprova in seguito."; /* No comment provided by engineer. */ "An error occurred while downloading the update. Please try again later." = "Si è verificato un errore durante lo scaricamento dell’aggiornamento. Riprova in seguito."; /* No comment provided by engineer. */ "An error occurred while extracting the archive. Please try again later." = "Si è verificato un errore durante l’estrazione dell’archivio. Riprova in seguito."; /* No comment provided by engineer. */ "An error occurred while launching the installer. Please try again later." = "Si è verificato un errore durante l’avvio del programma di installazione. Riprova in seguito."; /* No comment provided by engineer. */ "An error occurred while parsing the update feed." = "Si è verificato un errore durante la lettura del feed di aggiornamento."; /* No comment provided by engineer. */ "An error occurred while running the updater. Please try again later." = "Si è verificato un errore durante l’esecuzione del programma di aggiornamento. Riprova in seguito."; /* No comment provided by engineer. */ "An error occurred while starting the installer. Please try again later." = "Si è verificato un errore durante l’avvio del programma di installazione. Riprova in seguito."; /* No comment provided by engineer. */ "An important update to %@ is ready to install" = "Un importante aggiornamento di %@ è pronto per l’installazione"; /* No comment provided by engineer. */ "Cancel" = "Annulla"; /* No comment provided by engineer. */ "Cancel Update" = "Annulla Aggiornamento"; /* No comment provided by engineer. */ "Checking for updates…" = "Controllo aggiornamenti in corso…"; /* Take care not to overflow the status window. */ "Downloading update…" = "Scaricamento dell’aggiornamento…"; /* Take care not to overflow the status window. */ "Extracting update…" = "Estrazione dell’aggiornamento…"; /* No comment provided by engineer. */ "Failed to resume installing update." = "Impossibile riprendere l’installazione dell’aggiornamento."; /* No comment provided by engineer. */ "Install and Relaunch" = "Installa e Riavvia"; /* Alternate title for 'Remind Me Later' button when downloaded updates can be resumed */ "Install on Quit" = "Installa all’uscita"; /* Take care not to overflow the status window. */ "Installing update…" = "Installazione aggiornamento in corso…"; /* Alternate title for 'Install Update' button when there's no download in RSS feed. */ "Learn More…" = "Ulteriori informazioni…"; /* No comment provided by engineer. */ "OK" = "OK"; /* No comment provided by engineer. */ "Quit %1$@, move it into your Applications folder, relaunch it from there and try again." = "Esci da %1$@, spostala nella Cartella Applicazioni, riavviala da lì e riprova."; /* No comment provided by engineer. */ "Ready to Install" = "Pronto per l’installazione"; /* No comment provided by engineer. */ "Should %1$@ automatically check for updates? You can always check for updates manually from the %1$@ menu." = "Desideri che %1$@ verifichi gli aggiornamenti automaticamente? Puoi effettuare la verifica manualmente dal menu di %1$@."; /* No comment provided by engineer. */ "The update is improperly signed and could not be validated. Please try again later or contact the app developer." = "L’aggiornamento è firmato in modo errato e non può essere convalidato. Riprova in seguito o contatta lo sviluppatore dell’app."; /* No comment provided by engineer. */ "Unable to Check For Updates" = "Impossibile controllare gli aggiornamenti"; /* No comment provided by engineer. */ "Update Error!" = "Errore di Aggiornamento!"; /* No comment provided by engineer. */ "Update Installed" = "Aggiornamento installato"; /* No comment provided by engineer. */ "Updating %@" = "Aggiornamento di %@"; /* No comment provided by engineer. */ "Use Finder to copy %1$@ to the Applications folder, relaunch it from there, and try again." = "Spostare %1$@ nella Cartella Applicazioni, riavviarla e riprovare."; /* No comment provided by engineer. */ "Version History" = "Cronologia versioni"; /* No comment provided by engineer. */ "Your macOS version is too new" = "La tua versione di macOS è troppo recente"; /* No comment provided by engineer. */ "Your macOS version is too old" = "La tua versione di macOS è troppo vecchia"; /* Status message shown when the user checks for updates but is already current or the feed doesn't contain any updates. */ "You’re up to date!" = "La tua applicazione è aggiornata!"; /* Software Update title/label */ "Software Update" = "Aggiornamento Software"; /* Remind Me Later choice for update alert */ "Remind Me Later" = "Ricordamelo più tardi"; /* Skip This Version choice for update alert */ "Skip This Version" = "Ignora questa versione"; /* Install Update choice for update alert */ "Install Update" = "Installa"; /* Automatically download and install updates in the future option for update alert */ "Automatically download and install updates in the future" = "In futuro scarica e installa automaticamente gli aggiornamenti"; /* Check for updates automatically response for update permission dialog */ "Check Automatically" = "Controlla Automaticamente"; /* Don't check for updates automatically response for update permission dialog */ "Don’t Check" = "Non controllare"; /* Title question for update permission dialog */ "Check for updates automatically?" = "Controllo automaticamente gli aggiornamenti?"; /* Include anonymous system profile choice for update permission dialog */ "Include anonymous system profile" = "Include profilo di sistema anonimo"; /* Automatically download and install updates choice for update permission dialog */ "Automatically download and install updates" = "Scarica e installa automaticamente gli aggiornamenti"; /* Anonymous system profile information disclosure for update permission dialog */ "Anonymous system profile information is used to help us plan future development work. Please contact us if you have any questions about this.\n\nThis is the information that would be sent:" = "Le informazioni del profilo di sistema anomino sono utilizzate per aiutarci in futuri lavori di sviluppo. Contattaci se hai dei quesiti sull’argomento.\n\nQueste sono le informazioni che verrebbero inviate:"; ================================================ FILE: Sparkle/ja.lproj/Sparkle.strings ================================================ /* No comment provided by engineer. */ "%@ %@ is currently the newest version available." = "%1$@ %2$@は現在入手できる最新バージョンです。"; /* No comment provided by engineer. */ "%@ %@ is currently the newest version available.\n(You are currently running version %@.)" = "%1$@ %2$@は現在入手できる最新バージョンです。\n(現在使用中のバージョンは%3$@です。)"; /* Description text for SUUpdateAlert when the critical update is downloadable. */ "%@ %@ is now available—you have %@. This is an important update; would you like to download it now?" = "%1$@ %2$@が入手できます(使用中のバージョンは%3$@です)。これは重要なアップデートです。今すぐダウンロードしますか?"; /* Description text for SUUpdateAlert when the update is downloadable. */ "%@ %@ is now available—you have %@. Would you like to download it now?" = "%1$@ %2$@が入手できます(使用中のバージョンは%3$@です)。今すぐダウンロードしますか?"; /* Description text for SUUpdateAlert when the update informational with no download. */ "%@ %@ is now available—you have %@. Would you like to learn more about this update on the web?" = "%1$@ %2$@が入手できます(使用中のバージョンは%3$@です)。このアップデートの詳しい情報をWebで確認しますか?"; /* The download progress in a unit of bytes, e.g. 100 KB */ "%@ downloaded" = "%@ダウンロード済み"; /* No comment provided by engineer. */ "%@ is now updated to version %@!" = "%@はバージョン%@にアップデートされました!"; /* No comment provided by engineer. */ "%@ is now updated!" = "%@はアップデートされました!"; /* The download progress in units of bytes, e.g. 100 KB of 1,0 MB */ "%@ of %@" = "%1$@/%2$@"; /* Description text for SUUpdateAlert when the critical update has already been downloaded and ready to install. */ "%1$@ %2$@ has been downloaded and is ready to use! This is an important update; would you like to install it and relaunch %1$@ now?" = "%1$@ %2$@がダウンロードされました! これは重要なアップデートです。今すぐ%1$@をインストールして再起動しますか?"; /* Description text for SUUpdateAlert when the update has already been downloaded and ready to install. */ "%1$@ %2$@ has been downloaded and is ready to use! Would you like to install it and relaunch %1$@ now?" = "%1$@ %2$@がダウンロードされました! 今すぐ%1$@をインストールして再起動しますか?"; /* No comment provided by engineer. */ "%1$@ %2$@ is available but your macOS version is too new for this update. This update only supports up to macOS %3$@." = "%1$@ %2$@が入手可能ですが、インストールするにはmacOSのバージョンが新しすぎます。このアップデートはmacOS %3$@までしか対応していません。"; /* No comment provided by engineer. */ "%1$@ %2$@ is available but your macOS version is too old to install it. At least macOS %3$@ is required." = "%1$@ %2$@が入手可能ですが、インストールするにはmacOSのバージョンが古すぎます。macOS %3$@以降が必要です。"; /* No comment provided by engineer. */ "%1$@ can’t be updated if it’s running from the location it was downloaded to." = "この場所ではダウンロードされたアップデートを%1$@に適用できません。"; /* No comment provided by engineer. */ "%1$@ can’t be updated because it was opened from a read-only or a temporary location." = "%1$@は読み出し専用または一時的な場所で開かれているためアップデートできません。"; /* No comment provided by engineer. */ "A new version of %@ is available!" = "新しいバージョンの%@が入手できます!"; /* No comment provided by engineer. */ "A new version of %@ is ready to install!" = "新しいバージョンの%@が今すぐインストールできます!"; /* No comment provided by engineer. */ "An error occurred in retrieving update information. Please try again later." = "アップデート情報の取得中にエラーが発生しました。あとでやり直してください。"; /* No comment provided by engineer. */ "An error occurred while connecting to the installer. Please try again later." = "インストーラに接続中にエラーが発生しました。あとでやり直してください。"; /* No comment provided by engineer. */ "An error occurred while downloading the update. Please try again later." = "アップデートをダウンロード中にエラーが発生しました。あとでやり直してください。"; /* No comment provided by engineer. */ "An error occurred while extracting the archive. Please try again later." = "アーカイブの展開中にエラーが発生しました。あとでやり直してください。"; /* No comment provided by engineer. */ "An error occurred while launching the installer. Please try again later." = "インストーラを起動中にエラーが発生しました。あとでやり直してください。"; /* No comment provided by engineer. */ "An error occurred while parsing the update feed." = "アップデートフィードを解析中にエラーが発生しました。"; /* No comment provided by engineer. */ "An error occurred while running the updater. Please try again later." = "アップデータを実行中にエラーが発生しました。あとでやり直してください。"; /* No comment provided by engineer. */ "An error occurred while starting the installer. Please try again later." = "インストーラを始動中にエラーが発生しました。あとでやり直してください。"; /* No comment provided by engineer. */ "An important update to %@ is ready to install" = "%@の重要なアップデートがインストールできます。"; /* No comment provided by engineer. */ "Application Name" = "アプリ名"; /* No comment provided by engineer. */ "Application Version" = "アプリバージョン"; /* No comment provided by engineer. */ "Cancel" = "キャンセル"; /* No comment provided by engineer. */ "Cancel Update" = "アップデートを中止"; /* No comment provided by engineer. */ "Checking for updates…" = "アップデートを確認しています…"; /* No comment provided by engineer. */ "CPU is 64-Bit?" = "CPUは64-Bit?"; /* No comment provided by engineer. */ "CPU Speed (MHz)" = "CPUスピード (MHz)"; /* No comment provided by engineer. */ "CPU Subtype" = "CPUサブタイプ"; /* No comment provided by engineer. */ "CPU Type" = "CPUタイプ"; /* Take care not to overflow the status window. */ "Downloading update…" = "アップデートをダウンロードしています…"; /* Take care not to overflow the status window. */ "Extracting update…" = "アップデートを展開しています…"; /* No comment provided by engineer. */ "Failed to resume installing update." = "アップデートのインストール再開に失敗しました。"; /* No comment provided by engineer. */ "Install and Relaunch" = "インストールして再起動"; /* Alternate title for 'Remind Me Later' button when downloaded updates can be resumed */ "Install on Quit" = "終了時にインストール"; /* Take care not to overflow the status window. */ "Installing update…" = "アップデートをインストールしています…"; /* Alternate title for 'Install Update' button when there's no download in RSS feed. */ "Learn More…" = "詳しい情報…"; /* No comment provided by engineer. */ "Mac Model" = "Macモデル"; /* No comment provided by engineer. */ "Memory (MB)" = "メモリ (MB)"; /* No comment provided by engineer. */ "No" = "いいえ"; /* No comment provided by engineer. */ "Number of CPUs" = "CPU数"; /* No comment provided by engineer. */ "OK" = "OK"; /* No comment provided by engineer. */ "OS Version" = "OSバージョン"; /* No comment provided by engineer. */ "Preferred Language" = "優先する言語"; /* No comment provided by engineer. */ "Quit %1$@, move it into your Applications folder, relaunch it from there and try again." = "%1$@を終了させ、アプリケーションフォルダに移動の上再度お試しください。"; /* No comment provided by engineer. */ "Ready to Install" = "インストールできます"; /* No comment provided by engineer. */ "Should %1$@ automatically check for updates? You can always check for updates manually from the %1$@ menu." = " %1$@のアップデートを自動で確認しますか? アップデートは%1$@メニューから手動でいつでも確認することができます。"; /* No comment provided by engineer. */ "The installation failed due to not having permission to write the new update." = "新しいアップデートを書き込むアクセス権がないためインストールできませんでした。"; /* No comment provided by engineer. */ "The update is improperly signed and could not be validated. Please try again later or contact the app developer." = "アップデートの署名が不適切で認証できませんでした。あとでやり直すかアプリケーション開発者に連絡してください。"; /* No comment provided by engineer. */ "Unable to Check For Updates" = "アップデートを確認できませんでした"; /* No comment provided by engineer. */ "Update Error!" = "アップデートエラー!"; /* No comment provided by engineer. */ "Update Installed" = "アップデートがインストールされました"; /* No comment provided by engineer. */ "Updating %@" = "%@をアップデート中"; /* No comment provided by engineer. */ "Use Finder to copy %1$@ to the Applications folder, relaunch it from there, and try again." = "%1$@をアプリケーションフォルダに移動し、そこから再起動したあとやり直してください。"; /* No comment provided by engineer. */ "Version History" = "バージョン履歴"; /* No comment provided by engineer. */ "Yes" = "はい"; /* No comment provided by engineer. */ "You may need to allow modifications from %1$@ in System Settings under Privacy & Security and App Management to install future updates." = "今後アップデートをインストールするには、システム設定の「プライバシーとセキュリティ」と「アプリ管理」で%1$@からの変更を許可する必要があります。"; /* No comment provided by engineer. */ "Your macOS version is too new" = "このmacOSのバージョンは新しすぎます"; /* No comment provided by engineer. */ "Your macOS version is too old" = "このmacOSのバージョンは古すぎます"; /* Status message shown when the user checks for updates but is already current or the feed doesn't contain any updates. */ "You’re up to date!" = "最新版を使用しています!"; /* Software Update title/label */ "Software Update" = "ソフトウェア・アップデート"; /* Remind Me Later choice for update alert */ "Remind Me Later" = "あとで通知"; /* Skip This Version choice for update alert */ "Skip This Version" = "このバージョンはスキップ"; /* Install Update choice for update alert */ "Install Update" = "アップデートをインストール"; /* Automatically download and install updates in the future option for update alert */ "Automatically download and install updates in the future" = "今後はアップデートのダウンロードとインストールを自動で行う"; /* Check for updates automatically response for update permission dialog */ "Check Automatically" = "自動で確認"; /* Don't check for updates automatically response for update permission dialog */ "Don’t Check" = "確認しない"; /* Title question for update permission dialog */ "Check for updates automatically?" = "アップデートを自動で確認しますか?"; /* Include anonymous system profile choice for update permission dialog */ "Include anonymous system profile" = "匿名のシステム情報を含める"; /* Automatically download and install updates choice for update permission dialog */ "Automatically download and install updates" = "アップデートを自動でダウンロードしてインストール"; /* Anonymous system profile information disclosure for update permission dialog */ "Anonymous system profile information is used to help us plan future development work. Please contact us if you have any questions about this.\n\nThis is the information that would be sent:" = "匿名のシステムプロファイル情報は、今後の開発の参考にさせていただきます。この件に関してご質問があればご連絡下さい。\n\n以下の情報が送信されます:"; ================================================ FILE: Sparkle/ko.lproj/Sparkle.strings ================================================ /* Description text for SUUpdateAlert when the critical update has already been downloaded and ready to install. */ "%1$@ %2$@ has been downloaded and is ready to use! This is an important update; would you like to install it and relaunch %1$@ now?" = "%1$@ %2$@ 이(가) 다운로드 되었습니다. 프로그램을 업데이트하고 재실행 하시겠습니까?\n이 업데이트는 중요한 업데이트입니다."; /* Description text for SUUpdateAlert when the update has already been downloaded and ready to install. */ "%1$@ %2$@ has been downloaded and is ready to use! Would you like to install it and relaunch %1$@ now?" = "%1$@ %2$@이(가) 다운로드 되었습니다. 프로그램을 업데이트하고 재실행 하시겠습니까?"; /* No comment provided by engineer. */ "%1$@ %2$@ is available but your macOS version is too new for this update. This update only supports up to macOS %3$@." = "%1$@ %2$@ 업데이트는 macOS %3$@ 버전까지만 지원합니다."; /* No comment provided by engineer. */ "%1$@ %2$@ is available but your macOS version is too old to install it. At least macOS %3$@ is required." = "%1$@ %2$@ 업데이트는 최소 macOS %3$@ 버전을 요구합니다."; /* No comment provided by engineer. */ "%1$@ can’t be updated if it’s running from the location it was downloaded to." = "%1$@ 이(가) 다운로드된 위치에서 실행 중인 경우에는 업데이트할 수 없습니다."; /* No comment provided by engineer. */ "%1$@ can’t be updated because it was opened from a read-only or a temporary location." = "%1$@ 이(가) 디스크 이미지나 CD 드라이브 같은 읽기 전용 볼륨에서 실행되고 있으므로 업데이트할 수 없습니다."; /* No comment provided by engineer. */ "%@ %@ is currently the newest version available." = "%1$@ %2$@ 이(가) 현재 최신 버전입니다."; /* No comment provided by engineer. */ "%@ %@ is currently the newest version available.\n(You are currently running version %@.)" = "%1$@ %2$@ 이(가) 현재 최신 버전입니다.\n(현재 실행 중인 버전 : %3$@)"; /* Description text for SUUpdateAlert when the critical update is downloadable. */ "%@ %@ is now available—you have %@. This is an important update; would you like to download it now?" = "%1$@ %2$@ 업데이트를 설치할 수 있습니다. 다운로드 하시겠습니까?\n이 업데이트는 중요한 업데이트입니다. (현재 버전 : %3$@)"; /* Description text for SUUpdateAlert when the update is downloadable. */ "%@ %@ is now available—you have %@. Would you like to download it now?" = "%1$@ %2$@ 이(가) 업데이트 되었습니다. 다운로드 하시겠습니까? (현재 버전 : %3$@)"; /* Description text for SUUpdateAlert when the update informational with no download. */ "%@ %@ is now available—you have %@. Would you like to learn more about this update on the web?" = "%1$@ %2$@ 이(가) 업데이트 되었습니다. 업데이트에 대한 더 많은 정보를 확인하시겠습니까? (현재 버전 : %3$@)"; /* The download progress in a unit of bytes, e.g. 100 KB */ "%@ downloaded" = "%@ 다운로드 완료"; /* No comment provided by engineer. */ "%@ is now updated to version %@!" = "%1$@ 이(가) %2$@ 버전으로 업데이트 되었습니다!"; /* No comment provided by engineer. */ "%@ is now updated!" = "%@ 이(가) 업데이트 되었습니다!"; /* The download progress in units of bytes, e.g. 100 KB of 1,0 MB */ "%@ of %@" = "%1$@ / %2$@"; /* No comment provided by engineer. */ "A new version of %@ is available!" = "%@ 새 버전이 있습니다."; /* No comment provided by engineer. */ "A new version of %@ is ready to install!" = "%@ 새 버전을 설치할 준비가 되었습니다."; /* No comment provided by engineer. */ "An error occurred in retrieving update information. Please try again later." = "업데이트 정보를 수집하는 중 오류가 발생하였습니다. 나중에 다시 시도해 주십시오."; /* No comment provided by engineer. */ "An error occurred while connecting to the installer. Please try again later." = "설치 프로그램에 연결하는 중 오류가 발생하였습니다. 나중에 다시 시도해 주십시오."; /* No comment provided by engineer. */ "An error occurred while downloading the update. Please try again later." = "업데이트를 다운로드 하는 중 오류가 발생하였습니다. 나중에 다시 시도해 주십시오."; /* No comment provided by engineer. */ "An error occurred while extracting the archive. Please try again later." = "압축 파일을 푸는 중 오류가 발생하였습니다. 나중에 다시 시도해 주십시오."; /* No comment provided by engineer. */ "An error occurred while launching the installer. Please try again later." = "설치 프로그램을 실행하는 중 오류가 발생하였습니다. 나중에 다시 시도해 주십시오."; /* No comment provided by engineer. */ "An error occurred while parsing the update feed." = "업데이트 피드를 분석하는 중 오류가 발생하였습니다."; /* No comment provided by engineer. */ "An error occurred while running the updater. Please try again later." = "업데이트를 진행하는 중 오류가 발생하였습니다. 나중에 다시 시도해 주십시오."; /* No comment provided by engineer. */ "An error occurred while starting the installer. Please try again later." = "설치 프로그램을 시작하는 중 오류가 발생하였습니다. 나중에 다시 시도해 주십시오."; /* No comment provided by engineer. */ "An important update to %@ is ready to install" = "(중요 업데이트) %@ 을(를) 설치할 준비가 완료되었습니다."; /* No comment provided by engineer. */ "Cancel" = "취소"; /* No comment provided by engineer. */ "Cancel Update" = "업데이트 취소"; /* No comment provided by engineer. */ "Checking for updates…" = "업데이트 확인 중…"; /* Take care not to overflow the status window. */ "Downloading update…" = "다운로드 중…"; /* Take care not to overflow the status window. */ "Extracting update…" = "압축 푸는 중…"; /* No comment provided by engineer. */ "Failed to resume installing update." = "업데이트 설치를 재개하지 못했습니다."; /* No comment provided by engineer. */ "Install and Relaunch" = "설치 & 재실행"; /* Alternate title for 'Remind Me Later' button when downloaded updates can be resumed */ "Install on Quit" = "종료 시 설치"; /* Take care not to overflow the status window. */ "Installing update…" = "설치 중…"; /* Alternate title for 'Install Update' button when there's no download in RSS feed. */ "Learn More…" = "더 알아보기…"; /* No comment provided by engineer. */ "OK" = "확인"; /* No comment provided by engineer. */ "Quit %1$@, move it into your Applications folder, relaunch it from there and try again." = "%1$@ 을(를) 종료하고, 응용 프로그램 폴더로 옮긴 다음 다시 실행해 주십시오."; /* No comment provided by engineer. */ "Ready to Install" = "설치 준비 완료"; /* No comment provided by engineer. */ "Should %1$@ automatically check for updates? You can always check for updates manually from the %1$@ menu." = "%1$@ 업데이트 확인을 자동으로 할까요? %1$@ 메뉴에서 수동으로 변경 할 수 있습니다."; /* No comment provided by engineer. */ "The update is improperly signed and could not be validated. Please try again later or contact the app developer." = "업데이트가 잘못 서명되어 유효성 검사를 할 수 없습니다. 나중에 다시 시도하거나 앱 개발자에게 문의해주세요."; /* No comment provided by engineer. */ "Unable to Check For Updates" = "업데이트를 확인할 수 없습니다."; /* No comment provided by engineer. */ "Update Error!" = "업데이트 오류!"; /* No comment provided by engineer. */ "Update Installed" = "업데이트가 설치되었습니다."; /* No comment provided by engineer. */ "Updating %@" = "%@ 업데이트 중"; /* No comment provided by engineer. */ "Use Finder to copy %1$@ to the Applications folder, relaunch it from there, and try again." = "%1$@ 을(를) 응용 프로그램 폴더로 옮긴 다음 다시 실행해 주십시오."; /* No comment provided by engineer. */ "Version History" = "버전 기록"; /* No comment provided by engineer. */ "Your macOS version is too new" = "사용 중인 macOS 버전이 너무 높습니다."; /* No comment provided by engineer. */ "Your macOS version is too old" = "사용 중인 macOS 버전이 너무 낮습니다."; /* Status message shown when the user checks for updates but is already current or the feed doesn't contain any updates. */ "You’re up to date!" = "최신 버전입니다."; /* Software Update title/label */ "Software Update" = "소프트웨어 업데이트"; /* Remind Me Later choice for update alert */ "Remind Me Later" = "나중에"; /* Skip This Version choice for update alert */ "Skip This Version" = "이 버전 건너뛰기"; /* Install Update choice for update alert */ "Install Update" = "업데이트 설치"; /* Automatically download and install updates in the future option for update alert */ "Automatically download and install updates in the future" = "향후 업데이트 자동 다운로드 및 설치"; /* Check for updates automatically response for update permission dialog */ "Check Automatically" = "자동으로 확인"; /* Don't check for updates automatically response for update permission dialog */ "Don’t Check" = "취소"; /* Title question for update permission dialog */ "Check for updates automatically?" = "업데이트를 자동으로 확인할까요?"; /* Include anonymous system profile choice for update permission dialog */ "Include anonymous system profile" = "익명 시스템 정보 포함"; /* Automatically download and install updates choice for update permission dialog */ "Automatically download and install updates" = "업데이트 자동 다운로드 및 설치"; /* Anonymous system profile information disclosure for update permission dialog */ "Anonymous system profile information is used to help us plan future development work. Please contact us if you have any questions about this.\n\nThis is the information that would be sent:" = "익명으로 보내지는 시스템 정보로 차후 프로그램 개발에 도움이 될 수 있습니다. 질문이 있으시면 연락 주십시오.\n\n아래 정보가 전송될 것입니다."; ================================================ FILE: Sparkle/nb.lproj/Sparkle.strings ================================================ /* No comment provided by engineer. */ "%@ %@ is currently the newest version available." = "%1$@ %2$@ er nyeste versjon."; /* No comment provided by engineer. */ "%@ %@ is currently the newest version available.\n(You are currently running version %@.)" = "%1$@ %2$@ er nyeste versjon.\n(You are currently running version %3$@.)"; /* Description text for SUUpdateAlert when the update is downloadable. */ "%@ %@ is now available—you have %@. Would you like to download it now?" = "%1$@ %2$@ er nå tilgjengelig—du har %3$@. Ønsker du å laste ned og installere nå?"; /* The download progress in a unit of bytes, e.g. 100 KB */ "%@ downloaded" = "%@ lastet ned"; /* The download progress in units of bytes, e.g. 100 KB of 1,0 MB */ "%@ of %@" = "%1$@ av %2$@"; /* Description text for SUUpdateAlert when the critical update has already been downloaded and ready to install. */ "%1$@ %2$@ has been downloaded and is ready to use! This is an important update; would you like to install it and relaunch %1$@ now?" = "%1$@ %2$@ er lastet ned og er klar til bruk! Dette er en viktig oppdatering; ønsker du å installere og restarte %1$@ nå?"; /* Description text for SUUpdateAlert when the update has already been downloaded and ready to install. */ "%1$@ %2$@ has been downloaded and is ready to use! Would you like to install it and relaunch %1$@ now?" = "%1$@ %2$@ er lastet ned og er klar til bruk! Ønsker du å installere og restarte %1$@ nå?"; /* No comment provided by engineer. */ "%1$@ can’t be updated because it was opened from a read-only or a temporary location." = "%1$@ kan ikke oppdateres fra en 'bare lesbar' enhet som f.eks. en cd"; /* No comment provided by engineer. */ "A new version of %@ is available!" = "En ny versjon av %@ er tilgjengelig!"; /* No comment provided by engineer. */ "A new version of %@ is ready to install!" = "En ny versjon av %@ er klar for installering!"; /* No comment provided by engineer. */ "An error occurred in retrieving update information. Please try again later." = "En feil oppstod ved henting av oppdateringsinformasjon. Vennligst prøv igjen senere."; /* No comment provided by engineer. */ "An error occurred while downloading the update. Please try again later." = "En feil oppstod under nedlasting av oppdateringen. Vennligst prøv igjen senere."; /* No comment provided by engineer. */ "An error occurred while extracting the archive. Please try again later." = "En feil oppstod under utpakking av oppdateringen. Vennligst prøv igjen senere."; /* No comment provided by engineer. */ "An error occurred while parsing the update feed." = "En feil oppstod under lesing av oppdateringsstrømmen."; /* No comment provided by engineer. */ "An important update to %@ is ready to install" = "En viktig oppdatering for %@ er klar til å installeres"; /* No comment provided by engineer. */ "Cancel" = "Avbryt"; /* No comment provided by engineer. */ "Cancel Update" = "Avbryt oppdateringen"; /* No comment provided by engineer. */ "Checking for updates…" = "Søker etter oppdateringer…"; /* Take care not to overflow the status window. */ "Downloading update…" = "Laster ned oppdateringen…"; /* Take care not to overflow the status window. */ "Extracting update…" = "Pakker ut oppdateringen…"; /* No comment provided by engineer. */ "Install and Relaunch" = "Installer og start på ny"; /* Take care not to overflow the status window. */ "Installing update…" = "Installerer oppdateringen…"; /* Alternate title for 'Install Update' button when there's no download in RSS feed. */ "Learn More…" = "Mer info…"; /* No comment provided by engineer. */ "OK" = "OK"; /* No comment provided by engineer. */ "Ready to Install" = "Klar til å installere"; /* No comment provided by engineer. */ "Should %1$@ automatically check for updates? You can always check for updates manually from the %1$@ menu." = "Skal %1$@ søke automatisk etter oppdateringer? Du kan når som helst søke manuelt fra %1$@-menyen."; /* No comment provided by engineer. */ "Update Error!" = "Feil ved oppdateringen!"; /* No comment provided by engineer. */ "Updating %@" = "Oppdaterer %@"; /* No comment provided by engineer. */ "Use Finder to copy %1$@ to the Applications folder, relaunch it from there, and try again." = "Flytt %1$@ til Programmer-katalogen, start på ny og prøv igjen."; /* Status message shown when the user checks for updates but is already current or the feed doesn't contain any updates. */ "You’re up to date!" = "Ingen nye oppdateringer"; /* Software Update title/label */ "Software Update" = "Programoppdatering"; /* Remind Me Later choice for update alert */ "Remind Me Later" = "Utsett"; /* Skip This Version choice for update alert */ "Skip This Version" = "Hopp over"; /* Install Update choice for update alert */ "Install Update" = "Installer"; /* Automatically download and install updates in the future option for update alert */ "Automatically download and install updates in the future" = "Last ned og installer automatisk i fremtiden"; /* Check for updates automatically response for update permission dialog */ "Check Automatically" = "Søk automatisk"; /* Don't check for updates automatically response for update permission dialog */ "Don’t Check" = "Ikke søk"; /* Title question for update permission dialog */ "Check for updates automatically?" = "Søk etter oppdateringer automatisk?"; /* Include anonymous system profile choice for update permission dialog */ "Include anonymous system profile" = "Inkluder anonym systemprofil"; /* Automatically download and install updates choice for update permission dialog */ "Automatically download and install updates" = "Last ned og installer automatisk"; /* Anonymous system profile information disclosure for update permission dialog */ "Anonymous system profile information is used to help us plan future development work. Please contact us if you have any questions about this.\n\nThis is the information that would be sent:" = "Den anonyme systemprofilen hjelper oss med å planlegge fremtidig utviklingsarbeid. Ta gjerne kontakt med oss hvis du har spørsmål om dette.
\nFølgende innhold vil bli sendt:"; ================================================ FILE: Sparkle/nl.lproj/Sparkle.strings ================================================ /* No comment provided by engineer. */ "%@ %@ is currently the newest version available." = "%1$@ %2$@ is momenteel de nieuwste versie."; /* No comment provided by engineer. */ "%@ %@ is currently the newest version available.\n(You are currently running version %@.)" = "%1$@ %2$@ is momenteel de nieuwste versie.\n(Je gebruikt op dit moment versie %3$@.)"; /* Description text for SUUpdateAlert when the critical update is downloadable. */ "%@ %@ is now available—you have %@. This is an important update; would you like to download it now?" = "%1$@ %2$@ is nu beschikbaar – je hebt %3$@. Dit is een belangrijke update. Wil je deze nu downloaden?"; /* Description text for SUUpdateAlert when the update is downloadable. */ "%@ %@ is now available—you have %@. Would you like to download it now?" = "%1$@ %2$@ is nu beschikbaar – je hebt %3$@. Wil je de update nu downloaden?"; /* Description text for SUUpdateAlert when the update informational with no download. */ "%@ %@ is now available—you have %@. Would you like to learn more about this update on the web?" = "%1$@ %2$@ is nu beschikbaar – je hebt %3$@. Wil je een webpagina openen met meer informatie?"; /* The download progress in a unit of bytes, e.g. 100 KB */ "%@ downloaded" = "%@ gedownload"; /* No comment provided by engineer. */ "%@ is now updated to version %@!" = "%@ is nu bijgewerkt naar versie %@!"; /* No comment provided by engineer. */ "%@ is now updated!" = "%@ is nu bijgewerkt!"; /* The download progress in units of bytes, e.g. 100 KB of 1,0 MB */ "%@ of %@" = "%1$@ van %2$@"; /* Description text for SUUpdateAlert when the critical update has already been downloaded and ready to install. */ "%1$@ %2$@ has been downloaded and is ready to use! This is an important update; would you like to install it and relaunch %1$@ now?" = "%1$@ %2$@ is gedownload en klaar om te installeren! Dit is een belangrijke update. Wil je deze nu installeren en %1$@ herstarten?"; /* Description text for SUUpdateAlert when the update has already been downloaded and ready to install. */ "%1$@ %2$@ has been downloaded and is ready to use! Would you like to install it and relaunch %1$@ now?" = "%1$@ %2$@ is gedownload en klaar om te installeren! Wil je deze nu installeren en %1$@ herstarten?"; /* No comment provided by engineer. */ "%1$@ %2$@ is available but your macOS version is too new for this update. This update only supports up to macOS %3$@." = "%1$@ %2$@ is beschikbaar, maar je versie van macOS is te nieuw voor deze update. De update kan alleen worden gebruikt tot macOS %3$@."; /* No comment provided by engineer. */ "%1$@ %2$@ is available but your macOS version is too old to install it. At least macOS %3$@ is required." = "%1$@ %2$@ is beschikbaar, maar je versie van macOS is te oud om deze te installeren. Ten minste macOS %3$@ is vereist."; /* No comment provided by engineer. */ "%1$@ can’t be updated if it’s running from the location it was downloaded to." = "%1$@ kan niet worden bijgewerkt als het is geopend vanuit de downloadlocatie."; /* No comment provided by engineer. */ "%1$@ can’t be updated because it was opened from a read-only or a temporary location." = "%1$@ kan niet worden bijgewerkt, omdat het vanuit een alleen-lezen- of tijdelijke locatie is geopend."; /* No comment provided by engineer. */ "A new version of %@ is available!" = "Een nieuwe versie van %@ is beschikbaar!"; /* No comment provided by engineer. */ "A new version of %@ is ready to install!" = "Een nieuwe versie van %@ is klaar om te installeren!"; /* No comment provided by engineer. */ "An error occurred in retrieving update information. Please try again later." = "Er heeft zich een fout voorgedaan bij het ophalen van update-informatie. Probeer het later opnieuw."; /* No comment provided by engineer. */ "An error occurred while connecting to the installer. Please try again later." = "Er heeft zich een fout voorgedaan bij het verbinden met het installatieprogramma. Probeer het later opnieuw."; /* No comment provided by engineer. */ "An error occurred while downloading the update. Please try again later." = "Er heeft zich een fout voorgedaan bij het downloaden van de update. Probeer het later opnieuw"; /* No comment provided by engineer. */ "An error occurred while extracting the archive. Please try again later." = "Er heeft zich een fout voorgedaan bij het uitpakken van het archief. Probeer het later opnieuw."; /* No comment provided by engineer. */ "An error occurred while launching the installer. Please try again later." = "Er heeft zich een fout voorgedaan bij het starten van het installatieprogramma. Probeer het later opnieuw."; /* No comment provided by engineer. */ "An error occurred while parsing the update feed." = "Er heeft zich een fout voorgedaan bij het verwerken van de update-informatie."; /* No comment provided by engineer. */ "An error occurred while running the updater. Please try again later." = "Er heeft zich een fout voorgedaan bij het uitvoeren van het bijwerkprogramma. Probeer het later opnieuw."; /* No comment provided by engineer. */ "An error occurred while starting the installer. Please try again later." = "Er heeft zich een fout voorgedaan bij het starten van het installatieprogramma. Probeer het later opnieuw."; /* No comment provided by engineer. */ "An important update to %@ is ready to install" = "Een belangrijke update voor %@ is klaar om te installeren"; /* No comment provided by engineer. */ "Application Name" = "Appnaam"; /* No comment provided by engineer. */ "Application Version" = "Appversie"; /* No comment provided by engineer. */ "Cancel" = "Annuleer"; /* No comment provided by engineer. */ "Cancel Update" = "Annuleer update"; /* No comment provided by engineer. */ "Checking for updates…" = "Zoeken naar updates…"; /* No comment provided by engineer. */ "CPU is 64-Bit?" = "CPU is 64-bits?"; /* No comment provided by engineer. */ "CPU Speed (MHz)" = "CPU-snelheid (MHz)"; /* No comment provided by engineer. */ "CPU Subtype" = "CPU-subtype"; /* No comment provided by engineer. */ "CPU Type" = "CPU-type"; /* Take care not to overflow the status window. */ "Downloading update…" = "Update downloaden…"; /* Take care not to overflow the status window. */ "Extracting update…" = "Update uitpakken…"; /* No comment provided by engineer. */ "Failed to resume installing update." = "Het hervatten van de update-installatie is mislukt."; /* No comment provided by engineer. */ "Install and Relaunch" = "Installeer en herstart"; /* Alternate title for 'Remind Me Later' button when downloaded updates can be resumed */ "Install on Quit" = "Installeer bij het stoppen"; /* Take care not to overflow the status window. */ "Installing update…" = "Update installeren…"; /* Alternate title for 'Install Update' button when there's no download in RSS feed. */ "Learn More…" = "Meer informatie…"; /* No comment provided by engineer. */ "Mac Model" = "Mac-model"; /* No comment provided by engineer. */ "Memory (MB)" = "Geheugen (MB)"; /* No comment provided by engineer. */ "No" = "Nee"; /* No comment provided by engineer. */ "Number of CPUs" = "Aantal CPU's"; /* No comment provided by engineer. */ "OK" = "OK"; /* No comment provided by engineer. */ "OS Version" = "OS-versie"; /* No comment provided by engineer. */ "Preferred Language" = "Voorkeurstaal"; /* No comment provided by engineer. */ "Quit %1$@, move it into your Applications folder, relaunch it from there and try again." = "Stop %1$@, verplaats het naar de map 'Apps', herstart het van daaruit en probeer opnieuw."; /* No comment provided by engineer. */ "Ready to Install" = "Klaar om te installeren"; /* No comment provided by engineer. */ "Should %1$@ automatically check for updates? You can always check for updates manually from the %1$@ menu." = "Wil je dat %1$@ automatisch naar updates zoekt? Je kunt altijd nog handmatig zoeken naar updates via het %1$@-menu."; /* No comment provided by engineer. */ "The installation failed due to not having permission to write the new update." = "De installatie is mislukt omdat je geen bevoegdheden hebt om de nieuwe update te schrijven."; /* No comment provided by engineer. */ "The update is improperly signed and could not be validated. Please try again later or contact the app developer." = "De update is onjuist gesigneerd en kan niet worden gecontroleerd. Probeer het later opnieuw of neem contact op met de app-ontwikkelaar."; /* No comment provided by engineer. */ "Unable to Check For Updates" = "Zoeken naar updates mislukt"; /* No comment provided by engineer. */ "Update Error!" = "Fout bij het bijwerken!"; /* No comment provided by engineer. */ "Update Installed" = "Update geïnstalleerd"; /* No comment provided by engineer. */ "Updating %@" = "%@ bijwerken…"; /* No comment provided by engineer. */ "Use Finder to copy %1$@ to the Applications folder, relaunch it from there, and try again." = "Gebruik Finder om %1$@ naar de map 'Apps' te kopiëren, herstart het van daaruit en probeer opnieuw."; /* No comment provided by engineer. */ "Version History" = "Versiegeschiedenis"; /* No comment provided by engineer. */ "Yes" = "Ja"; /* No comment provided by engineer. */ "You may need to allow modifications from %1$@ in System Settings under Privacy & Security and App Management to install future updates." = "Je moet mogelijk %1$@ toevoegen in Systeeminstellingen onder 'Privacy en beveiliging' en 'Appbeheer' om toekomstige updates te installeren."; /* Status message shown when the user checks for updates but is already current or the feed doesn't contain any updates. */ "You’re up to date!" = "Je hebt de nieuwste versie!"; /* No comment provided by engineer. */ "Your macOS version is too new" = "Je versie van macOS is te nieuw"; /* No comment provided by engineer. */ "Your macOS version is too old" = "Je versie van macOS is te oud"; /* Software Update title/label */ "Software Update" = "Software-update"; /* Remind Me Later choice for update alert */ "Remind Me Later" = "Herinner mij later"; /* Skip This Version choice for update alert */ "Skip This Version" = "Sla deze versie over"; /* Install Update choice for update alert */ "Install Update" = "Installeer update"; /* Automatically download and install updates in the future option for update alert */ "Automatically download and install updates in the future" = "Download en installeer updates voortaan automatisch"; /* Check for updates automatically response for update permission dialog */ "Check Automatically" = "Zoek automatisch"; /* Don't check for updates automatically response for update permission dialog */ "Don’t Check" = "Zoek niet"; /* Title question for update permission dialog */ "Check for updates automatically?" = "Automatisch zoeken naar updates?"; /* Include anonymous system profile choice for update permission dialog */ "Include anonymous system profile" = "Voeg anoniem systeemprofiel bij"; /* Automatically download and install updates choice for update permission dialog */ "Automatically download and install updates" = "Download en installeer updates automatisch"; /* Anonymous system profile information disclosure for update permission dialog */ "Anonymous system profile information is used to help us plan future development work. Please contact us if you have any questions about this.\n\nThis is the information that would be sent:" = "Aan de hand van anonieme informatie over het systeemprofiel kunnen wij toekomstige ontwikkelingswerkzaamheden beter plannen. Neem contact met ons op als je hierover vragen hebt.\n\nDit is de informatie die wordt verzonden:"; ================================================ FILE: Sparkle/nn.lproj/Sparkle.strings ================================================ /* No comment provided by engineer. */ "%@ %@ is currently the newest version available." = "%1$@ %2$@ er nyaste versjon."; /* No comment provided by engineer. */ "%@ %@ is currently the newest version available.\n(You are currently running version %@.)" = "%1$@ %2$@ er nyaste versjon.\n(Du køyrer versjon %3$@ akkurat no.)"; /* Description text for SUUpdateAlert when the update is downloadable. */ "%@ %@ is now available—you have %@. Would you like to download it now?" = "%1$@ %2$@ er no tilgjengeleg — du har %3$@. Ynskjer du å lasta ned og installera no?"; /* The download progress in a unit of bytes, e.g. 100 KB */ "%@ downloaded" = "%@ nedlasta"; /* The download progress in units of bytes, e.g. 100 KB of 1,0 MB */ "%@ of %@" = "%1$@ av %2$@"; /* Description text for SUUpdateAlert when the critical update has already been downloaded and ready to install. */ "%1$@ %2$@ has been downloaded and is ready to use! This is an important update; would you like to install it and relaunch %1$@ now?" = "%1$@ %2$@ er lasta ned og er klar til bruk! Dette er ei viktig oppdatering; ynskjer du å installera og starta om att %1$@ no?"; /* Description text for SUUpdateAlert when the update has already been downloaded and ready to install. */ "%1$@ %2$@ has been downloaded and is ready to use! Would you like to install it and relaunch %1$@ now?" = "%1$@ %2$@ er lasta ned og er klar til bruk! Ynskjer du å installera og starta om att %1$@ no?"; /* No comment provided by engineer. */ "%1$@ can’t be updated because it was opened from a read-only or a temporary location." = "%1$@ kan ikkje oppdaterast frå ei eining som berre kan lesast, t.d. frå ein cd"; /* No comment provided by engineer. */ "A new version of %@ is available!" = "Ein ny versjon av %@ er tilgjengeleg!"; /* No comment provided by engineer. */ "A new version of %@ is ready to install!" = "Ein ny versjon av %@ er klar for installering!"; /* No comment provided by engineer. */ "An error occurred in retrieving update information. Please try again later." = "Ein feil oppstod ved henting av oppdateringsinformasjon. Ver greid og prøv igjen seinare."; /* No comment provided by engineer. */ "An error occurred while downloading the update. Please try again later." = "Ein feil oppstod under nedlasting av oppdateringa. Ver greid og prøv igjen seinare."; /* No comment provided by engineer. */ "An error occurred while extracting the archive. Please try again later." = "Ein feil oppstod under utpakking av oppdateringa. Ver greid og prøv igjen seinare."; /* No comment provided by engineer. */ "An error occurred while parsing the update feed." = "Ein feil oppstod under lesing av oppdateringsstraumen."; /* No comment provided by engineer. */ "An important update to %@ is ready to install" = "Ei viktig oppdatering for %@ er klar til å verta installert"; /* No comment provided by engineer. */ "Cancel" = "Avbryt"; /* No comment provided by engineer. */ "Cancel Update" = "Avbryt oppdateringa"; /* No comment provided by engineer. */ "Checking for updates…" = "Ser etter oppdateringar…"; /* Take care not to overflow the status window. */ "Downloading update…" = "Lastar ned oppdateringa…"; /* Take care not to overflow the status window. */ "Extracting update…" = "Pakkar ut oppdateringa…"; /* No comment provided by engineer. */ "Install and Relaunch" = "Installer og start på nytt"; /* Take care not to overflow the status window. */ "Installing update…" = "Installerer oppdateringa…"; /* Alternate title for 'Install Update' button when there's no download in RSS feed. */ "Learn More…" = "Meir info…"; /* No comment provided by engineer. */ "OK" = "Greitt"; /* No comment provided by engineer. */ "Ready to Install" = "Klar til å installera"; /* No comment provided by engineer. */ "Should %1$@ automatically check for updates? You can always check for updates manually from the %1$@ menu." = "Skal %1$@ sjå automatisk etter oppdateringar? Du kan når som helst sjå etter manuelt i %1$@-menyen."; /* No comment provided by engineer. */ "Update Error!" = "Feil ved oppdateringa!"; /* No comment provided by engineer. */ "Updating %@" = "Oppdaterer %@"; /* No comment provided by engineer. */ "Use Finder to copy %1$@ to the Applications folder, relaunch it from there, and try again." = "Flytt %1$@ til Programmer-mappa, start på nytt og prøv igjen."; /* Status message shown when the user checks for updates but is already current or the feed doesn't contain any updates. */ "You’re up to date!" = "Ingen nye oppdateringar"; /* Software Update title/label; */ "Software Update" = "Programoppdatering"; /* Remind Me Later choice for update alert */ "Remind Me Later" = "Utsett"; /* Skip This Version choice for update alert */ "Skip This Version" = "Hopp over"; /* Install Update choice for update alert */ "Install Update" = "Installer"; /* Automatically download and install updates in the future option for update alert */ "Automatically download and install updates in the future" = "Last ned og installer automatisk i framtida"; /* Check for updates automatically response for update permission dialog */ "Check Automatically" = "Sjå etter automatisk"; /* Don't check for updates automatically response for update permission dialog */ "Don’t Check" = "Ikkje sjå etter"; /* Title question for update permission dialog */ "Check for updates automatically?" = "Sjå etter etter oppdateringar automatisk?"; /* Include anonymous system profile choice for update permission dialog */ "Include anonymous system profile" = "Inkluder anonym systemprofil"; /* Automatically download and install updates choice for update permission dialog */ "Automatically download and install updates" = "Last ned og installer automatisk"; /* Anonymous system profile information disclosure for update permission dialog */ "Anonymous system profile information is used to help us plan future development work. Please contact us if you have any questions about this.\n\nThis is the information that would be sent:" = "Den anonyme systemprofilen hjelper oss med å planleggja framtidig utviklingsarbeid. Ta gjerne kontakt med oss om du har spørsmål om dette.
\nFølgjande innhald vil bli sendt:"; ================================================ FILE: Sparkle/pl.lproj/Sparkle.strings ================================================ /* No comment provided by engineer. */ "%@ %@ is currently the newest version available." = "%1$@ %2$@ jest najnowszą dostępną wersją."; /* No comment provided by engineer. */ "%@ %@ is currently the newest version available.\n(You are currently running version %@.)" = "%1$@ %2$@ jest najnowszą dostępną wersją.\n(aktualnie posiadasz %3$@.)"; /* Description text for SUUpdateAlert when the update is downloadable. */ "%@ %@ is now available—you have %@. Would you like to download it now?" = "%1$@ %2$@ jest już dostępny (aktualnie posiadasz %3$@). Czy chcesz go teraz pobrać?"; /* Description text for SUUpdateAlert when the update informational with no download. */ "%@ %@ is now available—you have %@. Would you like to learn more about this update on the web?" = "%1$@ %2$@ jest już dostępny (aktualnie posiadasz %3$@). Czy chcesz otworzyć stronę z informacjami o tym uaktualnieniu?"; /* The download progress in a unit of bytes, e.g. 100 KB */ "%@ downloaded" = "Pobrano %@"; /* The download progress in units of bytes, e.g. 100 KB of 1,0 MB */ "%@ of %@" = "%1$@ z %2$@"; /* Description text for SUUpdateAlert when the update has already been downloaded and ready to install. */ "%1$@ %2$@ has been downloaded and is ready to use! Would you like to install it and relaunch %1$@ now?" = "%1$@ %2$@ został pobrany i jest gotowy do użycia! Czy chcesz teraz zainstalować i ponownie uruchomić %1$@?"; /* No comment provided by engineer. */ "%1$@ can’t be updated because it was opened from a read-only or a temporary location." = "%1$@ nie może zostać uaktualniony, ponieważ został uruchomiony z folderu tymczasowego lub tylko do odczytu."; /* No comment provided by engineer. */ "A new version of %@ is available!" = "Dostępna jest nowa wersja %@!"; /* No comment provided by engineer. */ "A new version of %@ is ready to install!" = "Nowa wersja %@ gotowa do zainstalowania!"; /* No comment provided by engineer. */ "An error occurred in retrieving update information. Please try again later." = "Błąd podczas pobierania informacji o uaktualnieniach. Spróbuj ponownie później."; /* No comment provided by engineer. */ "An error occurred while downloading the update. Please try again later." = "Błąd podczas pobierania uaktualnienia. Spróbuj ponownie później."; /* No comment provided by engineer. */ "An error occurred while extracting the archive. Please try again later." = "Błąd podczas rozpakowywania archiwum. Spróbuj ponownie później"; /* No comment provided by engineer. */ "An error occurred while parsing the update feed." = "Błąd podczas wczytywania danych o uaktualnieniu."; /* No comment provided by engineer. */ "Cancel" = "Anuluj"; /* No comment provided by engineer. */ "Cancel Update" = "Anuluj uaktualnianie"; /* No comment provided by engineer. */ "Checking for updates…" = "Sprawdzam uaktualnienia…"; /* Take care not to overflow the status window. */ "Downloading update…" = "Pobieram uaktualnienie…"; /* Take care not to overflow the status window. */ "Extracting update…" = "Rozpakowuję uaktualnienie…"; /* No comment provided by engineer. */ "Install and Relaunch" = "Zainstaluj i uruchom ponownie"; /* Take care not to overflow the status window. */ "Installing update…" = "Instalowanie uaktualnienia…"; /* No comment provided by engineer. */ "OK" = "OK"; /* No comment provided by engineer. */ "Ready to Install" = "Gotowy do instalacji"; /* No comment provided by engineer. */ "Should %1$@ automatically check for updates? You can always check for updates manually from the %1$@ menu." = "Czy %1$@ ma automatycznie sprawdzać uaktualnienia? Zawsze możesz ręcznie sprawdzać z menu %1$@."; /* No comment provided by engineer. */ "Update Error!" = "Błąd uaktualniania!"; /* No comment provided by engineer. */ "Updating %@" = "Uaktualniam %@"; /* No comment provided by engineer. */ "Use Finder to copy %1$@ to the Applications folder, relaunch it from there, and try again." = "Użyj Findera, aby skopiować %1$@ do folderu Programy, uruchom z nowej lokacji i spróbuj ponownie."; /* Status message shown when the user checks for updates but is already current or the feed doesn't contain any updates. */ "You’re up to date!" = "Jesteś na bieżąco!"; /* Software Update title/label */ "Software Update" = "Uaktualnienie oprogramowania"; /* Remind Me Later choice for update alert */ "Remind Me Later" = "Przypomnij później"; /* Skip This Version choice for update alert */ "Skip This Version" = "Pomiń tę wersję"; /* Install Update choice for update alert */ "Install Update" = "Zainstaluj teraz"; /* Automatically download and install updates in the future option for update alert */ "Automatically download and install updates in the future" = "Automatycznie pobierz i zainstaluj przyszłe uaktualnienia"; /* Check for updates automatically response for update permission dialog */ "Check Automatically" = "Sprawdzaj automatycznie"; /* Don't check for updates automatically response for update permission dialog */ "Don’t Check" = "Nie sprawdzaj"; /* Title question for update permission dialog */ "Check for updates automatically?" = "Sprawdzać automatycznie uaktualnienia?"; /* Include anonymous system profile choice for update permission dialog */ "Include anonymous system profile" = "Załącz anonimowe informacje o systemie"; /* Automatically download and install updates choice for update permission dialog */ "Automatically download and install updates" = "Automatycznie pobierz i zainstaluj uaktualnienia"; /* Anonymous system profile information disclosure for update permission dialog */ "Anonymous system profile information is used to help us plan future development work. Please contact us if you have any questions about this.\n\nThis is the information that would be sent:" = "Anonymous system profile information is used to help us plan future development work. Please contact us if you have any questions about this.\n\nThis is the information that would be sent:"; ================================================ FILE: Sparkle/pt-BR.lproj/Sparkle.strings ================================================ /* No comment provided by engineer. */ "%@ %@ is currently the newest version available." = "%1$@ %2$@ é a versão mais recente disponível."; /* Description text for SUUpdateAlert when the critical update is downloadable. */ "%@ %@ is now available—you have %@. This is an important update; would you like to download it now?" = "O app %1$@ %2$@ está disponível — sua versão é %3$@. Esta é uma atualização importante. Deseja baixá-la agora?"; /* Description text for SUUpdateAlert when the update is downloadable. */ "%@ %@ is now available—you have %@. Would you like to download it now?" = "O app %1$@ %2$@ está disponível — sua versão é %3$@. Deseja baixá-lo agora?"; /* Description text for SUUpdateAlert when the update informational with no download. */ "%@ %@ is now available—you have %@. Would you like to learn more about this update on the web?" = "O app %1$@ %2$@ está disponível — sua versão é %3$@. Deseja saber mais sobre esta atualização na web?"; /* The download progress in a unit of bytes, e.g. 100 KB */ "%@ downloaded" = "%@ baixados"; /* No comment provided by engineer. */ "%@ is now updated to version %@!" = "O app %@ foi atualizado para a versão %@!"; /* No comment provided by engineer. */ "%@ is now updated!" = "O app %@ foi atualizado!"; /* The download progress in units of bytes, e.g. 100 KB of 1,0 MB */ "%@ of %@" = "%1$@ de %2$@"; /* Description text for SUUpdateAlert when the critical update has already been downloaded and ready to install. */ "%1$@ %2$@ has been downloaded and is ready to use! This is an important update; would you like to install it and relaunch %1$@ now?" = "O app %1$@ %2$@ foi baixado e está pronto para uso! Esta é uma atualização importante; deseja instalar e reabrir o app %1$@ agora?"; /* Description text for SUUpdateAlert when the update has already been downloaded and ready to install. */ "%1$@ %2$@ has been downloaded and is ready to use! Would you like to install it and relaunch %1$@ now?" = "O app %1$@ %2$@ foi baixado e está pronto para uso! Deseja instalar e reabrir o app %1$@ agora?"; /* No comment provided by engineer. */ "%1$@ %2$@ is available but your macOS version is too new for this update. This update only supports up to macOS %3$@." = "O app %1$@ %2$@ está disponível, mas a versão do macOS é nova demais para esta atualização, que oferece compatibilidade até o macOS %3$@."; /* No comment provided by engineer. */ "%1$@ %2$@ is available but your macOS version is too old to install it. At least macOS %3$@ is required." = "O app %1$@ %2$@ está disponível, mas a versão do macOS é antiga demais para instalá-lo. Você deve ter, no mínimo, o macOS %3$@."; /* No comment provided by engineer. */ "%1$@ can’t be updated if it’s running from the location it was downloaded to." = "O app %1$@ não pode ser atualizado se ele estiver aberto do local onde foi baixado."; /* No comment provided by engineer. */ "%1$@ can’t be updated because it was opened from a read-only or a temporary location." = "O app %1$@ não pode ser atualizado porque foi aberto de um volume somente leitura ou local temporário."; /* No comment provided by engineer. */ "A new version of %@ is available!" = "Uma nova versão do app %@ está disponível!"; /* No comment provided by engineer. */ "A new version of %@ is ready to install!" = "Uma nova versão do app %@ está pronta para ser instalada!"; /* No comment provided by engineer. */ "An error occurred in retrieving update information. Please try again later." = "Ocorreu um erro ao obter informações de atualização. Tente novamente mais tarde."; /* No comment provided by engineer. */ "An error occurred while connecting to the installer. Please try again later." = "Ocorreu um erro ao conectar-se ao instalador. Tente novamente mais tarde."; /* No comment provided by engineer. */ "An error occurred while downloading the update. Please try again later." = "Ocorreu um erro ao baixar a atualização. Tente novamente mais tarde."; /* No comment provided by engineer. */ "An error occurred while extracting the archive. Please try again later." = "Ocorreu um erro ao extrair o arquivo comprimido. Tente novamente mais tarde."; /* No comment provided by engineer. */ "An error occurred while launching the installer. Please try again later." = "Ocorreu um erro ao abrir o instalador. Tente novamente mais tarde."; /* No comment provided by engineer. */ "An error occurred while parsing the update feed." = "Ocorreu um erro ao analisar o feed de atualização."; /* No comment provided by engineer. */ "An error occurred while running the updater. Please try again later." = "Ocorreu um erro ao abrir o atualizador. Tente novamente mais tarde."; /* No comment provided by engineer. */ "An error occurred while starting the installer. Please try again later." = "Ocorreu um erro ao iniciar o instalador. Tente novamente mais tarde."; /* No comment provided by engineer. */ "An important update to %@ is ready to install" = "Uma atualização importante do app %@ está pronta para ser instalada"; /* No comment provided by engineer. */ "Cancel" = "Cancelar"; /* No comment provided by engineer. */ "Cancel Update" = "Cancelar Atualização"; /* No comment provided by engineer. */ "Checking for updates…" = "Buscando atualizações…"; /* Take care not to overflow the status window. */ "Downloading update…" = "Baixando atualização…"; /* Take care not to overflow the status window. */ "Extracting update…" = "Extraindo atualização…"; /* No comment provided by engineer. */ "Failed to resume installing update." = "Falha ao retomar a instalação da atualização."; /* No comment provided by engineer. */ "Install and Relaunch" = "Instalar e Reabrir"; /* Alternate title for 'Remind Me Later' button when downloaded updates can be resumed */ "Install on Quit" = "Instalar ao Encerrar"; /* Take care not to overflow the status window. */ "Installing update…" = "Instalando atualização…"; /* Alternate title for 'Install Update' button when there's no download in RSS feed. */ "Learn More…" = "Saber Mais…"; /* No comment provided by engineer. */ "OK" = "OK"; /* No comment provided by engineer. */ "Quit %1$@, move it into your Applications folder, relaunch it from there and try again." = "Encerre o app %1$@, mova-o para a pasta Aplicativos, reabra-o e tente novamente."; /* No comment provided by engineer. */ "Ready to Install" = "Pronto para Instalar"; /* No comment provided by engineer. */ "Should %1$@ automatically check for updates? You can always check for updates manually from the %1$@ menu." = "Deseja que o app %1$@ busque atualizações automaticamente? Você também pode buscar atualizações manualmente no menu %1$@."; /* No comment provided by engineer. */ "The update is improperly signed and could not be validated. Please try again later or contact the app developer." = "A atualização foi assinada incorretamente e não pôde ser validada. Tente novamente mais tarde ou contate o desenvolvedor do app."; /* No comment provided by engineer. */ "Unable to Check For Updates" = "Não Foi Possível Buscar Atualizações"; /* No comment provided by engineer. */ "Update Error!" = "Erro de atualização!"; /* No comment provided by engineer. */ "Update Installed" = "Atualização Instalada"; /* No comment provided by engineer. */ "Updating %@" = "Atualizando o app %@"; /* No comment provided by engineer. */ "Use Finder to copy %1$@ to the Applications folder, relaunch it from there, and try again." = "Use o Finder para copiar o app %1$@ para a pasta Aplicativos, reabra-o e tente novamente."; /* No comment provided by engineer. */ "Version History" = "Histórico de Versões"; /* Status message shown when the user checks for updates but is already current or the feed doesn't contain any updates. */ "You’re up to date!" = "O app está atualizado!"; /* No comment provided by engineer. */ "Your macOS version is too new" = "A versão do macOS é nova demais"; /* No comment provided by engineer. */ "Your macOS version is too old" = "A versão do macOS é antiga demais"; /* Software Update title/label */ "Software Update" = "Atualização de Software"; /* Remind Me Later choice for update alert */ "Remind Me Later" = "Mais Tarde"; /* Skip This Version choice for update alert */ "Skip This Version" = "Ignorar Esta Versão"; /* Install Update choice for update alert */ "Install Update" = "Instalar Atualização"; /* Automatically download and install updates in the future option for update alert */ "Automatically download and install updates in the future" = "Baixar e instalar atualizações futuras automaticamente"; /* Check for updates automatically response for update permission dialog */ "Check Automatically" = "Buscar Automaticamente"; /* Don't check for updates automatically response for update permission dialog */ "Don’t Check" = "Não Buscar"; /* Title question for update permission dialog */ "Check for updates automatically?" = "Buscar atualizações automaticamente?"; /* Include anonymous system profile choice for update permission dialog */ "Include anonymous system profile" = "Incluir perfil anônimo do sistema"; /* Automatically download and install updates choice for update permission dialog */ "Automatically download and install updates" = "Baixar e instalar atualizações automaticamente"; /* Anonymous system profile information disclosure for update permission dialog */ "Anonymous system profile information is used to help us plan future development work. Please contact us if you have any questions about this.\n\nThis is the information that would be sent:" = "As informações anônimas do sistema são usadas para nos ajudar a planejar o desenvolvimento futuro do aplicativo. Contate-nos caso tenha dúvidas sobre este procedimento.\n\nAs seguintes informações seriam enviadas:"; ================================================ FILE: Sparkle/pt-PT.lproj/Sparkle.strings ================================================ /* No comment provided by engineer. */ "%@ %@ is currently the newest version available." = "O %1$@ %2$@ é neste momento a versão mais recente disponível."; /* No comment provided by engineer. */ "%@ %@ is currently the newest version available.\n(You are currently running version %@.)" = "O %1$@ %2$@ é neste momento a versão mais recente disponível.\n(Neste momento está na versão %3$@.)"; /* Description text for SUUpdateAlert when the update is downloadable. */ "%@ %@ is now available—you have %@. Would you like to download it now?" = "O %1$@ %2$@ está agora disponível e tem a versão %3$@. Gostaria de o transferir agora?"; /* The download progress in a unit of bytes, e.g. 100 KB */ "%@ downloaded" = "%@ transferido"; /* The download progress in units of bytes, e.g. 100 KB of 1,0 MB */ "%@ of %@" = "%1$@ de %2$@"; /* Description text for SUUpdateAlert when the update has already been downloaded and ready to install. */ "%1$@ %2$@ has been downloaded and is ready to use! Would you like to install it and relaunch %1$@ now?" = "O %1$@ %2$@ foi transferido e está pronto a instalar! Gostaria de o fazer agora e reiniciar o %1$@ posteriormente?"; /* No comment provided by engineer. */ "%1$@ can’t be updated because it was opened from a read-only or a temporary location." = "O %1$@ não pode ser actualizado quando estiver a ser executado a partir de um volume apenas de leitura como uma imagem de disco ou disco óptico."; /* No comment provided by engineer. */ "A new version of %@ is available!" = "Uma nova versão do %@ está dísponível!"; /* No comment provided by engineer. */ "A new version of %@ is ready to install!" = "Uma nova versão do %@ está pronta a instalar!"; /* No comment provided by engineer. */ "An error occurred in retrieving update information. Please try again later." = "Ocorreu um erro ao recolher informação sobre as actualizações disponíveis. Por favor tente novamente mais tarde."; /* No comment provided by engineer. */ "An error occurred while downloading the update. Please try again later." = "Ocorreu um erro ao transferir a actualização. Por favor novamente tente mais tarde."; /* No comment provided by engineer. */ "An error occurred while extracting the archive. Please try again later." = "Ocorreu um erro ao extrair o arquivo. Por favor novamente tente mais tarde."; /* No comment provided by engineer. */ "An error occurred while parsing the update feed." = "Ocorrer um erro ao processar o feed de actualização."; /* No comment provided by engineer. */ "Cancel" = "Cancelar"; /* No comment provided by engineer. */ "Cancel Update" = "Cancelar actualização"; /* No comment provided by engineer. */ "Checking for updates…" = "A procurar actualizações…"; /* Take care not to overflow the status window. */ "Downloading update…" = "A transferir actualização…"; /* Take care not to overflow the status window. */ "Extracting update…" = "A extrair actualização…"; /* No comment provided by engineer. */ "Install and Relaunch" = "Instalar e reiniciar"; /* Take care not to overflow the status window. */ "Installing update…" = "A instalar actualização…"; /* No comment provided by engineer. */ "OK" = "OK"; /* No comment provided by engineer. */ "Ready to Install" = "Pronto para instalar"; /* No comment provided by engineer. */ "Should %1$@ automatically check for updates? You can always check for updates manually from the %1$@ menu." = "Deverá o %1$@ procurar por actualizações automaticamente? Pode sempre procurar actualizações manualmente a partir do menu do %1$@."; /* No comment provided by engineer. */ "Update Error!" = "Erro na actualização!"; /* No comment provided by engineer. */ "Updating %@" = "A actualizar o %@"; /* No comment provided by engineer. */ "Use Finder to copy %1$@ to the Applications folder, relaunch it from there, and try again." = "Use o Finder para copiar o %1$@ para a sua pasta Aplicações, reinicie-o aí e tente novamente."; /* Status message shown when the user checks for updates but is already current or the feed doesn't contain any updates. */ "You’re up to date!" = "Está actualizado!"; /* Software Update title/label */ "Software Update" = "Actualização de Software"; /* Remind Me Later choice for update alert */ "Remind Me Later" = "Lembrar mais tarde"; /* Skip This Version choice for update alert */ "Skip This Version" = "Saltar esta versão"; /* Install Update choice for update alert */ "Install Update" = "Instalar actualização"; /* Automatically download and install updates in the future option for update alert */ "Automatically download and install updates in the future" = "No futuro, transferir e instalar actualizações automaticamente"; /* Check for updates automatically response for update permission dialog */ "Check Automatically" = "Procurar automaticamente"; /* Don't check for updates automatically response for update permission dialog */ "Don’t Check" = "Não procurar"; /* Title question for update permission dialog */ "Check for updates automatically?" = "Procurar actualizações automaticamente?"; /* Include anonymous system profile choice for update permission dialog */ "Include anonymous system profile" = "Incluir perfil de sistema anónimo"; /* Automatically download and install updates choice for update permission dialog */ "Automatically download and install updates" = "Transferir e instalar actualizações automaticamente"; /* Anonymous system profile information disclosure for update permission dialog */ "Anonymous system profile information is used to help us plan future development work. Please contact us if you have any questions about this.\n\nThis is the information that would be sent:" = "A informação anónima do perfil de sistema é usada para no futuro nos ajudar a planear o trabalho de desenvolvimento. Por favor contacte-nos se tiver alguma questão acerca deste assunto.\n\nEsta é a informação que seria enviada:"; ================================================ FILE: Sparkle/ro.lproj/Sparkle.strings ================================================ /* No comment provided by engineer. */ "%@ %@ is currently the newest version available." = "%1$@ %2$@ este cea ultima versiune disponibilă."; /* No comment provided by engineer. */ "%@ %@ is currently the newest version available.\n(You are currently running version %@.)" = "%1$@ %2$@ este cea ultima versiune disponibilă.\n(You are currently running version %3$@.)"; /* Description text for SUUpdateAlert when the update is downloadable. */ "%@ %@ is now available—you have %@. Would you like to download it now?" = "%1$@ %2$@ este disponibilă—tu ai %3$@. Dorești să o descărcarci acum?"; /* The download progress in a unit of bytes, e.g. 100 KB */ "%@ downloaded" = "%@ descărcat"; /* The download progress in units of bytes, e.g. 100 KB of 1,0 MB */ "%@ of %@" = "%1$@ din %2$@"; /* Description text for SUUpdateAlert when the critical update has already been downloaded and ready to install. */ "%1$@ %2$@ has been downloaded and is ready to use! This is an important update; would you like to install it and relaunch %1$@ now?" = "%1$@ %2$@ a fost descărcată și este gata de utilizare! Aceasta este o actualizare importantă; dorești să o instalezi și să relansați %1$@ acum?"; /* Description text for SUUpdateAlert when the update has already been downloaded and ready to install. */ "%1$@ %2$@ has been downloaded and is ready to use! Would you like to install it and relaunch %1$@ now?" = "%1$@ %2$@ a fost descărcată și este gata de utilizare! Dorești să o instalezi și să relansați %1$@ acum?"; /* No comment provided by engineer. */ "%1$@ can’t be updated because it was opened from a read-only or a temporary location." = "%1$@ nu poate fi actualizată atunci când a fost pornită de pe un volum read-only ca o imagine disc sau o unitate optică."; /* No comment provided by engineer. */ "A new version of %@ is available!" = "O nouă versiune pentru %@ este disponibilă!"; /* No comment provided by engineer. */ "A new version of %@ is ready to install!" = "O nouă versiune pentru %@ este gata de instalare!"; /* No comment provided by engineer. */ "An error occurred in retrieving update information. Please try again later." = "A apărut o eroare în timpul preluari informaţiilor pentru actualizare. Te rog încercă din nou mai târziu."; /* No comment provided by engineer. */ "An error occurred while downloading the update. Please try again later." = "A apărut o eroare în timp ce se descărca actualizărea. Te rog încercă din nou mai târziu."; /* No comment provided by engineer. */ "An error occurred while extracting the archive. Please try again later." = "A apărut o eroare în timpul dezarhivării. Te rog încercă din nou mai târziu."; /* No comment provided by engineer. */ "An error occurred while parsing the update feed." = "A apărut o eroare în timpul citiri feed-ului de actualizare."; /* No comment provided by engineer. */ "An important update to %@ is ready to install" = "O actualizare importantă pentru %@ este gata pentru a fi instalată"; /* No comment provided by engineer. */ "Cancel" = "Anulează"; /* No comment provided by engineer. */ "Cancel Update" = "Anulează actualizarea"; /* No comment provided by engineer. */ "Checking for updates…" = "Verifică de actualizări…"; /* Take care not to overflow the status window. */ "Downloading update…" = "Descarcă actualizarea…"; /* Take care not to overflow the status window. */ "Extracting update…" = "Dezarhivează actualizarea…"; /* No comment provided by engineer. */ "Install and Relaunch" = "Instalează și Redeschide"; /* Take care not to overflow the status window. */ "Installing update…" = "Instalează actualizarea…"; /* Alternate title for 'Install Update' button when there's no download in RSS feed. */ "Learn More…" = "Află mai multe…"; /* No comment provided by engineer. */ "OK" = "OK"; /* No comment provided by engineer. */ "Ready to Install" = "Pregatit pentru a instala"; /* No comment provided by engineer. */ "Should %1$@ automatically check for updates? You can always check for updates manually from the %1$@ menu." = "%1$@ ar trebui ca să caute în mod automat pentru actualizări? Puteți verifica mereu pentru actualizări din meniul %1$@."; /* No comment provided by engineer. */ "Update Error!" = "Eroare Actualizare!"; /* No comment provided by engineer. */ "Updating %@" = "Actualizează %@"; /* No comment provided by engineer. */ "Use Finder to copy %1$@ to the Applications folder, relaunch it from there, and try again." = "Mută %1$@ în directorul Aplicații, repornește-o de acolo și încearcă din nou."; /* Status message shown when the user checks for updates but is already current or the feed doesn't contain any updates. */ "You’re up to date!" = "Ai ultima versiune!"; /* Software Update title/label */ "Software Update" = "Actualizarea aplicației"; /* Remind Me Later choice for update alert */ "Remind Me Later" = "Amintește-mi mai târziu"; /* Skip This Version choice for update alert */ "Skip This Version" = "Sari peste…"; /* Install Update choice for update alert */ "Install Update" = "Instalează actualizarea"; /* Automatically download and install updates in the future option for update alert */ "Automatically download and install updates in the future" = "În viitor descarcă și instalează în automat actualizările"; /* Check for updates automatically response for update permission dialog */ "Check Automatically" = "Verifică în mod automat"; /* Don't check for updates automatically response for update permission dialog */ "Don’t Check" = "Nu verifica"; /* Title question for update permission dialog */ "Check for updates automatically?" = "Verifică pentru actualizări în mod automat"; /* Include anonymous system profile choice for update permission dialog */ "Include anonymous system profile" = "Include profil anomin de sistem"; /* Automatically download and install updates choice for update permission dialog */ "Automatically download and install updates" = "Descarcă și instalează în automat actualizările"; /* Anonymous system profile information disclosure for update permission dialog */ "Anonymous system profile information is used to help us plan future development work. Please contact us if you have any questions about this.\n\nThis is the information that would be sent:" = "Anonymous system profile information is used to help us plan future development work. Please contact us if you have any questions about this.\n\nThis is the information that would be sent:"; ================================================ FILE: Sparkle/ru.lproj/Sparkle.strings ================================================ /* No comment provided by engineer. */ "%@ %@ is currently the newest version available." = "В настоящий момент %1$@ %2$@ является новейшей версией."; /* No comment provided by engineer. */ "%@ %@ is currently the newest version available.\n(You are currently running version %@.)" = "В настоящий момент %1$@ %2$@ является новейшей версией.\n(У вас установлена %3$@.)"; /* Description text for SUUpdateAlert when the update is downloadable. */ "%@ %@ is now available—you have %@. Would you like to download it now?" = "%1$@ %2$@ теперь доступен – у вас установлен %3$@. Хотите загрузить его сейчас?"; /* The download progress in a unit of bytes, e.g. 100 KB */ "%@ downloaded" = "%@ загружено"; /* The download progress in units of bytes, e.g. 100 KB of 1,0 MB */ "%@ of %@" = "%1$@ из %2$@"; /* Description text for SUUpdateAlert when the update has already been downloaded and ready to install. */ "%1$@ %2$@ has been downloaded and is ready to use! Would you like to install it and relaunch %1$@ now?" = "%1$@ %2$@ загружен и готов к использованию! Хотите установить и перезапустить %1$@?"; /* No comment provided by engineer. */ "%1$@ can’t be updated because it was opened from a read-only or a temporary location." = "Не удаётся обновить %1$@ с тома, предназначенного только для чтения."; /* No comment provided by engineer. */ "A new version of %@ is available!" = "Имеется новая версия %@!"; /* No comment provided by engineer. */ "A new version of %@ is ready to install!" = "Новая версия %@ готова к установке!"; /* No comment provided by engineer. */ "An error occurred in retrieving update information. Please try again later." = "Произошла ошибка при извлечении информации обновления. Повторите попытку позже."; /* No comment provided by engineer. */ "An error occurred while downloading the update. Please try again later." = "Произошла ошибка при загрузке обновления. Повторите попытку позже."; /* No comment provided by engineer. */ "An error occurred while extracting the archive. Please try again later." = "Произошла ошибка при извлечении архива. Повторите попытку позже."; /* No comment provided by engineer. */ "An error occurred while parsing the update feed." = "Произошла ошибка при извлечении канала обновления."; /* No comment provided by engineer. */ "Cancel" = "Отменить"; /* No comment provided by engineer. */ "Cancel Update" = "Отменить обновление"; /* No comment provided by engineer. */ "Checking for updates…" = "Проверяю наличие обновлений…"; /* Take care not to overflow the status window. */ "Downloading update…" = "Загружаю обновление…"; /* Take care not to overflow the status window. */ "Extracting update…" = "Извлекаю обновление…"; /* No comment provided by engineer. */ "Install and Relaunch" = "Установить и перезапустить"; /* Take care not to overflow the status window. */ "Installing update…" = "Устанавливаю обновление…"; /* Alternate title for 'Install Update' button when there's no download in RSS feed. */ "Learn More…" = "Узнать больше…"; /* No comment provided by engineer. */ "OK" = "OK"; /* No comment provided by engineer. */ "Ready to Install" = "Готов к установке"; /* No comment provided by engineer. */ "Should %1$@ automatically check for updates? You can always check for updates manually from the %1$@ menu." = "Должен ли %1$@ выполнять автоматическую проверку обновлений? Вы всегда можете выполнять проверку обновлений вручную в меню %1$@."; /* No comment provided by engineer. */ "Update Error!" = "Ошибка при обновлении!"; /* No comment provided by engineer. */ "Updating %@" = "Обновляю %@"; /* No comment provided by engineer. */ "Use Finder to copy %1$@ to the Applications folder, relaunch it from there, and try again." = "Переместите %1$@ в Папку приложений, перезапустите его оттуда и повторите попытку."; /* Status message shown when the user checks for updates but is already current or the feed doesn't contain any updates. */ "You’re up to date!" = "У вас все обновлено!"; /* Software Update title/label */ "Software Update" = "Обновление программного обеспечения"; /* Remind Me Later choice for update alert */ "Remind Me Later" = "Напоминать позже"; /* Skip This Version choice for update alert */ "Skip This Version" = "Пропустить эту версию"; /* Install Update choice for update alert */ "Install Update" = "Установить обновление"; /* Automatically download and install updates in the future option for update alert */ "Automatically download and install updates in the future" = "Автоматически загружать и устанавливать обновления в будущем"; /* Check for updates automatically response for update permission dialog */ "Check Automatically" = "Проверять автоматически"; /* Don't check for updates automatically response for update permission dialog */ "Don’t Check" = "Не проверять"; /* Title question for update permission dialog */ "Check for updates automatically?" = "Выполнять автоматическую проверку наличия обновлений?"; /* Include anonymous system profile choice for update permission dialog */ "Include anonymous system profile" = "Включить анонимный профиль системы"; /* Automatically download and install updates choice for update permission dialog */ "Automatically download and install updates" = "Автоматически загружать и устанавливать обновления"; /* Anonymous system profile information disclosure for update permission dialog */ "Anonymous system profile information is used to help us plan future development work. Please contact us if you have any questions about this.\n\nThis is the information that would be sent:" = "Использование анонимного профиля системы помогает нам в планировании будущей работы по разработке. Если у вас есть какие-либо вопросы по этой теме, обращайтесь к нам.\n\nЭто информация, предназначенная для отправления:"; ================================================ FILE: Sparkle/sk.lproj/Sparkle.strings ================================================ /* No comment provided by engineer. */ "%@ %@ is currently the newest version available." = "%1$@ %2$@\nje najnovšia dostupná verzia."; /* No comment provided by engineer. */ "%@ %@ is currently the newest version available.\n(You are currently running version %@.)" = "%1$@ %2$@\nje najnovšia dostupná verzia.\n(You are currently running version %3$@.)"; /* Description text for SUUpdateAlert when the update is downloadable. */ "%@ %@ is now available—you have %@. Would you like to download it now?" = "Je dostupná aplikácia %1$@ %2$@ — máte %3$@. Chcete ju prevziať teraz?"; /* The download progress in a unit of bytes, e.g. 100 KB */ "%@ downloaded" = "%@ prevzaté"; /* The download progress in units of bytes, e.g. 100 KB of 1,0 MB */ "%@ of %@" = "%1$@ z %2$@"; /* Description text for SUUpdateAlert when the update has already been downloaded and ready to install. */ "%1$@ %2$@ has been downloaded and is ready to use! Would you like to install it and relaunch %1$@ now?" = "Aplikácia %1$@ %2$@ bola prevzatá a je pripravená na použitie! Chcete teraz nainštalovať a následne znovu spustiť %1$@?"; /* No comment provided by engineer. */ "%1$@ can’t be updated because it was opened from a read-only or a temporary location." = "Aplikáciu %1$@ nemožno aktualizovať, ak je spustená zo zväzku s právami len na čítanie (napríklad z obrazu disku alebo optického disku)."; /* No comment provided by engineer. */ "A new version of %@ is available!" = "Je dostupná nová verzia aplikácie %@!"; /* No comment provided by engineer. */ "A new version of %@ is ready to install!" = "Nová verzia aplikácie %@ je pripravená na inštaláciu!"; /* No comment provided by engineer. */ "An error occurred in retrieving update information. Please try again later." = "Pri získavaní informácie o aktualizácii sa vyskytla chyba. Skúste neskôr."; /* No comment provided by engineer. */ "An error occurred while downloading the update. Please try again later." = "Pri preberaní aktualizácie sa vyskytla chyba. Skúste neskôr."; /* No comment provided by engineer. */ "An error occurred while extracting the archive. Please try again later." = "Pri rozbaľovaní archívu sa vyskytla chyba. Skúste neskôr."; /* No comment provided by engineer. */ "An error occurred while parsing the update feed." = "Pri analyzovaní aktualizácie sa vyskytla chyba."; /* No comment provided by engineer. */ "Cancel" = "Zrušiť"; /* No comment provided by engineer. */ "Cancel Update" = "Zrušiť aktualizáciu"; /* No comment provided by engineer. */ "Checking for updates…" = "Kontrolujú sa aktualizácie…"; /* Take care not to overflow the status window. */ "Downloading update…" = "Preberá sa aktualizácia…"; /* Take care not to overflow the status window. */ "Extracting update…" = "Rozbaľuje sa aktualizácia…"; /* No comment provided by engineer. */ "Install and Relaunch" = "Inštalovať a znovu spustiť"; /* Take care not to overflow the status window. */ "Installing update…" = "Inštaluje sa aktualizácia…"; /* No comment provided by engineer. */ "OK" = "OK"; /* No comment provided by engineer. */ "Ready to Install" = "Pripravené na inštaláciu"; /* No comment provided by engineer. */ "Should %1$@ automatically check for updates? You can always check for updates manually from the %1$@ menu." = "Môže %1$@ automaticky kontrolovať aktualizácie? Kedykoľvek ich môžete z ponuky %1$@ skontrolovať aj manuálne."; /* No comment provided by engineer. */ "Update Error!" = "Chyba aktualizácie!"; /* No comment provided by engineer. */ "Updating %@" = "Aktualizuje sa %@"; /* No comment provided by engineer. */ "Use Finder to copy %1$@ to the Applications folder, relaunch it from there, and try again." = "Presuňte aplikáciu %1$@ do priečinka Applications, spustite ju odtiaľ a potom znova skúste aktualizáciu."; /* Status message shown when the user checks for updates but is already current or the feed doesn't contain any updates. */ "You’re up to date!" = "Máte aktuálnu verziu!"; /* Software Update title/label */ "Software Update" = "Aktualizácia softvéru"; /* Remind Me Later choice for update alert */ "Remind Me Later" = "Pripomenúť neskôr"; /* Skip This Version choice for update alert */ "Skip This Version" = "Vynechať túto verziu"; /* Install Update choice for update alert */ "Install Update" = "Nainštalovať"; /* Automatically download and install updates in the future option for update alert */ "Automatically download and install updates in the future" = "V budúcnosti aktualizácie preberať a inštalovať automaticky"; /* Check for updates automatically response for update permission dialog */ "Check Automatically" = "Kontrolovať automaticky"; /* Don't check for updates automatically response for update permission dialog */ "Don’t Check" = "Nekontrolovať"; /* Title question for update permission dialog */ "Check for updates automatically?" = "Kontrolovať aktualizácie automaticky?"; /* Include anonymous system profile choice for update permission dialog */ "Include anonymous system profile" = "Zahrnúť anonymný profil systému"; /* Automatically download and install updates choice for update permission dialog */ "Automatically download and install updates" = "Automatically download and install updates"; /* Anonymous system profile information disclosure for update permission dialog */ "Anonymous system profile information is used to help us plan future development work. Please contact us if you have any questions about this.\n\nThis is the information that would be sent:" = "Anonymný profil systému nám umožní zlepšiť plánovanie budúceho vývoja aplikácie. Ak máte ohľadom tohto akékoľvek otázky, neváhajte a kontaktujte nás.\n\nOdosielané budú nasledujúce informácie:"; ================================================ FILE: Sparkle/sl.lproj/Sparkle.strings ================================================ /* No comment provided by engineer. */ "%@ %@ is currently the newest version available." = "%1$@ %2$@ je najnovejša verzija programa."; /* No comment provided by engineer. */ "%@ %@ is currently the newest version available.\n(You are currently running version %@.)" = "%1$@ %2$@ je najnovejša verzija programa.\n(You are currently running version %3$@.)"; /* Description text for SUUpdateAlert when the update is downloadable. */ "%@ %@ is now available—you have %@. Would you like to download it now?" = "Na voljo je %1$@ %2$@ — vi imate %3$@. Ga želite prenesti s spleta sedaj?"; /* The download progress in a unit of bytes, e.g. 100 KB */ "%@ downloaded" = "prenešenih je %@"; /* The download progress in units of bytes, e.g. 100 KB of 1,0 MB */ "%@ of %@" = "%1$@ od %2$@"; /* Description text for SUUpdateAlert when the update has already been downloaded and ready to install. */ "%1$@ %2$@ has been downloaded and is ready to use! Would you like to install it and relaunch %1$@ now?" = "%1$@ %2$@ je bil uspešno prenešen s spleta in je pripravljen na namestitev. Ga želite namestiti in ponovno zagnati takoj?"; /* No comment provided by engineer. */ "%1$@ can’t be updated because it was opened from a read-only or a temporary location." = "Programa %1$@ ni mogoče posodobiti, ker ga poganjate iz lokacije, kamor pisanje ni dovoljeno (pogosto je to slika diska dmg ali optična enota)."; /* No comment provided by engineer. */ "A new version of %@ is available!" = "Na voljo je nova verzija programa %@."; /* No comment provided by engineer. */ "A new version of %@ is ready to install!" = "Najnovejša verzija programa %@ je že nameščena."; /* No comment provided by engineer. */ "An error occurred in retrieving update information. Please try again later." = "Med iskanjem posodobitev je prišlo do napake. Prosimo poskusite ponovno čez nekaj časa."; /* No comment provided by engineer. */ "An error occurred while downloading the update. Please try again later." = "Med prenašanje posodobitve s spleta je prišlo do napake. Prosimo poskusite ponovno čez nekaj časa."; /* No comment provided by engineer. */ "An error occurred while extracting the archive. Please try again later." = "Med odpiranjem arhiva je prišlo do napake. Prosimo poskusite ponovno čez nekaj časa."; /* No comment provided by engineer. */ "An error occurred while parsing the update feed." = "Napaka pri interpretaciji RSS vira s posodobitvami."; /* No comment provided by engineer. */ "Cancel" = "Prekliči"; /* No comment provided by engineer. */ "Cancel Update" = "Prekliči posodabljanje"; /* No comment provided by engineer. */ "Checking for updates…" = "Iskanje posodobitev …"; /* Take care not to overflow the status window. */ "Downloading update…" = "Prenašanje posodobitve …"; /* Take care not to overflow the status window. */ "Extracting update…" = "Razpakiranje posodobitve …"; /* No comment provided by engineer. */ "Install and Relaunch" = "Namesti in ponovno zaženi"; /* Take care not to overflow the status window. */ "Installing update…" = "Nameščanje posodobitve …"; /* No comment provided by engineer. */ "OK" = "V redu"; /* No comment provided by engineer. */ "Ready to Install" = "Pripravljen na namestitev"; /* No comment provided by engineer. */ "Should %1$@ automatically check for updates? You can always check for updates manually from the %1$@ menu." = "Naj %1$@ samodejno preverja, če so na voljo posodobitve? To lahko kadarkoli preverite tudi sami iz menija za %1$@."; /* No comment provided by engineer. */ "Update Error!" = "Napaka pri posodabljanju"; /* No comment provided by engineer. */ "Updating %@" = "Posodabljam %@"; /* No comment provided by engineer. */ "Use Finder to copy %1$@ to the Applications folder, relaunch it from there, and try again." = "Poskusite %1$@ premakniti v direktorij z aplikacijami (Applications), ga ponovno zagnati in šele nato posodobiti."; /* Status message shown when the user checks for updates but is already current or the feed doesn't contain any updates. */ "You’re up to date!" = "Uporabljate zadnjo verzijo."; /* Software Update title/label */ "Software Update" = "Posodabljanje programske opreme"; /* Remind Me Later choice for update alert */ "Remind Me Later" = "Spomni me kasneje"; /* Skip This Version choice for update alert */ "Skip This Version" = "Preskoči to verzijo"; /* Install Update choice for update alert */ "Install Update" = "Namesti posodobitev"; /* Automatically download and install updates in the future option for update alert */ "Automatically download and install updates in the future" = "V prihodnje samodejno nameščaj posodobitve"; /* Check for updates automatically response for update permission dialog */ "Check Automatically" = "Samodejno preverjaj"; /* Don't check for updates automatically response for update permission dialog */ "Don’t Check" = "Ne preverjaj"; /* Title question for update permission dialog */ "Check for updates automatically?" = "Naj občasno preverjam, če so na voljo posodobitve?"; /* Include anonymous system profile choice for update permission dialog */ "Include anonymous system profile" = "Vključi anonimni profil sistema"; /* Automatically download and install updates choice for update permission dialog */ "Automatically download and install updates" = "Samodejno namestite posodobitve"; /* Anonymous system profile information disclosure for update permission dialog */ "Anonymous system profile information is used to help us plan future development work. Please contact us if you have any questions about this.\n\nThis is the information that would be sent:" = "Anonimni profil sistema se uporablja za načrtovanje nadaljnega razvoja programa. V primeru vprašanj nas lahko kontaktirate.\n\nPošljejo se sledeče informacije:"; ================================================ FILE: Sparkle/sv.lproj/Sparkle.strings ================================================ /* No comment provided by engineer. */ "%@ %@ is currently the newest version available." = "%1$@ %2$@ är för närvarande den senaste tillgängliga versionen."; /* No comment provided by engineer. */ "%@ %@ is currently the newest version available.\n(You are currently running version %@.)" = "%1$@ %2$@ är för närvarande den senaste tillgängliga versionen.\n(You are currently running version %3$@.)"; /* Description text for SUUpdateAlert when the update is downloadable. */ "%@ %@ is now available—you have %@. Would you like to download it now?" = "%1$@ %2$@ finns nu tillgänglig—du har %3$@. Vill hämta uppdateringen nu?"; /* The download progress in a unit of bytes, e.g. 100 KB */ "%@ downloaded" = "%@ hämtat"; /* The download progress in units of bytes, e.g. 100 KB of 1,0 MB */ "%@ of %@" = "%1$@ av %2$@"; /* Description text for SUUpdateAlert when the update has already been downloaded and ready to install. */ "%1$@ %2$@ has been downloaded and is ready to use! Would you like to install it and relaunch %1$@ now?" = "%1$@ %2$@ har hämtats och är klar att använda! Vill du installera det och starta om %1$@ nu?"; /* No comment provided by engineer. */ "A new version of %@ is available!" = "En ny version av %@ finns tillgänglig!"; /* No comment provided by engineer. */ "A new version of %@ is ready to install!" = "En ny version av %@ är redo att installeras!"; /* No comment provided by engineer. */ "An error occurred in retrieving update information. Please try again later." = "Ett fel uppstod vid hämtning av information om uppdateringar. Försök igen senare."; /* No comment provided by engineer. */ "An error occurred while downloading the update. Please try again later." = "Ett fel uppstod vid hämtning av uppdateringen. Försök igen senare."; /* No comment provided by engineer. */ "An error occurred while extracting the archive. Please try again later." = "Ett fel uppstod vid extrahering av arkivet. Försök igen senare."; /* No comment provided by engineer. */ "An error occurred while parsing the update feed." = "Ett fel uppstod vid tolkning av uppdateringslänken."; /* No comment provided by engineer. */ "Cancel" = "Avbryt"; /* No comment provided by engineer. */ "Cancel Update" = "Avbryt uppdatering"; /* No comment provided by engineer. */ "Checking for updates…" = "Letar efter uppdateringar…"; /* Take care not to overflow the status window. */ "Downloading update…" = "Hämtar uppdatering…"; /* Take care not to overflow the status window. */ "Extracting update…" = "Extraherar uppdatering…"; /* No comment provided by engineer. */ "Install and Relaunch" = "Installera och starta om"; /* Take care not to overflow the status window. */ "Installing update…" = "Installerar uppdatering…"; /* Alternate title for 'Install Update' button when there's no download in RSS feed. */ "Learn More…" = "Mer info…"; /* No comment provided by engineer. */ "OK" = "OK"; /* No comment provided by engineer. */ "Ready to Install" = "Redo att installera"; /* No comment provided by engineer. */ "Should %1$@ automatically check for updates? You can always check for updates manually from the %1$@ menu." = "Ska %1$@ leta efter uppdateringar automatiskt? Du kan alltid leta efter uppdateringar manuellt från %1$@-menyn."; /* No comment provided by engineer. */ "Update Error!" = "Uppdateringsfel!"; /* No comment provided by engineer. */ "Updating %@" = "Uppdaterar %@"; /* No comment provided by engineer. */ "Use Finder to copy %1$@ to the Applications folder, relaunch it from there, and try again." = "Flytta %1$@ till mappen Program, starta om det därifrån, och försök igen."; /* Status message shown when the user checks for updates but is already current or the feed doesn't contain any updates. */ "You’re up to date!" = "Du är uppdaterad!"; /* Software Update title/label */ "Software Update" = "Programuppdatering"; /* Remind Me Later choice for update alert */ "Remind Me Later" = "Påminn mig senare"; /* Skip This Version choice for update alert */ "Skip This Version" = "Hoppa över denna version"; /* Install Update choice for update alert */ "Install Update" = "Installera uppdatering"; /* Automatically download and install updates in the future option for update alert */ "Automatically download and install updates in the future" = "Hämta och installera nya uppdateringar automatiskt i framtiden."; /* Check for updates automatically response for update permission dialog */ "Check Automatically" = "Kontrollera automatiskt"; /* Don't check for updates automatically response for update permission dialog */ "Don’t Check" = "Kontrollera inte"; /* Title question for update permission dialog */ "Check for updates automatically?" = "Leta efter uppdateringar automatiskt?\n"; /* Include anonymous system profile choice for update permission dialog */ "Include anonymous system profile" = "Inkludera anonym systemprofil"; /* Automatically download and install updates choice for update permission dialog */ "Automatically download and install updates" = "Hämta och installera nya uppdateringar automatiskt."; /* Anonymous system profile information disclosure for update permission dialog */ "Anonymous system profile information is used to help us plan future development work. Please contact us if you have any questions about this.\n\nThis is the information that would be sent:" = "Anonym systemprofilinformation används för att hjälpa oss att planera framtida utvecklingsarbete. Vänligen kontakta oss ifall du har några frågot om detta.\n\nDetta är informationen som skulle sändas:"; ================================================ FILE: Sparkle/th.lproj/Sparkle.strings ================================================ /* No comment provided by engineer. */ "%@ %@ is currently the newest version available." = "%1$@ %2$@ เป็นเวอร์ชั่นใหม่ล่าสุดแล้ว"; /* No comment provided by engineer. */ "%@ %@ is currently the newest version available.\n(You are currently running version %@.)" = "%1$@ %2$@ เป็นเวอร์ชั่นใหม่ล่าสุดแล้ว\n(You are currently running version %3$@.)"; /* Description text for SUUpdateAlert when the update is downloadable. */ "%@ %@ is now available—you have %@. Would you like to download it now?" = "%1$@ %2$@ พร้อมให้ดาวน์โหลดแล้ว (คุณมีเวอร์ชั่น %3$@) ต้องการดาวน์โหลดเลยหรือไม่"; /* The download progress in a unit of bytes, e.g. 100 KB */ "%@ downloaded" = "ดาวน์โหลด %@ เสร็จสิ้น"; /* The download progress in units of bytes, e.g. 100 KB of 1,0 MB */ "%@ of %@" = "%1$@ จาก %2$@"; /* Description text for SUUpdateAlert when the update has already been downloaded and ready to install. */ "%1$@ %2$@ has been downloaded and is ready to use! Would you like to install it and relaunch %1$@ now?" = "%1$@ %2$@ ได้ถูกดาวน์โหลดและพร้อมใช้งานแล้ว คุณต้องการติดตั้งและเริ่ม %1$@ ใหม่เดี๋ยวนี้หรือไม่"; /* No comment provided by engineer. */ "%1$@ can’t be updated because it was opened from a read-only or a temporary location." = "%1$@ ไม่สามารถรับการอัพเดทได้เมื่อถูกเรียกจากดิสก์แบบอ่านอย่างเดียวหรือซีดีรอม ย้าย"; /* No comment provided by engineer. */ "A new version of %@ is available!" = "%@ มีเวอร์ชั่นใหม่!"; /* No comment provided by engineer. */ "A new version of %@ is ready to install!" = "เวอร์ชั่นใหม่ของ %@ พร้อมสำหรับการติดตั้งแล้ว"; /* No comment provided by engineer. */ "An error occurred in retrieving update information. Please try again later." = "เกิดข้อผิดพลาดระหว่างการรับข้อมูลอัพเดท กรุณาลองใหม่ในภายหลัง"; /* No comment provided by engineer. */ "An error occurred while downloading the update. Please try again later." = "เกิดข้อผิดพลาดระหว่างพยายามดาวน์โหลดอัพเดท กรุณาลองใหม่ในภายหลัง"; /* No comment provided by engineer. */ "An error occurred while extracting the archive. Please try again later." = "เกิดข้อผิดพลาดระหว่างการแตกไฟล์ที่ถูกบีบอัด กรุณาลองใหม่ในภายหลัง"; /* No comment provided by engineer. */ "An error occurred while parsing the update feed." = "เกิดข้อผิดพลาดระหว่างการประมวลผลฟีดอัพเดท กรุณาลองใหม่ในภายหลัง"; /* No comment provided by engineer. */ "Cancel" = "ยกเลิก"; /* No comment provided by engineer. */ "Cancel Update" = "ยกเลิกอัพเดท"; /* No comment provided by engineer. */ "Checking for updates…" = "ตรวจสอบอัพเดท…"; /* Take care not to overflow the status window. */ "Downloading update…" = "กำลังดาวน์โหลดอัพเดท…"; /* Take care not to overflow the status window. */ "Extracting update…" = "กำลังแตกไฟล์อัพเดท…"; /* No comment provided by engineer. */ "Install and Relaunch" = "ติดตั้งและเริ่มแอปพลิเคชันใหม่"; /* Take care not to overflow the status window. */ "Installing update…" = "กำลังติดตั้งอัพเดท…"; /* Alternate title for 'Install Update' button when there's no download in RSS feed. */ "Learn More…" = "เรียนรู้เพิ่มเติม…"; /* No comment provided by engineer. */ "OK" = "ตกลง"; /* No comment provided by engineer. */ "Ready to Install" = "พร้อมติดตั้ง"; /* No comment provided by engineer. */ "Should %1$@ automatically check for updates? You can always check for updates manually from the %1$@ menu." = "คุณต้องการให้ %1$@ ตรวจสอบอัพเดทโดยอัตโนมัติหรือไม่ คุณสามารถเริ่มการตรวจสอบอัพเดทด้วยตนเองได้ทุกเมื่อจากเมนูของ %1$@"; /* No comment provided by engineer. */ "Update Error!" = "อัพเดทผิดพลาด!"; /* No comment provided by engineer. */ "Updating %@" = "กำลังอัพเดท %@"; /* No comment provided by engineer. */ "Use Finder to copy %1$@ to the Applications folder, relaunch it from there, and try again." = "%1$@ ไปยังโฟลเดอร์แอปพลิเคชัน และเรียกใช้งาน จากนั้นลองใหม่อีกครั้ง"; /* Status message shown when the user checks for updates but is already current or the feed doesn't contain any updates. */ "You’re up to date!" = "คุณมีเวอร์ชั่นล่าสุดแล้ว!"; /* Software Update title/label */ "Software Update" = "อัพเดทซอฟต์แวร์"; /* Remind Me Later choice for update alert */ "Remind Me Later" = "เตือนในภายหลัง"; /* Skip This Version choice for update alert */ "Skip This Version" = "ข้ามเวอร์ชั่นนี้"; /* Install Update choice for update alert */ "Install Update" = "ติดตั้งอัพเดท"; /* Automatically download and install updates in the future option for update alert */ "Automatically download and install updates in the future" = "ดาวน์โหลดและติดตั้งอัพเดทโดยอัตโนมัติในอนาคต"; /* Check for updates automatically response for update permission dialog */ "Check Automatically" = "ตรวจสอบโดยอัตโนมัติ"; /* Don't check for updates automatically response for update permission dialog */ "Don’t Check" = "ไม่ต้องตรวจสอบ"; /* Title question for update permission dialog */ "Check for updates automatically?" = "ตรวจสอบอัพเดทอัตโนมัติ?"; /* Include anonymous system profile choice for update permission dialog */ "Include anonymous system profile" = "ส่งข้อมูลระบบแบบนิรนาม"; /* Automatically download and install updates choice for update permission dialog */ "Automatically download and install updates" = "ดาวน์โหลดและติดตั้งอัพเดทโดยอัตโนมัติ"; /* Anonymous system profile information disclosure for update permission dialog */ "Anonymous system profile information is used to help us plan future development work. Please contact us if you have any questions about this.\n\nThis is the information that would be sent:" = "ข้อมูลระบบแบบนิรนามช่วยในการวางแผนพัฒนาแอปพลิเคชันของเราในอนาคต กรุณาติดต่อเราถ้าคุณมีข้อสงสัยในเรื่องนี้\n\nนี่คือข้อมูลที่จะถูกส่งไป:"; ================================================ FILE: Sparkle/tr.lproj/Sparkle.strings ================================================ /* No comment provided by engineer. */ "%@ %@ is currently the newest version available." = "%1$@ %2$@, kullanılabilir en yeni sürümdür."; /* No comment provided by engineer. */ "%@ %@ is currently the newest version available.\n(You are currently running version %@.)" = "%1$@ %2$@, kullanılabilir en yeni sürümdür.\n(Şu anda %3$@ sürümünü kullanıyorsunuz.)"; /* Description text for SUUpdateAlert when the update is downloadable. */ "%@ %@ is now available—you have %@. Would you like to download it now?" = "%1$@ %2$@ çıktı (kullandığınız sürüm: %3$@). Şimdi yeni sürümü indirmek ister misiniz?"; /* Description text for SUUpdateAlert when the update informational with no download. */ "%@ %@ is now available—you have %@. Would you like to learn more about this update on the web?" = "%1$@ %2$@ çıktı (kullandığınız sürüm: %3$@). Bu güncelleme ile ilgili olarak web sayfasından daha fazla bilgi almak ister misiniz?"; /* The download progress in a unit of bytes, e.g. 100 KB */ "%@ downloaded" = "%@ indirildi"; /* The download progress in units of bytes, e.g. 100 KB of 1,0 MB */ "%@ of %@" = "%1$@ / %2$@"; /* Description text for SUUpdateAlert when the update has already been downloaded and ready to install. */ "%1$@ %2$@ has been downloaded and is ready to use! Would you like to install it and relaunch %1$@ now?" = "%1$@ %2$@ indirildi ve kullanıma hazır! Şimdi yüklemek ister misiniz? Uygulama yeniden başlatılacaktır."; /* No comment provided by engineer. */ "%1$@ can’t be updated because it was opened from a read-only or a temporary location." = "%1$@ uygulaması geçici veya salt okunur bir konumdan açıldığı için güncellenemiyor."; /* No comment provided by engineer. */ "A new version of %@ is available!" = "%@ uygulaması için yeni bir sürüm kullanılabilir!"; /* No comment provided by engineer. */ "A new version of %@ is ready to install!" = "%@ uygulamasının yeni bir sürümü yüklenmeye hazır!"; /* No comment provided by engineer. */ "An error occurred in retrieving update information. Please try again later." = "Güncelleme bilgilerini alırken bir hata oluştu. Lütfen daha sonra yeniden deneyin."; /* No comment provided by engineer. */ "An error occurred while downloading the update. Please try again later." = "Güncelleme indirilirken bir hata oluştu. Lütfen daha sonra yeniden deneyin."; /* No comment provided by engineer. */ "An error occurred while extracting the archive. Please try again later." = "İndirilen arşivi açarken bir hata oluştu. Lütfen daha sonra yeniden deneyin."; /* No comment provided by engineer. */ "An error occurred while parsing the update feed." = "Güncelleme kaynağı ayrıştırılırken bir hata oluştu."; /* No comment provided by engineer. */ "Cancel" = "Vazgeç"; /* No comment provided by engineer. */ "Cancel Update" = "Güncellemeden Vazgeç"; /* No comment provided by engineer. */ "Checking for updates…" = "Güncellemeler denetleniyor…"; /* Take care not to overflow the status window. */ "Downloading update…" = "Güncelleme indiriliyor…"; /* Take care not to overflow the status window. */ "Extracting update…" = "Güncelleme çıkarılıyor…"; /* No comment provided by engineer. */ "Install and Relaunch" = "Yükle ve Yeniden Başlat"; /* Take care not to overflow the status window. */ "Installing update…" = "Güncelleme kuruluyor…"; /* No comment provided by engineer. */ "OK" = "Tamam"; /* No comment provided by engineer. */ "Ready to Install" = "Yüklemeye Hazır"; /* No comment provided by engineer. */ "Should %1$@ automatically check for updates? You can always check for updates manually from the %1$@ menu." = "%1$@ uygulaması güncellemeleri otomatik olarak denetlensin mi? Güncelleme işlemini el ile uygulama menüsünden de başlatabilirsiniz."; /* No comment provided by engineer. */ "Update Error!" = "Güncelleme Hatası!"; /* No comment provided by engineer. */ "Updating %@" = "%@ güncelleniyor"; /* No comment provided by engineer. */ "Use Finder to copy %1$@ to the Applications folder, relaunch it from there, and try again." = "%1$@ uygulamasını Uygulamalar klasörüne kopyalamak için Finder'ı kullanın, onu oradan başlatın ve yeniden deneyin."; /* Status message shown when the user checks for updates but is already current or the feed doesn't contain any updates. */ "You’re up to date!" = "Uygulama güncel!"; /* Software Update title/label */ "Software Update" = "Yazılım Güncelleme"; /* Remind Me Later choice for update alert */ "Remind Me Later" = "Daha Sonra Anımsat"; /* Skip This Version choice for update alert */ "Skip This Version" = "Bu Sürümü Atla"; /* Install Update choice for update alert */ "Install Update" = "Yükle"; /* Automatically download and install updates in the future option for update alert */ "Automatically download and install updates in the future" = "Gelecekte güncellemeleri otomatik olarak indir ve yükle"; /* Check for updates automatically response for update permission dialog */ "Check Automatically" = "Otomatik Olarak Denetle"; /* Don't check for updates automatically response for update permission dialog */ "Don’t Check" = "Denetleme"; /* Title question for update permission dialog */ "Check for updates automatically?" = "Güncellemeler otomatik olarak denetlensin mi?"; /* Include anonymous system profile choice for update permission dialog */ "Include anonymous system profile" = "Anonim sistem profilini içer"; /* Automatically download and install updates choice for update permission dialog */ "Automatically download and install updates" = "Güncellemeleri otomatik olarak indir ve yükle"; /* Anonymous system profile information disclosure for update permission dialog */ "Anonymous system profile information is used to help us plan future development work. Please contact us if you have any questions about this.\n\nThis is the information that would be sent:" = "Gönderdiğiniz anonim sistem bilgileri bu yazılımın geliştirilmesi için kullanılacaktır. Konu ile ilgili ayrıntılı bilgi için lütfen bizimle iletişime geçin. Gönderilecek bilgiler:"; ================================================ FILE: Sparkle/uk.lproj/Sparkle.strings ================================================ /* No comment provided by engineer. */ "%@ %@ is currently the newest version available." = "У данний момент %1$@ %2$@ є останньою версією."; /* No comment provided by engineer. */ "%@ %@ is currently the newest version available.\n(You are currently running version %@.)" = "%У данний момент %1$@ %2$@ є останньою версією.\n(You are currently running version %3$@.)"; /* Description text for SUUpdateAlert when the update is downloadable. */ "%@ %@ is now available—you have %@. Would you like to download it now?" = "%1$@ %2$@ доступна – ви маєте %3$@. Бажаєте завантажити її зараз?"; /* The download progress in a unit of bytes, e.g. 100 KB */ "%@ downloaded" = "%@ завантажено"; /* The download progress in units of bytes, e.g. 100 KB of 1,0 MB */ "%@ of %@" = "%1$@ із %2$@"; /* Description text for SUUpdateAlert when the update has already been downloaded and ready to install. */ "%1$@ %2$@ has been downloaded and is ready to use! Would you like to install it and relaunch %1$@ now?" = "%1$@ %2$@ завантажений і готовий до використання! Бажаєте встановити і перезавантажити %1$@?"; /* No comment provided by engineer. */ "%1$@ can’t be updated because it was opened from a read-only or a temporary location." = "Під час роботи з %1$@ з тому, що призначений лише для читання, наприклад, образу диска чи оптичного диску, його неможливо оновити."; /* No comment provided by engineer. */ "A new version of %@ is available!" = "Доступна нова версія %@!"; /* No comment provided by engineer. */ "A new version of %@ is ready to install!" = "Нова версія %@ готова до встановлення!"; /* No comment provided by engineer. */ "An error occurred in retrieving update information. Please try again later." = "Виникла помилка при отриманні інформації про оновлення. Спробуйте ще раз пізніше."; /* No comment provided by engineer. */ "An error occurred while downloading the update. Please try again later." = "Виникла помилка при завантаження оновлення. Спробуйте ще раз пізніше."; /* No comment provided by engineer. */ "An error occurred while extracting the archive. Please try again later." = "Виникла помилка при розпаковуванні архіву. Спробуйте ще раз пізніше."; /* No comment provided by engineer. */ "An error occurred while parsing the update feed." = "Виникла помилка розбору каналу оновлень."; /* No comment provided by engineer. */ "Cancel" = "Відмінити"; /* No comment provided by engineer. */ "Cancel Update" = "Відмінити оновлення"; /* No comment provided by engineer. */ "Checking for updates…" = "Перевіряю наявність оновлень…"; /* Take care not to overflow the status window. */ "Downloading update…" = "Завантажую оновлення…"; /* Take care not to overflow the status window. */ "Extracting update…" = "Розпаковую оновлення…"; /* No comment provided by engineer. */ "Install and Relaunch" = "Встановити та перезавантажити"; /* Take care not to overflow the status window. */ "Installing update…" = "Встановлюю оновлення…"; /* Alternate title for 'Install Update' button when there's no download in RSS feed. */ "Learn More…" = "Дізнатись більше…"; /* No comment provided by engineer. */ "OK" = "OK"; /* No comment provided by engineer. */ "Ready to Install" = "Готовий до встановлення"; /* No comment provided by engineer. */ "Should %1$@ automatically check for updates? You can always check for updates manually from the %1$@ menu." = "Чи повинен %1$@ автоматично виконувати перевірку на оновлення? Ви завжди можете самостійно перевірити оновлення у меню %1$@."; /* No comment provided by engineer. */ "Update Error!" = "Помилка оновлення!"; /* No comment provided by engineer. */ "Updating %@" = "Оновлюю %@"; /* No comment provided by engineer. */ "Use Finder to copy %1$@ to the Applications folder, relaunch it from there, and try again." = "Перемістіть %1$@ у папку з програмами, перезавантажте його звідти і спробуйте ще раз."; /* Status message shown when the user checks for updates but is already current or the feed doesn't contain any updates. */ "You’re up to date!" = "У вас остання версія!"; /* Software Update title/label */ "Software Update" = "Оновлення програмного забезпечення"; /* Remind Me Later choice for update alert */ "Remind Me Later" = "Нагадати пізніше"; /* Skip This Version choice for update alert */ "Skip This Version" = "Пропустити цю версію"; /* Install Update choice for update alert */ "Install Update" = "Встановити оновлення"; /* Automatically download and install updates in the future option for update alert */ "Automatically download and install updates in the future" = "Автоматично завантажувати та встановлювати оновлення у майбутньому"; /* Check for updates automatically response for update permission dialog */ "Check Automatically" = "Перевіряти автоматично"; /* Don't check for updates automatically response for update permission dialog */ "Don’t Check" = "Не перервіряти"; /* Title question for update permission dialog */ "Check for updates automatically?" = "Виконувати автоматичну перевірку оновлень?"; /* Include anonymous system profile choice for update permission dialog */ "Include anonymous system profile" = "Автоматично надсилати профіль системи"; /* Automatically download and install updates choice for update permission dialog */ "Automatically download and install updates" = "Автоматично завантажувати та встановлювати оновлення"; /* Anonymous system profile information disclosure for update permission dialog */ "Anonymous system profile information is used to help us plan future development work. Please contact us if you have any questions about this.\n\nThis is the information that would be sent:" = "Використання анонімного профілю системи допомагає нам у планування майбутньої розробки. Якщо у вас виникли питання щодо цього, звертайтесь до нас.\n\nІнформація, що буде надіслано:"; ================================================ FILE: Sparkle/vi.lproj/Sparkle.strings ================================================ /* Description text for SUUpdateAlert when the critical update has already been downloaded and ready to install. */ "%1$@ %2$@ has been downloaded and is ready to use! This is an important update; would you like to install it and relaunch %1$@ now?" = "%1$@ %2$@ đã được tải xuống và sẵn sàng sử dụng! Đây là bản cập nhật quan trọng; bạn có muốn cài đặt và khởi động lại %1$@ ngay bây giờ không?"; /* Description text for SUUpdateAlert when the update has already been downloaded and ready to install. */ "%1$@ %2$@ has been downloaded and is ready to use! Would you like to install it and relaunch %1$@ now?" = "%1$@ %2$@ đã được tải xuống và sẵn sàng sử dụng! Bạn có muốn cài đặt và khởi động lại %1$@ ngay bây giờ không?"; /* No comment provided by engineer. */ "%1$@ %2$@ is available but your macOS version is too new for this update. This update only supports up to macOS %3$@." = "%1$@ %2$@ khả dụng nhưng phiên bản macOS của bạn quá mới đối với bản cập nhật này. Bản cập nhật này chỉ hỗ trợ tối đa đến macOS %3$@."; /* No comment provided by engineer. */ "%1$@ %2$@ is available but your macOS version is too old to install it. At least macOS %3$@ is required." = "%1$@ %2$@ khả dụng nhưng phiên bản macOS của bạn quá cũ để cài đặt. Yêu cầu tối thiểu macOS %3$@."; /* Mac is too old and update requires Apple silicon Mac */ "%1$@ %2$@ is available but this update requires a new Apple silicon Mac." = "%1$@ %2$@ khả dụng nhưng bản cập nhật này yêu cầu máy Mac chạy chip Apple silicon mới."; /* No comment provided by engineer. */ "%1$@ can’t be updated if it’s running from the location it was downloaded to." = "Không thể cập nhật %1$@ nếu ứng dụng đang chạy từ vị trí mà nó đã được tải xuống."; /* No comment provided by engineer. */ "%1$@ can’t be updated because it was opened from a read-only or a temporary location." = "Không thể cập nhật %1$@ vì ứng dụng đã được mở từ một vị trí chỉ đọc hoặc vị trí tạm thời."; /* No comment provided by engineer. */ "%@ %@ is currently the newest version available." = "%1$@ %2$@ hiện là phiên bản mới nhất."; /* No comment provided by engineer. */ "%@ %@ is currently the newest version available.\n(You are currently running version %@.)" = "%1$@ %2$@ hiện là phiên bản mới nhất.\n(Bạn đang chạy phiên bản %3$@.)"; /* Description text for SUUpdateAlert when the critical update is downloadable. */ "%@ %@ is now available—you have %@. This is an important update; would you like to download it now?" = "%1$@ %2$@ hiện đã có—bạn đang dùng %3$@. Đây là bản cập nhật quan trọng; bạn có muốn tải xuống ngay bây giờ không?"; /* Description text for SUUpdateAlert when the update is downloadable. */ "%@ %@ is now available—you have %@. Would you like to download it now?" = "%1$@ %2$@ hiện đã có—bạn đang dùng %3$@. Bạn có muốn tải xuống ngay bây giờ không?"; /* Description text for SUUpdateAlert when the update informational with no download. */ "%@ %@ is now available—you have %@. Would you like to learn more about this update on the web?" = "%1$@ %2$@ hiện đã có—bạn đang dùng %3$@. Bạn có muốn tìm hiểu thêm về bản cập nhật này trên web không?"; /* The download progress in a unit of bytes, e.g. 100 KB */ "%@ downloaded" = "%@ đã tải xuống"; /* No comment provided by engineer. */ "%@ is now updated to version %@!" = "%1$@ đã được cập nhật lên phiên bản %2$@!"; /* No comment provided by engineer. */ "%@ is now updated!" = "%@ đã được cập nhật!"; /* The download progress in units of bytes, e.g. 100 KB of 1,0 MB */ "%@ of %@" = "%1$@ trên %2$@"; /* No comment provided by engineer. */ "A new version of %@ is available!" = "Đã có phiên bản mới của %@!"; /* No comment provided by engineer. */ "A new version of %@ is ready to install!" = "Phiên bản mới của %@ đã sẵn sàng để cài đặt!"; /* No comment provided by engineer. */ "An error occurred in retrieving update information. Please try again later." = "Đã xảy ra lỗi khi lấy thông tin cập nhật. Vui lòng thử lại sau."; /* No comment provided by engineer. */ "An error occurred while connecting to the installer. Please try again later." = "Đã xảy ra lỗi khi kết nối tới trình cài đặt. Vui lòng thử lại sau."; /* No comment provided by engineer. */ "An error occurred while downloading the update. Please try again later." = "Đã xảy ra lỗi khi tải xuống bản cập nhật. Vui lòng thử lại sau."; /* No comment provided by engineer. */ "An error occurred while extracting the archive. Please try again later." = "Đã xảy ra lỗi khi giải nén gói lưu trữ. Vui lòng thử lại sau."; /* No comment provided by engineer. */ "An error occurred while launching the installer. Please try again later." = "Đã xảy ra lỗi khi khởi chạy trình cài đặt. Vui lòng thử lại sau."; /* No comment provided by engineer. */ "An error occurred while parsing the update feed." = "Đã xảy ra lỗi khi phân tích nguồn cập nhật."; /* No comment provided by engineer. */ "An error occurred while running the updater. Please try again later." = "Đã xảy ra lỗi khi chạy trình cập nhật. Vui lòng thử lại sau."; /* No comment provided by engineer. */ "An error occurred while starting the installer. Please try again later." = "Đã xảy ra lỗi khi khởi động trình cài đặt. Vui lòng thử lại sau."; /* No comment provided by engineer. */ "An important update to %@ is ready to install" = "Một bản cập nhật quan trọng cho %@ đã sẵn sàng để cài đặt"; /* No comment provided by engineer. */ "Application Name" = "Tên ứng dụng"; /* No comment provided by engineer. */ "Application Version" = "Phiên bản ứng dụng"; /* No comment provided by engineer. */ "Cancel" = "Hủy"; /* No comment provided by engineer. */ "Cancel Update" = "Hủy cập nhật"; /* No comment provided by engineer. */ "Checking for updates…" = "Đang kiểm tra cập nhật…"; /* No comment provided by engineer. */ "CPU is 64-Bit?" = "CPU là 64-bit?"; /* No comment provided by engineer. */ "CPU Speed (MHz)" = "Tốc độ CPU (MHz)"; /* No comment provided by engineer. */ "CPU Subtype" = "Phân loại CPU"; /* No comment provided by engineer. */ "CPU Type" = "Loại CPU"; /* Take care not to overflow the status window. */ "Downloading update…" = "Đang tải xuống bản cập nhật…"; /* Take care not to overflow the status window. */ "Extracting update…" = "Đang giải nén bản cập nhật…"; /* No comment provided by engineer. */ "Failed to resume installing update." = "Không thể tiếp tục cài đặt bản cập nhật."; /* No comment provided by engineer. */ "Install and Relaunch" = "Cài đặt và khởi động lại"; /* Alternate title for 'Remind Me Later' button when downloaded updates can be resumed */ "Install on Quit" = "Cài đặt khi thoát"; /* Take care not to overflow the status window. */ "Installing update…" = "Đang cài đặt bản cập nhật…"; /* Alternate title for 'Install Update' button when there's no download in RSS feed. */ "Learn More…" = "Tìm hiểu thêm…"; /* No comment provided by engineer. */ "Mac Model" = "Mẫu Mac"; /* No comment provided by engineer. */ "Memory (MB)" = "Bộ nhớ (MB)"; /* No comment provided by engineer. */ "No" = "Không"; /* No comment provided by engineer. */ "Number of CPUs" = "Số lượng CPU"; /* No comment provided by engineer. */ "OK" = "OK"; /* No comment provided by engineer. */ "OS Version" = "Phiên bản hệ điều hành"; /* No comment provided by engineer. */ "Preferred Language" = "Ngôn ngữ ưu tiên"; /* No comment provided by engineer. */ "Quit %1$@, move it into your Applications folder, relaunch it from there and try again." = "Thoát %1$@, di chuyển vào thư mục Applications, khởi động lại từ đó và thử lại."; /* No comment provided by engineer. */ "Ready to Install" = "Sẵn sàng cài đặt"; /* No comment provided by engineer. */ "Should %1$@ automatically check for updates? You can always check for updates manually from the %1$@ menu." = "Có nên để %1$@ tự động kiểm tra cập nhật không? Bạn luôn có thể kiểm tra cập nhật thủ công từ menu %1$@."; /* No comment provided by engineer. */ "The installation failed due to not having permission to write the new update." = "Cài đặt thất bại do không có quyền ghi bản cập nhật mới."; /* No comment provided by engineer. */ "The updater failed to start. Please verify you have the latest version of %@ and contact the app developer if the issue still persists. Check the Console logs for more information." = "Trình cập nhật không thể khởi động. Vui lòng xác minh bạn đang dùng phiên bản mới nhất của %@ và liên hệ nhà phát triển nếu sự cố vẫn tiếp diễn. Kiểm tra nhật ký Console để biết thêm thông tin."; /* No comment provided by engineer. */ "The update is improperly signed and could not be validated. Please try again later or contact the app developer." = "Bản cập nhật có chữ ký không hợp lệ và không thể xác thực. Vui lòng thử lại sau hoặc liên hệ nhà phát triển."; /* No comment provided by engineer. */ "Unable to Check For Updates" = "Không thể kiểm tra cập nhật"; /* No comment provided by engineer. */ "Update Error!" = "Lỗi cập nhật!"; /* No comment provided by engineer. */ "Update Installed" = "Đã cài đặt cập nhật"; /* No comment provided by engineer. */ "Updating %@" = "Đang cập nhật %@"; /* No comment provided by engineer. */ "Use Finder to copy %1$@ to the Applications folder, relaunch it from there, and try again." = "Sử dụng Finder để sao chép %1$@ vào thư mục Applications, khởi động lại từ đó và thử lại."; /* No comment provided by engineer. */ "Version History" = "Lịch sử phiên bản"; /* No comment provided by engineer. */ "Yes" = "Có"; /* No comment provided by engineer. */ "You may need to allow modifications from %1$@ in System Settings under Privacy & Security and App Management to install future updates." = "Bạn có thể cần cho phép sửa đổi từ %1$@ trong Cài đặt hệ thống tại Quyền riêng tư & Bảo mật và Quản lý ứng dụng để cài đặt các bản cập nhật trong tương lai."; /* No comment provided by engineer. */ "Your macOS version is too new" = "Phiên bản macOS của bạn quá mới"; /* No comment provided by engineer. */ "Your macOS version is too old" = "Phiên bản macOS của bạn quá cũ"; /* Mac is too old and update requires newer hardware */ "Your Mac is too old" = "Máy Mac của bạn quá cũ"; /* Status message shown when the user checks for updates but is already current or the feed doesn't contain any updates. */ "You’re up to date!" = "Bạn đang dùng phiên bản mới nhất!"; /* Software Update title/label */ "Software Update" = "Cập nhật phần mềm"; /* Remind Me Later choice for update alert */ "Remind Me Later" = "Nhắc tôi sau"; /* Skip This Version choice for update alert */ "Skip This Version" = "Bỏ qua phiên bản này"; /* Install Update choice for update alert */ "Install Update" = "Cài đặt cập nhật"; /* Automatically download and install updates in the future option for update alert */ "Automatically download and install updates in the future" = "Tự động tải xuống và cài đặt cập nhật trong tương lai"; /* Check for updates automatically response for update permission dialog */ "Check Automatically" = "Tự động kiểm tra"; /* Don't check for updates automatically response for update permission dialog */ "Don’t Check" = "Không kiểm tra"; /* Title question for update permission dialog */ "Check for updates automatically?" = "Tự động kiểm tra cập nhật?"; /* Include anonymous system profile choice for update permission dialog */ "Include anonymous system profile" = "Bao gồm hồ sơ hệ thống ẩn danh"; /* Automatically download and install updates choice for update permission dialog */ "Automatically download and install updates" = "Tự động tải xuống và cài đặt cập nhật"; /* Anonymous system profile information disclosure for update permission dialog */ "Anonymous system profile information is used to help us plan future development work. Please contact us if you have any questions about this.\n\nThis is the information that would be sent:" = "Thông tin hồ sơ hệ thống ẩn danh được sử dụng để giúp chúng tôi lập kế hoạch phát triển trong tương lai. Vui lòng liên hệ nếu bạn có bất kỳ câu hỏi nào.\n\nĐây là thông tin sẽ được gửi:"; ================================================ FILE: Sparkle/zh_CN.lproj/Sparkle.strings ================================================ /* Description text for SUUpdateAlert when the critical update has already been downloaded and ready to install. */ "%1$@ %2$@ has been downloaded and is ready to use! This is an important update; would you like to install it and relaunch %1$@ now?" = "%1$@ %2$@已下载完毕并可以使用。这是重要更新,要立刻安装并重启%1$@吗?"; /* Description text for SUUpdateAlert when the update has already been downloaded and ready to install. */ "%1$@ %2$@ has been downloaded and is ready to use! Would you like to install it and relaunch %1$@ now?" = "%1$@ %2$@已下载完毕并可以使用。要立刻安装并重启%1$@吗?"; /* No comment provided by engineer. */ "%1$@ %2$@ is available but your macOS version is too new for this update. This update only supports up to macOS %3$@." = "%1$@ %2$@可供下载,但您的macOS版本因太新而无法安装本次更新。本次更新仅支持至macOS %3$@。"; /* No comment provided by engineer. */ "%1$@ %2$@ is available but your macOS version is too old to install it. At least macOS %3$@ is required." = "%1$@ %2$@可供下载,但您的macOS版本因太旧而无法安装。最低版本要求为macOS %3$@。"; /* No comment provided by engineer. */ "%1$@ can’t be updated if it’s running from the location it was downloaded to." = "如果在下载位置运行此程序,则无法对%1$@进行更新。"; /* No comment provided by engineer. */ "%1$@ can’t be updated because it was opened from a read-only or a temporary location." = "无法更新%1$@,因为它打开自一个只读或临时位置。"; /* No comment provided by engineer. */ "%@ %@ is currently the newest version available." = "%1$@ %2$@是当前的最新版本。"; /* No comment provided by engineer. */ "%@ %@ is currently the newest version available.\n(You are currently running version %@.)" = "%1$@ %2$@是当前的最新版本\n(您正在运行 %3$@)。"; /* Description text for SUUpdateAlert when the critical update is downloadable. */ "%@ %@ is now available—you have %@. This is an important update; would you like to download it now?" = "%1$@ %2$@可供下载,您现在的版本是%3$@。这是重要更新,要现在下载吗?"; /* Description text for SUUpdateAlert when the update is downloadable. */ "%@ %@ is now available—you have %@. Would you like to download it now?" = "%1$@ %2$@可供下载,您现在的版本是%3$@。要现在下载吗?"; /* Description text for SUUpdateAlert when the update informational with no download. */ "%@ %@ is now available—you have %@. Would you like to learn more about this update on the web?" = "%1$@ %2$@可供下载,您现在的版本是%3$@。您要在网站上查看关于本次更新的更多信息吗?"; /* The download progress in a unit of bytes, e.g. 100 KB */ "%@ downloaded" = "%@已下载"; /* No comment provided by engineer. */ "%@ is now updated to version %@!" = "%1$@已更新至版本%2$@!"; /* No comment provided by engineer. */ "%@ is now updated!" = "%@已完成更新"; /* The download progress in units of bytes, e.g. 100 KB of 1,0 MB */ "%@ of %@" = "%1$@ / %2$@"; /* No comment provided by engineer. */ "A new version of %@ is available!" = "新版本的%@已经发布"; /* No comment provided by engineer. */ "A new version of %@ is ready to install!" = "新版本的%@可以安装了"; /* No comment provided by engineer. */ "An error occurred in retrieving update information. Please try again later." = "获取升级信息时出现错误,请稍后再试。"; /* No comment provided by engineer. */ "An error occurred while connecting to the installer. Please try again later." = "连接到安装程序时出现错误,请稍后再试。"; /* No comment provided by engineer. */ "An error occurred while downloading the update. Please try again later." = "下载更新时出现错误,请稍后再试。"; /* No comment provided by engineer. */ "An error occurred while extracting the archive. Please try again later." = "解压时出现错误,请稍后再试。"; /* No comment provided by engineer. */ "An error occurred while launching the installer. Please try again later." = "打开安装程序时出现错误,请稍后再试。"; /* No comment provided by engineer. */ "An error occurred while parsing the update feed." = "解析更新信息时出现错误,请稍后再试。"; /* No comment provided by engineer. */ "An error occurred while running the updater. Please try again later." = "运行更新程序时出现错误,请稍后再试。"; /* No comment provided by engineer. */ "An error occurred while starting the installer. Please try again later." = "打开安装程序时出现错误,请稍后再试。"; /* No comment provided by engineer. */ "An important update to %@ is ready to install" = "已准备好安装%@的重要更新"; /* No comment provided by engineer. */ "Application Name" = "应用名称"; /* No comment provided by engineer. */ "Application Version" = "应用版本"; /* No comment provided by engineer. */ "Cancel" = "取消"; /* No comment provided by engineer. */ "Cancel Update" = "取消更新"; /* No comment provided by engineer. */ "Checking for updates…" = "正在检查更新…"; /* No comment provided by engineer. */ "CPU is 64-Bit?" = "CPU是64位的吗?"; /* No comment provided by engineer. */ "CPU Speed (MHz)" = "CPU主频(MHz)"; /* No comment provided by engineer. */ "CPU Subtype" = "CPU子类型"; /* No comment provided by engineer. */ "CPU Type" = "CPU类型"; /* Take care not to overflow the status window. */ "Downloading update…" = "正在下载更新…"; /* Take care not to overflow the status window. */ "Extracting update…" = "正在解压更新…"; /* No comment provided by engineer. */ "Failed to resume installing update." = "无法继续安装更新。"; /* No comment provided by engineer. */ "Install and Relaunch" = "安装并重启应用"; /* Alternate title for 'Remind Me Later' button when downloaded updates can be resumed */ "Install on Quit" = "退出应用时安装"; /* Take care not to overflow the status window. */ "Installing update…" = "正在安装更新…"; /* Alternate title for 'Install Update' button when there's no download in RSS feed. */ "Learn More…" = "查看更多…"; /* No comment provided by engineer. */ "Mac Model" = "Mac机型"; /* No comment provided by engineer. */ "Memory (MB)" = "内存(MB)"; /* No comment provided by engineer. */ "No" = "不"; /* No comment provided by engineer. */ "Number of CPUs" = "CPU数量"; /* No comment provided by engineer. */ "OK" = "好"; /* No comment provided by engineer. */ "OS Version" = "系统版本"; /* No comment provided by engineer. */ "Preferred Language" = "偏好语言"; /* No comment provided by engineer. */ "Quit %1$@, move it into your Applications folder, relaunch it from there and try again." = "退出%1$@,将其移至“应用程序”文件夹,并在那里重新启动,然后再试。"; /* No comment provided by engineer. */ "Ready to Install" = "可以开始安装了"; /* No comment provided by engineer. */ "Should %1$@ automatically check for updates? You can always check for updates manually from the %1$@ menu." = "您想让%1$@在启动时自动检查更新么?您也可以在程序菜单%1$@中手动检查。"; /* No comment provided by engineer. */ "The installation failed due to not having permission to write the new update." = "安装失败,没有权限写入更新。"; /* No comment provided by engineer. */ "The updater failed to start. Please verify you have the latest version of %@ and contact the app developer if the issue still persists. Check the Console logs for more information." = "无法启动更新程序。请验证是否有最新版本的%@,并联系App开发者如果此问题持续。请查看控制台以获得更多信息。"; /* No comment provided by engineer. */ "The update is improperly signed and could not be validated. Please try again later or contact the app developer." = "此更新未正确签名,无法验证其真实性。请稍后再试或联系App开发者。"; /* No comment provided by engineer. */ "Unable to Check For Updates" = "无法检查更新"; /* No comment provided by engineer. */ "Update Error!" = "更新错误!"; /* No comment provided by engineer. */ "Update Installed" = "更新已安装"; /* No comment provided by engineer. */ "Updating %@" = "正在更新%@"; /* No comment provided by engineer. */ "Use Finder to copy %1$@ to the Applications folder, relaunch it from there, and try again." = "移动%1$@到应用程序文件夹并再试一次。"; /* No comment provided by engineer. */ "Version History" = "版本历史记录"; /* No comment provided by engineer. */ "Yes" = "是"; /* No comment provided by engineer. */ "You may need to allow modifications from %1$@ in System Settings under Privacy & Security and App Management to install future updates." = "为了正常安装后续更新,您需要在“系统设置 → 隐私与安全 → App管理”内允许%1$@更新应用。"; /* No comment provided by engineer. */ "Your macOS version is too new" = "您的macOS版本太新了"; /* No comment provided by engineer. */ "Your macOS version is too old" = "您的macOS版本太旧了"; /* Status message shown when the user checks for updates but is already current or the feed doesn't contain any updates. */ "You’re up to date!" = "您使用的就是最新版!"; /* Software Update title/label */ "Software Update" = "软件更新"; /* Remind Me Later choice for update alert */ "Remind Me Later" = "稍后提醒我"; /* Skip This Version choice for update alert */ "Skip This Version" = "跳过这个版本"; /* Install Update choice for update alert */ "Install Update" = "安装更新"; /* Automatically download and install updates in the future option for update alert */ "Automatically download and install updates in the future" = "以后自动下载并安装更新"; /* Check for updates automatically response for update permission dialog */ "Check Automatically" = "自动检查"; /* Don't check for updates automatically response for update permission dialog */ "Don’t Check" = "不检查"; /* Title question for update permission dialog */ "Check for updates automatically?" = "自动检查更新?"; /* Include anonymous system profile choice for update permission dialog */ "Include anonymous system profile" = "包括匿名的系统概况"; /* Automatically download and install updates choice for update permission dialog */ "Automatically download and install updates" = "自动下载并安装更新"; /* Anonymous system profile information disclosure for update permission dialog */ "Anonymous system profile information is used to help us plan future development work. Please contact us if you have any questions about this.\n\nThis is the information that would be sent:" = "匿名的系统概况信息有助于我们安排将来的开发工作。如果对此存在疑问请联系我们。\n\n这是将要被发送的信息:"; ================================================ FILE: Sparkle/zh_HK.lproj/Sparkle.strings ================================================ /* Description text for SUUpdateAlert when the critical update has already been downloaded and ready to install. */ "%1$@ %2$@ has been downloaded and is ready to use! This is an important update; would you like to install it and relaunch %1$@ now?" = "%1$@ %2$@ 已下載並可供使用!此爲重要更新,是否立即安裝並重新啓動 %1$@?"; /* Description text for SUUpdateAlert when the update has already been downloaded and ready to install. */ "%1$@ %2$@ has been downloaded and is ready to use! Would you like to install it and relaunch %1$@ now?" = "%1$@ %2$@ 已下載並可供使用!是否立即安裝並重新啓動 %1$@?"; /* No comment provided by engineer. */ "%1$@ %2$@ is available but your macOS version is too new for this update. This update only supports up to macOS %3$@." = "%1$@ %2$@ 現已可供下載,但你嘅 macOS 版本過新以致無法安裝。此更新僅支援至 macOS %3$@。"; /* No comment provided by engineer. */ "%1$@ %2$@ is available but your macOS version is too old to install it. At least macOS %3$@ is required." = "%1$@ %2$@ 現已可供下載,但你嘅 macOS 版本過舊以致無法安裝。最低版本需求爲 macOS %3$@。"; /* No comment provided by engineer. */ "%1$@ can’t be updated if it’s running from the location it was downloaded to." = "如果從下載位置運行 %1$@,則無法進行更新。"; /* No comment provided by engineer. */ "%1$@ can’t be updated because it was opened from a read-only or a temporary location." = "無法更新 %1$@,因爲佢運行於一個只讀或臨時位置。"; /* No comment provided by engineer. */ "%@ %@ is currently the newest version available." = "%1$@ %2$@ 已係目前最新版本。"; /* No comment provided by engineer. */ "%@ %@ is currently the newest version available.\n(You are currently running version %@.)" = "%1$@ %2$@ 已係目前最新版本。\n(你正在運行版本係 %3$@。)"; /* Description text for SUUpdateAlert when the critical update is downloadable. */ "%@ %@ is now available—you have %@. This is an important update; would you like to download it now?" = "%1$@ %2$@ 現已可供下載,你目前版本係 %3$@。此爲重要更新,是否立即下載?"; /* Description text for SUUpdateAlert when the update is downloadable. */ "%@ %@ is now available—you have %@. Would you like to download it now?" = "%1$@ %2$@ 現已可供下載,你目前版本係 %3$@。是否立即下載?"; /* Description text for SUUpdateAlert when the update informational with no download. */ "%@ %@ is now available—you have %@. Would you like to learn more about this update on the web?" = "%1$@ %2$@ 現已可供下載,你目前版本係 %3$@。要毋要去網站度瞭解更多關於此更新嘅資訊?"; /* The download progress in a unit of bytes, e.g. 100 KB */ "%@ downloaded" = "%@ 已下載"; /* No comment provided by engineer. */ "%@ is now updated to version %@!" = "%1$@ 已更新至版本 %2$@!"; /* No comment provided by engineer. */ "%@ is now updated!" = "%@ 已完成更新!"; /* The download progress in units of bytes, e.g. 100 KB of 1,0 MB */ "%@ of %@" = "%1$@ / %2$@"; /* No comment provided by engineer. */ "A new version of %@ is available!" = "新版本 %@ 已可供下載!"; /* No comment provided by engineer. */ "A new version of %@ is ready to install!" = "新版本 %@ 已可供安裝!"; /* No comment provided by engineer. */ "An error occurred in retrieving update information. Please try again later." = "擷取更新資訊時發生錯誤。請稍後再試。"; /* No comment provided by engineer. */ "An error occurred while connecting to the installer. Please try again later." = "連線到安裝程式時發生錯誤。請稍後再試。"; /* No comment provided by engineer. */ "An error occurred while downloading the update. Please try again later." = "下載更新時發生錯誤。請稍後再試。"; /* No comment provided by engineer. */ "An error occurred while extracting the archive. Please try again later." = "解壓縮時發生錯誤。請稍後再試。"; /* No comment provided by engineer. */ "An error occurred while launching the installer. Please try again later." = "啓動安裝程式時發生錯誤。請稍後再試。"; /* No comment provided by engineer. */ "An error occurred while parsing the update feed." = "剖析更新摘要時發生錯誤。"; /* No comment provided by engineer. */ "An error occurred while running the updater. Please try again later." = "執行更新程式時發生錯誤。請稍後再試。"; /* No comment provided by engineer. */ "An error occurred while starting the installer. Please try again later." = "啓動安裝程式時發生錯誤。請稍後再試。"; /* No comment provided by engineer. */ "An important update to %@ is ready to install" = " %@ 重要更新已可供安裝"; /* No comment provided by engineer. */ "Cancel" = "取消"; /* No comment provided by engineer. */ "Cancel Update" = "取消更新"; /* No comment provided by engineer. */ "Checking for updates…" = "檢查緊更新⋯"; /* Take care not to overflow the status window. */ "Downloading update…" = "下載緊更新⋯"; /* Take care not to overflow the status window. */ "Extracting update…" = "解壓緊更新⋯"; /* No comment provided by engineer. */ "Failed to resume installing update." = "嘗試繼續安裝更新失敗。"; /* No comment provided by engineer. */ "Install and Relaunch" = "安裝並重新啓動"; /* Alternate title for 'Remind Me Later' button when downloaded updates can be resumed */ "Install on Quit" = "結束時安裝"; /* Take care not to overflow the status window. */ "Installing update…" = "安裝緊更新⋯"; /* Alternate title for 'Install Update' button when there's no download in RSS feed. */ "Learn More…" = "瞭解更多⋯"; /* No comment provided by engineer. */ "OK" = "好"; /* No comment provided by engineer. */ "Quit %1$@, move it into your Applications folder, relaunch it from there and try again." = "請結束 %1$@ 並將其移至「應用程式」檔案夾,從該處重新啓動後再試。"; /* No comment provided by engineer. */ "Ready to Install" = "已可供安裝"; /* No comment provided by engineer. */ "Should %1$@ automatically check for updates? You can always check for updates manually from the %1$@ menu." = "%1$@ 是否應自動檢查更新?你可隨時從 %1$@ 選單手動檢查更新。"; /* No comment provided by engineer. */ "The update is improperly signed and could not be validated. Please try again later or contact the app developer." = "此更新並未正確簽署且無法驗證。請稍後再試或聯絡軟體開發者。"; /* No comment provided by engineer. */ "Unable to Check For Updates" = "無法檢查更新"; /* No comment provided by engineer. */ "Update Error!" = "更新發生錯誤!"; /* No comment provided by engineer. */ "Update Installed" = "已安裝更新"; /* No comment provided by engineer. */ "Updating %@" = "更新緊 %@"; /* No comment provided by engineer. */ "Use Finder to copy %1$@ to the Applications folder, relaunch it from there, and try again." = "請將 %1$@ 複製至「應用程式」檔案夾,從該處重新啓動後再試。"; /* No comment provided by engineer. */ "Version History" = "版本歷史"; /* No comment provided by engineer. */ "Your macOS version is too new" = "你 macOS 版本過新"; /* No comment provided by engineer. */ "Your macOS version is too old" = "你 macOS 版本過舊"; /* Status message shown when the user checks for updates but is already current or the feed doesn't contain any updates. */ "You’re up to date!" = "你用緊最新版本!"; /* Software Update title/label */ "Software Update" = "軟體更新"; /* Remind Me Later choice for update alert */ "Remind Me Later" = "稍後提醒我"; /* Skip This Version choice for update alert */ "Skip This Version" = "跳過此版本"; /* Install Update choice for update alert */ "Install Update" = "安裝更新"; /* Automatically download and install updates in the future option for update alert */ "Automatically download and install updates in the future" = "以後自動下載並安裝更新"; /* Check for updates automatically response for update permission dialog */ "Check Automatically" = "自動檢查"; /* Don't check for updates automatically response for update permission dialog */ "Don’t Check" = "毋檢查"; /* Title question for update permission dialog */ "Check for updates automatically?" = "自動檢查更新?"; /* Include anonymous system profile choice for update permission dialog */ "Include anonymous system profile" = "包含匿名系統概況"; /* Automatically download and install updates choice for update permission dialog */ "Automatically download and install updates" = "自動下載並安裝更新"; /* Anonymous system profile information disclosure for update permission dialog */ "Anonymous system profile information is used to help us plan future development work. Please contact us if you have any questions about this.\n\nThis is the information that would be sent:" = "匿名系統概況資訊可用來協助我等計畫未來開發工作。若對此有任何疑問,請聯繫我等。\n\n以下係會傳送嘅資訊:"; ================================================ FILE: Sparkle/zh_TW.lproj/Sparkle.strings ================================================ /* Description text for SUUpdateAlert when the critical update has already been downloaded and ready to install. */ "%1$@ %2$@ has been downloaded and is ready to use! This is an important update; would you like to install it and relaunch %1$@ now?" = "%1$@ %2$@已下載且可供使用!此為重要更新,您是否要立即安裝並重新啟動%1$@?"; /* Description text for SUUpdateAlert when the update has already been downloaded and ready to install. */ "%1$@ %2$@ has been downloaded and is ready to use! Would you like to install it and relaunch %1$@ now?" = "%1$@ %2$@已下載且可供使用!您是否要立即安裝並重新啟動%1$@?"; /* No comment provided by engineer. */ "%1$@ %2$@ is available but your macOS version is too new for this update. This update only supports up to macOS %3$@." = "%1$@ %2$@現在已可供下載,但您的macOS版本過新以致無法安裝。此更新僅支援至macOS %3$@。"; /* No comment provided by engineer. */ "%1$@ %2$@ is available but your macOS version is too old to install it. At least macOS %3$@ is required." = "%1$@ %2$@現在已可供下載,但您的macOS版本過舊以致無法安裝。最低版本需求為macOS %3$@。"; /* No comment provided by engineer. */ "%1$@ can’t be updated if it’s running from the location it was downloaded to." = "當從下載位置執行%1$@時,無法進行更新。"; /* No comment provided by engineer. */ "%1$@ can’t be updated because it was opened from a read-only or a temporary location." = "當%1$@正從唯讀卷宗(如磁碟映像檔或光碟機)執行時,無法進行更新。"; /* No comment provided by engineer. */ "%@ %@ is currently the newest version available." = "%1$@ %2$@已是目前最新的版本。"; /* No comment provided by engineer. */ "%@ %@ is currently the newest version available.\n(You are currently running version %@.)" = "%1$@ %2$@已是目前最新的版本。\n(您正在執行的版本是%3$@。)"; /* Description text for SUUpdateAlert when the critical update is downloadable. */ "%@ %@ is now available—you have %@. This is an important update; would you like to download it now?" = "%1$@ %2$@現在已可供下載,您的版本則是%3$@。此為重要更新,您想要立即下載嗎?"; /* Description text for SUUpdateAlert when the update is downloadable. */ "%@ %@ is now available—you have %@. Would you like to download it now?" = "%1$@ %2$@現在已可供下載,您的版本則是%3$@。您想要立即下載嗎?"; /* Description text for SUUpdateAlert when the update informational with no download. */ "%@ %@ is now available—you have %@. Would you like to learn more about this update on the web?" = "%1$@ %2$@現在已可供下載,您的版本則是%3$@。您想要瞭解更多關於此更新的資訊嗎?"; /* The download progress in a unit of bytes, e.g. 100 KB */ "%@ downloaded" = "%@已下載"; /* No comment provided by engineer. */ "%@ is now updated to version %@!" = "%1$@已更新至版本%2$@!"; /* No comment provided by engineer. */ "%@ is now updated!" = "%@已完成更新!"; /* The download progress in units of bytes, e.g. 100 KB of 1,0 MB */ "%@ of %@" = "%1$@ / %2$@"; /* No comment provided by engineer. */ "A new version of %@ is available!" = "已有新版本的%@可供下載!"; /* No comment provided by engineer. */ "A new version of %@ is ready to install!" = "已準備安裝新版本的%@!"; /* No comment provided by engineer. */ "An error occurred in retrieving update information. Please try again later." = "擷取更新資訊時發生錯誤。請稍後再試一次。"; /* No comment provided by engineer. */ "An error occurred while connecting to the installer. Please try again later." = "連線到安裝程式時發生錯誤。請稍後再試一次。"; /* No comment provided by engineer. */ "An error occurred while downloading the update. Please try again later." = "下載更新項目時發生錯誤。請稍後再試一次。"; /* No comment provided by engineer. */ "An error occurred while extracting the archive. Please try again later." = "解壓縮封存檔時發生錯誤。請稍後再試一次。"; /* No comment provided by engineer. */ "An error occurred while launching the installer. Please try again later." = "啟動安裝程式時發生錯誤。請稍後再試一次。"; /* No comment provided by engineer. */ "An error occurred while parsing the update feed." = "剖析更新摘要時發生錯誤。"; /* No comment provided by engineer. */ "An error occurred while running the updater. Please try again later." = "執行更新程式時發生錯誤。請稍後再試一次。"; /* No comment provided by engineer. */ "An error occurred while starting the installer. Please try again later." = "啟動安裝程式時發生錯誤。請稍後再試一次。"; /* No comment provided by engineer. */ "An important update to %@ is ready to install" = "已準備安裝%@的重要更新"; /* No comment provided by engineer. */ "Application Name" = "应用名称"; /* No comment provided by engineer. */ "Application Version" = "应用版本"; /* No comment provided by engineer. */ "Cancel" = "取消"; /* No comment provided by engineer. */ "Cancel Update" = "取消更新"; /* No comment provided by engineer. */ "Checking for updates…" = "正在檢查更新項目⋯"; /* No comment provided by engineer. */ "CPU is 64-Bit?" = "CPU是64位的吗?"; /* No comment provided by engineer. */ "CPU Speed (MHz)" = "CPU主频(MHz)"; /* No comment provided by engineer. */ "CPU Subtype" = "CPU子类型"; /* No comment provided by engineer. */ "CPU Type" = "CPU类型"; /* Take care not to overflow the status window. */ "Downloading update…" = "正在下載更新項目⋯"; /* Take care not to overflow the status window. */ "Extracting update…" = "正在解壓縮更新項目⋯"; /* No comment provided by engineer. */ "Failed to resume installing update." = "嘗試繼續安裝更新失敗。"; /* No comment provided by engineer. */ "Install and Relaunch" = "安裝並重新啟動"; /* Alternate title for 'Remind Me Later' button when downloaded updates can be resumed */ "Install on Quit" = "結束時安裝"; /* Take care not to overflow the status window. */ "Installing update…" = "正在安裝更新項目⋯"; /* Alternate title for 'Install Update' button when there's no download in RSS feed. */ "Learn More…" = "瞭解更多⋯"; /* No comment provided by engineer. */ "Mac Model" = "Mac機型"; /* No comment provided by engineer. */ "Memory (MB)" = "記憶體(MB)"; /* No comment provided by engineer. */ "No" = "否"; /* No comment provided by engineer. */ "Number of CPUs" = "CPU數量"; /* No comment provided by engineer. */ "OK" = "好"; /* No comment provided by engineer. */ "OS Version" = "系統版本"; /* No comment provided by engineer. */ "Preferred Language" = "偏好語言"; /* No comment provided by engineer. */ "Quit %1$@, move it into your Applications folder, relaunch it from there and try again." = "請結束%1$@並移至您的「應用程式」檔案夾,從該處重新啟動後再試一次。"; /* No comment provided by engineer. */ "Ready to Install" = "準備安裝"; /* No comment provided by engineer. */ "Should %1$@ automatically check for updates? You can always check for updates manually from the %1$@ menu." = "%1$@是否應自動檢查更新項目?您可隨時從%1$@選單手動檢查更新項目。"; /* No comment provided by engineer. */ "The installation failed due to not having permission to write the new update." = "安裝失敗,沒有權限寫入更新。"; /* No comment provided by engineer. */ "The updater failed to start. Please verify you have the latest version of %@ and contact the app developer if the issue still persists. Check the Console logs for more information." = "無法啟動更新程式。請確認您是否正在使用%@的最新版本,若問題仍持續,請聯絡軟體開發者。請查看主控台記錄以取得更多資訊。"; /* No comment provided by engineer. */ "The update is improperly signed and could not be validated. Please try again later or contact the app developer." = "此更新並未正確簽署且無法驗證。請稍後再試一次或聯絡軟體開發者。"; /* No comment provided by engineer. */ "Unable to Check For Updates" = "無法檢查更新項目"; /* No comment provided by engineer. */ "Update Error!" = "更新發生錯誤!"; /* No comment provided by engineer. */ "Update Installed" = "已安裝更新"; /* No comment provided by engineer. */ "Updating %@" = "正在更新%@"; /* No comment provided by engineer. */ "Use Finder to copy %1$@ to the Applications folder, relaunch it from there, and try again." = "請將%1$@移至您的「應用程式」檔案夾,從該處重新啟動後再試一次。"; /* No comment provided by engineer. */ "Version History" = "版本歷程記錄"; /* No comment provided by engineer. */ "Yes" = "是"; /* No comment provided by engineer. */ "You may need to allow modifications from %1$@ in System Settings under Privacy & Security and App Management to install future updates." = "若要安裝後續更新,您可能需要在「系統設定」的「隱私權與安全性」→「App管理」中允許%1$@修改應用程式。"; /* No comment provided by engineer. */ "Your macOS version is too new" = "您的macOS版本過新"; /* No comment provided by engineer. */ "Your macOS version is too old" = "您的macOS版本過舊"; /* Status message shown when the user checks for updates but is already current or the feed doesn't contain any updates. */ "You’re up to date!" = "您已有最新版本!"; /* Software Update title/label */ "Software Update" = "軟體更新"; /* Remind Me Later choice for update alert */ "Remind Me Later" = "暫緩提醒"; /* Skip This Version choice for update alert */ "Skip This Version" = "跳過此版本"; /* Install Update choice for update alert */ "Install Update" = "安裝更新項目"; /* Automatically download and install updates in the future option for update alert */ "Automatically download and install updates in the future" = "自動下載並安裝未來的更新項目"; /* Check for updates automatically response for update permission dialog */ "Check Automatically" = "自動檢查"; /* Don't check for updates automatically response for update permission dialog */ "Don’t Check" = "不要檢查"; /* Title question for update permission dialog */ "Check for updates automatically?" = "自動檢查更新項目?"; /* Include anonymous system profile choice for update permission dialog */ "Include anonymous system profile" = "包含匿名的系統描述資料"; /* Automatically download and install updates choice for update permission dialog */ "Automatically download and install updates" = "自動下載並安裝更新項目"; /* Anonymous system profile information disclosure for update permission dialog */ "Anonymous system profile information is used to help us plan future development work. Please contact us if you have any questions about this.\n\nThis is the information that would be sent:" = "匿名系統描述資訊可用來協助我們計畫未來的開發工作。若您有任何相關問題,請與我們聯繫。\n\n以下是會傳送的資訊:"; ================================================ FILE: Sparkle.podspec ================================================ Pod::Spec.new do |s| s.name = "Sparkle" s.version = "2.9.0" s.summary = "A software update framework for macOS" s.description = "Sparkle is an easy-to-use software update framework for macOS." s.homepage = "https://sparkle-project.org" s.documentation_url = "https://sparkle-project.org/documentation/" s.screenshot = "https://sparkle-project.org/images/screenshot-noshadow2.png" s.license = { :type => 'MIT', :file => 'LICENSE' } s.authors = { 'Zorg' => 'zorgiepoo@gmail.com', 'Kornel Lesiński' => 'pornel@pornel.net', 'Jake Petroules' => 'jake.petroules@petroules.com', 'C.W. Betts' => 'computers57@hotmail.com', 'Andy Matuschak' => 'andy@andymatuschak.org', } s.platform = :osx, '10.13' s.source = { :http => "https://github.com/sparkle-project/Sparkle/releases/download/#{s.version}/Sparkle-#{s.version}.tar.xz" } s.source_files = 'Sparkle.framework/Versions/B/Headers/*.h' s.preserve_paths = ['bin/*', 'Symbols'] s.public_header_files = 'Sparkle.framework/Versions/B/Headers/*.h' s.vendored_frameworks = 'Sparkle.framework' s.xcconfig = { 'FRAMEWORK_SEARCH_PATHS' => '"${PODS_ROOT}/Sparkle"', 'LD_RUNPATH_SEARCH_PATHS' => '@loader_path/../Frameworks' } s.requires_arc = true end ================================================ FILE: Sparkle.xcodeproj/project.pbxproj ================================================ // !$*UTF8*$! { archiveVersion = 1; classes = { }; objectVersion = 54; objects = { /* Begin PBXAggregateTarget section */ 1495005F195FB89400BC5B5B /* All */ = { isa = PBXAggregateTarget; buildConfigurationList = 14950060195FB89500BC5B5B /* Build configuration list for PBXAggregateTarget "All" */; buildPhases = ( ); dependencies = ( 14950064195FB8A600BC5B5B /* PBXTargetDependency */, 14950066195FB8A600BC5B5B /* PBXTargetDependency */, 14950068195FB8A600BC5B5B /* PBXTargetDependency */, 1495006A195FB8A600BC5B5B /* PBXTargetDependency */, 7205C4691E1306FB00E370AE /* PBXTargetDependency */, ); name = All; productName = All; }; 895C5DC024D78E210058A82D /* XCFrameworks */ = { isa = PBXAggregateTarget; buildConfigurationList = 895C5DC424D78E210058A82D /* Build configuration list for PBXAggregateTarget "XCFrameworks" */; buildPhases = ( 895C5DC524D78E460058A82D /* ShellScript */, ); dependencies = ( 72EF30B926747E39008CE987 /* PBXTargetDependency */, ); name = XCFrameworks; productName = Sparkle.xcframework; }; /* End PBXAggregateTarget section */ /* Begin PBXBuildFile section */ 142E0E0019A6954400E4312B /* Sparkle.framework in Copy Frameworks */ = {isa = PBXBuildFile; fileRef = 8DC2EF5B0486A6940098B216 /* Sparkle.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 142E0E0219A6A14700E4312B /* Sparkle.framework in Copy Files */ = {isa = PBXBuildFile; fileRef = 8DC2EF5B0486A6940098B216 /* Sparkle.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 142E0E0919A83AAC00E4312B /* SUBinaryDeltaTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 142E0E0819A83AAC00E4312B /* SUBinaryDeltaTest.m */; }; 14652F8019A9740F00959E44 /* Security.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 61B5F8F609C4CEB300B25A18 /* Security.framework */; }; 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, ); }; }; 14732BD019610A0D00593899 /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0867D69BFE84028FC02AAC07 /* Foundation.framework */; }; 14732BD119610A1200593899 /* AppKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0867D6A5FE840307C02AAC07 /* AppKit.framework */; }; 14732BD319610A1800593899 /* XCTest.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 14732BD219610A1800593899 /* XCTest.framework */; }; 1495006F195FCE1800BC5B5B /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0867D69BFE84028FC02AAC07 /* Foundation.framework */; }; 14950072195FCE4B00BC5B5B /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0867D69BFE84028FC02AAC07 /* Foundation.framework */; }; 14950073195FCE4E00BC5B5B /* AppKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0867D6A5FE840307C02AAC07 /* AppKit.framework */; }; 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 */; }; 1EAA4C8B2132C7BF00604473 /* ReleaseNotesColorStyle.css in Resources */ = {isa = PBXBuildFile; fileRef = 1EAA4C8A2132C7BF00604473 /* ReleaseNotesColorStyle.css */; }; 3772FEA913DE0B6B00F79537 /* SUVersionDisplayProtocol.h in Headers */ = {isa = PBXBuildFile; fileRef = 3772FEA813DE0B6B00F79537 /* SUVersionDisplayProtocol.h */; settings = {ATTRIBUTES = (Public, ); }; }; 55C14BEF136EF21700649790 /* SUStatus.xib in Resources */ = {isa = PBXBuildFile; fileRef = 55C14BD8136EF00C00649790 /* SUStatus.xib */; }; 55C14F06136EF6DB00649790 /* SULog.h in Headers */ = {isa = PBXBuildFile; fileRef = 55C14F04136EF6DB00649790 /* SULog.h */; }; 55C14F07136EF6DB00649790 /* SULog.m in Sources */ = {isa = PBXBuildFile; fileRef = 55C14F05136EF6DB00649790 /* SULog.m */; }; 55E6F33319EC9F6C00005E76 /* SUErrors.h in Headers */ = {isa = PBXBuildFile; fileRef = 55E6F33219EC9F6C00005E76 /* SUErrors.h */; settings = {ATTRIBUTES = (Public, ); }; }; 5A06357023FE332300478A72 /* libed25519.a in Frameworks */ = {isa = PBXBuildFile; fileRef = EA1E282D22B660BE004AA304 /* libed25519.a */; }; 5A06357323FE333600478A72 /* libed25519.a in Frameworks */ = {isa = PBXBuildFile; fileRef = EA1E282D22B660BE004AA304 /* libed25519.a */; }; 5A06357423FE33A400478A72 /* libed25519.a in Frameworks */ = {isa = PBXBuildFile; fileRef = EA1E282D22B660BE004AA304 /* libed25519.a */; }; 5A4094481C74EA5200983BE0 /* SUAppcastTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5AA4DCD01C73E5510078F128 /* SUAppcastTest.swift */; }; 5A5DD401249585E70045EB3E /* SUUpdateValidatorTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5A5DD400249585E70045EB3E /* SUUpdateValidatorTest.swift */; }; 5A5DD402249586840045EB3E /* SUUpdateValidator.m in Sources */ = {isa = PBXBuildFile; fileRef = 729924931DF4A45000DBCDF5 /* SUUpdateValidator.m */; }; 5A5DD40424958B000045EB3E /* SUUpdateValidatorTest in Resources */ = {isa = PBXBuildFile; fileRef = 5A5DD40324958AFF0045EB3E /* SUUpdateValidatorTest */; }; 5A5DD41D249F116E0045EB3E /* test-relative-urls.xml in Resources */ = {isa = PBXBuildFile; fileRef = 5A5DD41B249F0F4B0045EB3E /* test-relative-urls.xml */; }; 5A6DD17123FE1FFC000AEF33 /* SUSignatures.m in Sources */ = {isa = PBXBuildFile; fileRef = EA1E286D22B665E8004AA304 /* SUSignatures.m */; }; 5AA89BA523FE27660094DAB8 /* SUSignatures.m in Sources */ = {isa = PBXBuildFile; fileRef = EA1E286D22B665E8004AA304 /* SUSignatures.m */; }; 5AA89BA623FE276A0094DAB8 /* SUSignatures.m in Sources */ = {isa = PBXBuildFile; fileRef = EA1E286D22B665E8004AA304 /* SUSignatures.m */; }; 5AD0FA7F1C73F2E2004BCEFF /* testappcast.xml in Resources */ = {isa = PBXBuildFile; fileRef = 5AD0FA7E1C73F2E2004BCEFF /* testappcast.xml */; }; 5AE459001C34118500E3BB47 /* SUUpdaterTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 14950074195FDF5900BC5B5B /* SUUpdaterTest.m */; }; 5AE459021C34118500E3BB47 /* SUVersionComparisonTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 61227A150DB548B800AB99EA /* SUVersionComparisonTest.m */; }; 5AF6C74F1AEA46D10014A3AB /* test.pkg in Resources */ = {isa = PBXBuildFile; fileRef = 5AF6C74E1AEA46D10014A3AB /* test.pkg */; }; 5AF6C7541AEA49840014A3AB /* SUInstallerTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 5AF6C74C1AEA40760014A3AB /* SUInstallerTest.m */; }; 5AF9DC3C1981DBEE001EA135 /* SUSignatureVerifierTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 5AF9DC3B1981DBEE001EA135 /* SUSignatureVerifierTest.m */; }; 5D06E8FF0FD68D6D005AE3F6 /* libbz2.dylib in Frameworks */ = {isa = PBXBuildFile; fileRef = 5D06E8FB0FD68D61005AE3F6 /* libbz2.dylib */; }; 5D06E9050FD68D7D005AE3F6 /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0867D69BFE84028FC02AAC07 /* Foundation.framework */; }; 5D1AF58B0FD7678C0065DB48 /* libxar.1.dylib in Frameworks */ = {isa = PBXBuildFile; fileRef = 5D1AF5890FD7678C0065DB48 /* libxar.1.dylib */; }; 5D1AF5900FD767AD0065DB48 /* libxml2.dylib in Frameworks */ = {isa = PBXBuildFile; fileRef = 5D1AF58F0FD767AD0065DB48 /* libxml2.dylib */; }; 5D1AF59A0FD767E50065DB48 /* libz.dylib in Frameworks */ = {isa = PBXBuildFile; fileRef = 5D1AF5990FD767E50065DB48 /* libz.dylib */; }; 5F1510A21C96E591006E1629 /* testnamespaces.xml in Resources */ = {isa = PBXBuildFile; fileRef = 5F1510A11C96E591006E1629 /* testnamespaces.xml */; }; 61299A5C09CA6D4500B7442F /* SUConstants.h in Headers */ = {isa = PBXBuildFile; fileRef = 61299A5B09CA6D4500B7442F /* SUConstants.h */; }; 61299A6009CA6EB100B7442F /* SUConstants.m in Sources */ = {isa = PBXBuildFile; fileRef = 61299A5F09CA6EB100B7442F /* SUConstants.m */; }; 61299B3609CB04E000B7442F /* Sparkle.h in Headers */ = {isa = PBXBuildFile; fileRef = 61299B3509CB04E000B7442F /* Sparkle.h */; settings = {ATTRIBUTES = (Public, ); }; }; 612DCBB00D488BC60015DBEA /* SUUpdatePermissionPrompt.m in Sources */ = {isa = PBXBuildFile; fileRef = 612DCBAE0D488BC60015DBEA /* SUUpdatePermissionPrompt.m */; }; 6196CFF909C72148000DC222 /* SUStatusController.h in Headers */ = {isa = PBXBuildFile; fileRef = 6196CFE309C71ADE000DC222 /* SUStatusController.h */; settings = {ATTRIBUTES = (); }; }; 6196CFFA09C72149000DC222 /* SUStatusController.m in Sources */ = {isa = PBXBuildFile; fileRef = 6196CFE409C71ADE000DC222 /* SUStatusController.m */; }; 61A2259E0D1C495D00430CCD /* SUVersionComparisonProtocol.h in Headers */ = {isa = PBXBuildFile; fileRef = 61A2259C0D1C495D00430CCD /* SUVersionComparisonProtocol.h */; settings = {ATTRIBUTES = (Public, ); }; }; 61A225A40D1C4AC000430CCD /* SUStandardVersionComparator.h in Headers */ = {isa = PBXBuildFile; fileRef = 61A225A20D1C4AC000430CCD /* SUStandardVersionComparator.h */; settings = {ATTRIBUTES = (Public, ); }; }; 61A225A50D1C4AC000430CCD /* SUStandardVersionComparator.m in Sources */ = {isa = PBXBuildFile; fileRef = 61A225A30D1C4AC000430CCD /* SUStandardVersionComparator.m */; }; 61A2279C0D1CEE7600430CCD /* SUSystemProfiler.h in Headers */ = {isa = PBXBuildFile; fileRef = 61A2279A0D1CEE7600430CCD /* SUSystemProfiler.h */; settings = {ATTRIBUTES = (); }; }; 61A2279D0D1CEE7600430CCD /* SUSystemProfiler.m in Sources */ = {isa = PBXBuildFile; fileRef = 61A2279B0D1CEE7600430CCD /* SUSystemProfiler.m */; }; 61AAE8280A321A7F00D8810D /* Sparkle.strings in Resources */ = {isa = PBXBuildFile; fileRef = 61AAE8220A321A7F00D8810D /* Sparkle.strings */; }; 61B5F8ED09C4CE3C00B25A18 /* SPUUpdater.h in Headers */ = {isa = PBXBuildFile; fileRef = 61B5F8E309C4CE3C00B25A18 /* SPUUpdater.h */; settings = {ATTRIBUTES = (Public, ); }; }; 61B5F8EE09C4CE3C00B25A18 /* SPUUpdater.m in Sources */ = {isa = PBXBuildFile; fileRef = 61B5F8E409C4CE3C00B25A18 /* SPUUpdater.m */; }; 61B5F90F09C4CF3A00B25A18 /* Sparkle.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 8DC2EF5B0486A6940098B216 /* Sparkle.framework */; }; 61B5F93009C4CFDC00B25A18 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 61B5F92409C4CFC900B25A18 /* main.m */; }; 61B5FBB709C4FAFF00B25A18 /* SUAppcast.m in Sources */ = {isa = PBXBuildFile; fileRef = 61B5FB9509C4F04600B25A18 /* SUAppcast.m */; }; 61B5FC0D09C4FC8200B25A18 /* SUAppcast.h in Headers */ = {isa = PBXBuildFile; fileRef = 61B5FB9409C4F04600B25A18 /* SUAppcast.h */; settings = {ATTRIBUTES = (Public, ); }; }; 61B5FC6F09C51F4900B25A18 /* SUAppcastItem.m in Sources */ = {isa = PBXBuildFile; fileRef = 61B5FC5409C5182000B25A18 /* SUAppcastItem.m */; }; 61B5FC7009C51F4A00B25A18 /* SUAppcastItem.h in Headers */ = {isa = PBXBuildFile; fileRef = 61B5FC5309C5182000B25A18 /* SUAppcastItem.h */; settings = {ATTRIBUTES = (Public, ); }; }; 61B5FCDE09C52A9F00B25A18 /* SUUpdateAlert.m in Sources */ = {isa = PBXBuildFile; fileRef = 61B5FCA109C5228F00B25A18 /* SUUpdateAlert.m */; }; 61B5FCDF09C52A9F00B25A18 /* SUUpdateAlert.h in Headers */ = {isa = PBXBuildFile; fileRef = 61B5FCA009C5228F00B25A18 /* SUUpdateAlert.h */; settings = {ATTRIBUTES = (); }; }; 61EF67560E25B58D00F754E0 /* SUHost.m in Sources */ = {isa = PBXBuildFile; fileRef = 61EF67550E25B58D00F754E0 /* SUHost.m */; }; 61EF67590E25C5B400F754E0 /* SUHost.h in Headers */ = {isa = PBXBuildFile; fileRef = 61EF67580E25C5B400F754E0 /* SUHost.h */; }; 654F352529B1548700B10EEB /* Security.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 61B5F8F609C4CEB300B25A18 /* Security.framework */; }; 654F352829B154B500B10EEB /* SystemConfiguration.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 55C14F31136EFC2400649790 /* SystemConfiguration.framework */; }; 654F352929B154BB00B10EEB /* ServiceManagement.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 724BB3B51D35AAC3005D534A /* ServiceManagement.framework */; }; 654F352D29B1551000B10EEB /* CoreServices.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 721BC20A1D17A5AD002BC71E /* CoreServices.framework */; }; 7202DC9A269ABD3500737EC4 /* testappcast_phasedRollout.xml in Resources */ = {isa = PBXBuildFile; fileRef = 7202DC99269ABD3500737EC4 /* testappcast_phasedRollout.xml */; }; 72045CE026FEE535004F96E5 /* Downloader.xpc in Copy XPC Services */ = {isa = PBXBuildFile; fileRef = 726E07EF1CAF37BD001A286B /* Downloader.xpc */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; 72045CE126FEE535004F96E5 /* InstallerConnection.xpc in Copy XPC Services */ = {isa = PBXBuildFile; fileRef = 724BB36C1D31D0B7005D534A /* InstallerConnection.xpc */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; 72045CE226FEE535004F96E5 /* Installer.xpc in Copy XPC Services */ = {isa = PBXBuildFile; fileRef = 726E07AD1CAF08D6001A286B /* Installer.xpc */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; 72045CE326FEE535004F96E5 /* InstallerStatus.xpc in Copy XPC Services */ = {isa = PBXBuildFile; fileRef = 724BB3931D333832005D534A /* InstallerStatus.xpc */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; 720595EF1D700568000572E8 /* SUApplicationInfo.m in Sources */ = {isa = PBXBuildFile; fileRef = 725602D41C83551C00DAA70E /* SUApplicationInfo.m */; }; 7205C4411E13049400E370AE /* main.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7205C4401E13049400E370AE /* main.swift */; }; 7205C44C1E1304CE00E370AE /* Appcast.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7205C4471E1304CE00E370AE /* Appcast.swift */; }; 7205C44D1E1304CE00E370AE /* ArchiveItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7205C4481E1304CE00E370AE /* ArchiveItem.swift */; }; 7205C44F1E1304CE00E370AE /* FeedXML.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7205C44A1E1304CE00E370AE /* FeedXML.swift */; }; 7205C4501E1304CE00E370AE /* Unarchive.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7205C44B1E1304CE00E370AE /* Unarchive.swift */; }; 7205C4561E13060D00E370AE /* SUStandardVersionComparator.m in Sources */ = {isa = PBXBuildFile; fileRef = 61A225A30D1C4AC000430CCD /* SUStandardVersionComparator.m */; }; 7205C4571E13061F00E370AE /* SUUnarchiver.m in Sources */ = {isa = PBXBuildFile; fileRef = 7267E5851D3D89B300D1BF90 /* SUUnarchiver.m */; }; 7205C4581E13061F00E370AE /* SUUnarchiverNotifier.m in Sources */ = {isa = PBXBuildFile; fileRef = 72316BD21E0DA8430039EFD9 /* SUUnarchiverNotifier.m */; }; 7205C4591E13062500E370AE /* SUPipedUnarchiver.m in Sources */ = {isa = PBXBuildFile; fileRef = 7267E5831D3D89B300D1BF90 /* SUPipedUnarchiver.m */; }; 7205C45A1E13063E00E370AE /* SUDiskImageUnarchiver.m in Sources */ = {isa = PBXBuildFile; fileRef = 7267E5811D3D89B300D1BF90 /* SUDiskImageUnarchiver.m */; }; 7205C45B1E13064C00E370AE /* SUBinaryDeltaUnarchiver.m in Sources */ = {isa = PBXBuildFile; fileRef = 7267E57E1D3D896700D1BF90 /* SUBinaryDeltaUnarchiver.m */; }; 7205C45C1E13065800E370AE /* SULog.m in Sources */ = {isa = PBXBuildFile; fileRef = 55C14F05136EF6DB00649790 /* SULog.m */; }; 7205C45D1E13065F00E370AE /* SUConstants.m in Sources */ = {isa = PBXBuildFile; fileRef = 61299A5F09CA6EB100B7442F /* SUConstants.m */; }; 7205C45E1E13066800E370AE /* SUFileManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 7267E5E41D3D90AA00D1BF90 /* SUFileManager.m */; }; 7205C45F1E13066F00E370AE /* SUBinaryDeltaApply.m in Sources */ = {isa = PBXBuildFile; fileRef = 7267E56F1D3D895B00D1BF90 /* SUBinaryDeltaApply.m */; }; 7205C4611E13069000E370AE /* SUBinaryDeltaCreate.m in Sources */ = {isa = PBXBuildFile; fileRef = 7267E5731D3D895B00D1BF90 /* SUBinaryDeltaCreate.m */; }; 7205C4621E1306A600E370AE /* SUBinaryDeltaCommon.m in Sources */ = {isa = PBXBuildFile; fileRef = 7267E5711D3D895B00D1BF90 /* SUBinaryDeltaCommon.m */; }; 7205C4631E1306B500E370AE /* libxar.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = 726E07681CA616A4001A286B /* libxar.tbd */; }; 7205C4641E1306BE00E370AE /* libbz2.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = 726E076A1CA616B3001A286B /* libbz2.tbd */; }; 720AC2A42618E85700E25A3E /* SPUInstallationInfo.m in Sources */ = {isa = PBXBuildFile; fileRef = 7267E5B51D3D8AEE00D1BF90 /* SPUInstallationInfo.m */; }; 720AC2C92618E89500E25A3E /* SUAppcastItem.m in Sources */ = {isa = PBXBuildFile; fileRef = 61B5FC5409C5182000B25A18 /* SUAppcastItem.m */; }; 720AC2DC2618E8A500E25A3E /* SPUSecureCoding.m in Sources */ = {isa = PBXBuildFile; fileRef = 726E075B1CA3A6D6001A286B /* SPUSecureCoding.m */; }; 720B16451C66433D006985FB /* SUTestApplicationTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 720B16431C66433D006985FB /* SUTestApplicationTest.swift */; }; 720DC50427A51A6500DFF3EC /* testappcast_minimumAutoupdateVersionSkipping.xml in Resources */ = {isa = PBXBuildFile; fileRef = 720DC50327A51A6500DFF3EC /* testappcast_minimumAutoupdateVersionSkipping.xml */; }; 720DC50627A62CDC00DFF3EC /* testappcast_minimumAutoupdateVersionSkipping2.xml in Resources */ = {isa = PBXBuildFile; fileRef = 720DC50527A62CDC00DFF3EC /* testappcast_minimumAutoupdateVersionSkipping2.xml */; }; 720E217B1D0D00BF003A311C /* SPUUpdaterCycle.h in Headers */ = {isa = PBXBuildFile; fileRef = 720E21791D0D00BF003A311C /* SPUUpdaterCycle.h */; }; 720E217C1D0D00BF003A311C /* SPUUpdaterCycle.m in Sources */ = {isa = PBXBuildFile; fileRef = 720E217A1D0D00BF003A311C /* SPUUpdaterCycle.m */; }; 7210C7681B9A9A1500EB90AC /* SUUnarchiverTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7210C7671B9A9A1500EB90AC /* SUUnarchiverTest.swift */; }; 72162B081C82C9600013C1C5 /* SULocalizations.h in Headers */ = {isa = PBXBuildFile; fileRef = 72162B071C82C9600013C1C5 /* SULocalizations.h */; }; 721BC20E1D1CDE55002BC71E /* SPULocalCacheDirectory.h in Headers */ = {isa = PBXBuildFile; fileRef = 721BC20C1D1CDE55002BC71E /* SPULocalCacheDirectory.h */; }; 721BC20F1D1CDE55002BC71E /* SPULocalCacheDirectory.m in Sources */ = {isa = PBXBuildFile; fileRef = 721BC20D1D1CDE55002BC71E /* SPULocalCacheDirectory.m */; }; 721BC2101D1CDE55002BC71E /* SPULocalCacheDirectory.m in Sources */ = {isa = PBXBuildFile; fileRef = 721BC20D1D1CDE55002BC71E /* SPULocalCacheDirectory.m */; }; 721C245A1CB75756005440CB /* Cocoa.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 525A278F133D6AE900FD8D70 /* Cocoa.framework */; }; 721C245C1CB7576E005440CB /* SUStatusController.m in Sources */ = {isa = PBXBuildFile; fileRef = 6196CFE409C71ADE000DC222 /* SUStatusController.m */; }; 721C245E1CB757DE005440CB /* SUConstants.m in Sources */ = {isa = PBXBuildFile; fileRef = 61299A5F09CA6EB100B7442F /* SUConstants.m */; }; 721C24611CB75C5D005440CB /* SUHost.m in Sources */ = {isa = PBXBuildFile; fileRef = 61EF67550E25B58D00F754E0 /* SUHost.m */; }; 721C24621CB75C68005440CB /* SULog.m in Sources */ = {isa = PBXBuildFile; fileRef = 55C14F05136EF6DB00649790 /* SULog.m */; }; 721CF1AA1AD7647000D9AC09 /* libxar.1.dylib in Frameworks */ = {isa = PBXBuildFile; fileRef = 5D1AF5890FD7678C0065DB48 /* libxar.1.dylib */; }; 721CF1AB1AD764EB00D9AC09 /* libbz2.dylib in Frameworks */ = {isa = PBXBuildFile; fileRef = 5D06E8FB0FD68D61005AE3F6 /* libbz2.dylib */; }; 721D588D25BE59F900D23BEA /* SUPhasedUpdateGroupInfo.h in Headers */ = {isa = PBXBuildFile; fileRef = 721D588B25BE59F900D23BEA /* SUPhasedUpdateGroupInfo.h */; }; 721D588F25BE59F900D23BEA /* SUPhasedUpdateGroupInfo.m in Sources */ = {isa = PBXBuildFile; fileRef = 721D588C25BE59F900D23BEA /* SUPhasedUpdateGroupInfo.m */; }; 721D5A8525C65D3F00D23BEA /* SUFlatPackageUnarchiver.m in Sources */ = {isa = PBXBuildFile; fileRef = 721D5A8425C65D3F00D23BEA /* SUFlatPackageUnarchiver.m */; }; 721D5ABC25C680A300D23BEA /* SUFlatPackageUnarchiver.m in Sources */ = {isa = PBXBuildFile; fileRef = 721D5A8425C65D3F00D23BEA /* SUFlatPackageUnarchiver.m */; }; 721D5B1B25C692BB00D23BEA /* SUFlatPackageUnarchiver.m in Sources */ = {isa = PBXBuildFile; fileRef = 721D5A8425C65D3F00D23BEA /* SUFlatPackageUnarchiver.m */; }; 721D8A611D48413B0032E472 /* Security.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 61B5F8F609C4CEB300B25A18 /* Security.framework */; }; 721D8A7B1D4963190032E472 /* SPULocalCacheDirectory.m in Sources */ = {isa = PBXBuildFile; fileRef = 721BC20D1D1CDE55002BC71E /* SPULocalCacheDirectory.m */; }; 721D8A861D4ADFEB0032E472 /* SPULocalCacheDirectory.m in Sources */ = {isa = PBXBuildFile; fileRef = 721BC20D1D1CDE55002BC71E /* SPULocalCacheDirectory.m */; }; 721D8A871D4C5BF10032E472 /* SULog.m in Sources */ = {isa = PBXBuildFile; fileRef = 55C14F05136EF6DB00649790 /* SULog.m */; }; 722545B626805FF80036465C /* testappcast_info_updates.xml in Resources */ = {isa = PBXBuildFile; fileRef = 722545B526805FF80036465C /* testappcast_info_updates.xml */; }; 72266A872946359600645376 /* SUFileManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 7267E5E41D3D90AA00D1BF90 /* SUFileManager.m */; }; 72266A88294635BA00645376 /* SUAppcastDriver.m in Sources */ = {isa = PBXBuildFile; fileRef = 72B767C91C9B707000A07552 /* SUAppcastDriver.m */; }; 72266A8A2946493C00645376 /* SUCodeSigningVerifier.m in Sources */ = {isa = PBXBuildFile; fileRef = 7267E5991D3D8A5A00D1BF90 /* SUCodeSigningVerifier.m */; }; 72266A8B29464CEA00645376 /* SUHost.m in Sources */ = {isa = PBXBuildFile; fileRef = 61EF67550E25B58D00F754E0 /* SUHost.m */; }; 72266A8C29464D0200645376 /* SUSignatures.m in Sources */ = {isa = PBXBuildFile; fileRef = EA1E286D22B665E8004AA304 /* SUSignatures.m */; }; 7229E1B61C97C91100CB50D0 /* SPUUpdateDriver.h in Headers */ = {isa = PBXBuildFile; fileRef = 7229E1B51C97C91100CB50D0 /* SPUUpdateDriver.h */; }; 7229E1B91C97CC4D00CB50D0 /* SPUScheduledUpdateDriver.h in Headers */ = {isa = PBXBuildFile; fileRef = 7229E1B71C97CC4D00CB50D0 /* SPUScheduledUpdateDriver.h */; }; 7229E1BA1C97CC4D00CB50D0 /* SPUScheduledUpdateDriver.m in Sources */ = {isa = PBXBuildFile; fileRef = 7229E1B81C97CC4D00CB50D0 /* SPUScheduledUpdateDriver.m */; }; 7229E1BD1C98EFF200CB50D0 /* SPUDownloadDriver.h in Headers */ = {isa = PBXBuildFile; fileRef = 7229E1BB1C98EFF200CB50D0 /* SPUDownloadDriver.h */; }; 7229E1BE1C98EFF200CB50D0 /* SPUDownloadDriver.m in Sources */ = {isa = PBXBuildFile; fileRef = 7229E1BC1C98EFF200CB50D0 /* SPUDownloadDriver.m */; }; 722C9539286EA68C0033908A /* SPUGentleUserDriverReminders.h in Headers */ = {isa = PBXBuildFile; fileRef = 722C9538286EA54A0033908A /* SPUGentleUserDriverReminders.h */; settings = {ATTRIBUTES = (Private, ); }; }; 722FB7E5260EE53F00EB571C /* SUNormalization.m in Sources */ = {isa = PBXBuildFile; fileRef = 722FB7E4260EE53F00EB571C /* SUNormalization.m */; }; 722FB7E6260EE53F00EB571C /* SUNormalization.m in Sources */ = {isa = PBXBuildFile; fileRef = 722FB7E4260EE53F00EB571C /* SUNormalization.m */; }; 7230BCDE2E22E2BC00B71297 /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 7230BCDD2E22E2BC00B71297 /* Images.xcassets */; }; 72316BD31E0DA8430039EFD9 /* SUUnarchiverNotifier.m in Sources */ = {isa = PBXBuildFile; fileRef = 72316BD21E0DA8430039EFD9 /* SUUnarchiverNotifier.m */; }; 72316BD41E0DB37E0039EFD9 /* SUUnarchiverNotifier.m in Sources */ = {isa = PBXBuildFile; fileRef = 72316BD21E0DA8430039EFD9 /* SUUnarchiverNotifier.m */; }; 723414172EBFB482009727E0 /* testappcast_arm64HardwareRequirement.xml in Resources */ = {isa = PBXBuildFile; fileRef = 723414162EBFB482009727E0 /* testappcast_arm64HardwareRequirement.xml */; }; 723ABDDB259A9E8600BDB4FA /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 723ABDD9259A9E8600BDB4FA /* InfoPlist.strings */; }; 723ABE02259A9E9E00BDB4FA /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 723ABE00259A9E9E00BDB4FA /* MainMenu.xib */; }; 723ABF1B259D055E00BDB4FA /* SUReleaseNotesView.h in Headers */ = {isa = PBXBuildFile; fileRef = 723ABF1A259D055E00BDB4FA /* SUReleaseNotesView.h */; }; 723ABF30259D062F00BDB4FA /* SULegacyWebView.h in Headers */ = {isa = PBXBuildFile; fileRef = 723ABF2E259D062F00BDB4FA /* SULegacyWebView.h */; }; 723ABF31259D062F00BDB4FA /* SULegacyWebView.m in Sources */ = {isa = PBXBuildFile; fileRef = 723ABF2F259D062F00BDB4FA /* SULegacyWebView.m */; }; 723ABFC4259D4CB300BDB4FA /* SUWKWebView.h in Headers */ = {isa = PBXBuildFile; fileRef = 723ABFC2259D4CB300BDB4FA /* SUWKWebView.h */; }; 723ABFC5259D4CB300BDB4FA /* SUWKWebView.m in Sources */ = {isa = PBXBuildFile; fileRef = 723ABFC3259D4CB300BDB4FA /* SUWKWebView.m */; }; 723AC010259DBDAA00BDB4FA /* SUReleaseNotesCommon.h in Headers */ = {isa = PBXBuildFile; fileRef = 723AC00E259DBDAA00BDB4FA /* SUReleaseNotesCommon.h */; }; 723AC011259DBDAA00BDB4FA /* SUReleaseNotesCommon.m in Sources */ = {isa = PBXBuildFile; fileRef = 723AC00F259DBDAA00BDB4FA /* SUReleaseNotesCommon.m */; }; 723AD12F29922B5F006BB02F /* test-dangerous-link.xml in Resources */ = {isa = PBXBuildFile; fileRef = 723AD12E29922B5F006BB02F /* test-dangerous-link.xml */; }; 723B5DA71CF7AB0100365F95 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 723B5DA01CF7AB0100365F95 /* main.m */; }; 723B5DA91CF7AB0100365F95 /* SPUDownloader.m in Sources */ = {isa = PBXBuildFile; fileRef = 723B5DA31CF7AB0100365F95 /* SPUDownloader.m */; }; 723B5DAA1CF7AB6A00365F95 /* SPUDownloader.m in Sources */ = {isa = PBXBuildFile; fileRef = 723B5DA31CF7AB0100365F95 /* SPUDownloader.m */; }; 723C8A541E2D60DB00C14942 /* SUTouchBarButtonGroup.h in Headers */ = {isa = PBXBuildFile; fileRef = 723C8A521E2D60DB00C14942 /* SUTouchBarButtonGroup.h */; }; 723C8A551E2D60DB00C14942 /* SUTouchBarButtonGroup.m in Sources */ = {isa = PBXBuildFile; fileRef = 723C8A531E2D60DB00C14942 /* SUTouchBarButtonGroup.m */; }; 723C8A561E2D60DB00C14942 /* SUTouchBarButtonGroup.m in Sources */ = {isa = PBXBuildFile; fileRef = 723C8A531E2D60DB00C14942 /* SUTouchBarButtonGroup.m */; }; 723EDC3F26885A8E000BCBA4 /* testappcast_channels.xml in Resources */ = {isa = PBXBuildFile; fileRef = 723EDC3E26885A8E000BCBA4 /* testappcast_channels.xml */; }; 7240852E2CA11C1400ED2FCD /* libz.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = 7240852D2CA11C1400ED2FCD /* libz.tbd */; }; 7245495C2ECA648000ABA991 /* SPUUpdaterSettings+Debug.h in Headers */ = {isa = PBXBuildFile; fileRef = 7245495B2ECA648000ABA991 /* SPUUpdaterSettings+Debug.h */; }; 72464F701E1F31E000FB341C /* SUOperatingSystem.m in Sources */ = {isa = PBXBuildFile; fileRef = 726F2CE41BC9C33D001971A4 /* SUOperatingSystem.m */; }; 72464F7C1E2097F600FB341C /* SUOperatingSystem.m in Sources */ = {isa = PBXBuildFile; fileRef = 726F2CE41BC9C33D001971A4 /* SUOperatingSystem.m */; }; 72464F7E1E21ED8C00FB341C /* SUHost.m in Sources */ = {isa = PBXBuildFile; fileRef = 61EF67550E25B58D00F754E0 /* SUHost.m */; }; 7246E0A31C83B685003B4E75 /* SPUStandardUpdaterController.h in Headers */ = {isa = PBXBuildFile; fileRef = 7246E0A11C83B685003B4E75 /* SPUStandardUpdaterController.h */; settings = {ATTRIBUTES = (Public, ); }; }; 7246E0A41C83B685003B4E75 /* SPUStandardUpdaterController.m in Sources */ = {isa = PBXBuildFile; fileRef = 7246E0A21C83B685003B4E75 /* SPUStandardUpdaterController.m */; }; 724BB3711D31D0B7005D534A /* SUInstallerConnection.m in Sources */ = {isa = PBXBuildFile; fileRef = 724BB3701D31D0B7005D534A /* SUInstallerConnection.m */; }; 724BB3731D31D0B7005D534A /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 724BB3721D31D0B7005D534A /* main.m */; }; 724BB3871D32A167005D534A /* SUXPCInstallerConnection.h in Headers */ = {isa = PBXBuildFile; fileRef = 724BB3851D32A167005D534A /* SUXPCInstallerConnection.h */; }; 724BB3881D32A167005D534A /* SUXPCInstallerConnection.m in Sources */ = {isa = PBXBuildFile; fileRef = 724BB3861D32A167005D534A /* SUXPCInstallerConnection.m */; }; 724BB3891D32B915005D534A /* SUInstallerConnection.m in Sources */ = {isa = PBXBuildFile; fileRef = 724BB3701D31D0B7005D534A /* SUInstallerConnection.m */; }; 724BB3981D333832005D534A /* SUInstallerStatus.m in Sources */ = {isa = PBXBuildFile; fileRef = 724BB3971D333832005D534A /* SUInstallerStatus.m */; }; 724BB39A1D333832005D534A /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 724BB3991D333832005D534A /* main.m */; }; 724BB3A81D33461B005D534A /* SUXPCInstallerStatus.h in Headers */ = {isa = PBXBuildFile; fileRef = 724BB3A61D33461B005D534A /* SUXPCInstallerStatus.h */; }; 724BB3A91D33461B005D534A /* SUXPCInstallerStatus.m in Sources */ = {isa = PBXBuildFile; fileRef = 724BB3A71D33461B005D534A /* SUXPCInstallerStatus.m */; }; 724BB3AA1D3347C2005D534A /* SUInstallerStatus.m in Sources */ = {isa = PBXBuildFile; fileRef = 724BB3971D333832005D534A /* SUInstallerStatus.m */; }; 724BB3B71D35ABA8005D534A /* Security.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 61B5F8F609C4CEB300B25A18 /* Security.framework */; }; 724F76F91D6EAD0D00ECD062 /* Cocoa.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 525A278F133D6AE900FD8D70 /* Cocoa.framework */; }; 72526BE62EF3349D005791ED /* testappcast_minimumUpdateVersion.xml in Resources */ = {isa = PBXBuildFile; fileRef = 72526BE52EF3349D005791ED /* testappcast_minimumUpdateVersion.xml */; }; 725453552C04DB3700362123 /* SparkleTestCodeSign_apfs_lzma_aux_files_adhoc.dmg in Resources */ = {isa = PBXBuildFile; fileRef = 725453542C04DB3700362123 /* SparkleTestCodeSign_apfs_lzma_aux_files_adhoc.dmg */; }; 725602D51C83551C00DAA70E /* SUApplicationInfo.h in Headers */ = {isa = PBXBuildFile; fileRef = 725602D31C83551C00DAA70E /* SUApplicationInfo.h */; }; 725602D61C83551C00DAA70E /* SUApplicationInfo.m in Sources */ = {isa = PBXBuildFile; fileRef = 725602D41C83551C00DAA70E /* SUApplicationInfo.m */; }; 725B3A82263FBF0C0041AB8E /* testappcast_minimumAutoupdateVersion.xml in Resources */ = {isa = PBXBuildFile; fileRef = 725B3A81263FBF0C0041AB8E /* testappcast_minimumAutoupdateVersion.xml */; }; 725B81FA2781AEAF0041746F /* libcompression.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = 725B81F92781AEA40041746F /* libcompression.tbd */; }; 725B81FB2781CD930041746F /* libcompression.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = 725B81F92781AEA40041746F /* libcompression.tbd */; }; 725B81FC2781CDC20041746F /* libcompression.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = 725B81F92781AEA40041746F /* libcompression.tbd */; }; 725C2EAA2782EC61007CB7B5 /* main.swift in Sources */ = {isa = PBXBuildFile; fileRef = 725C2EA92782EC61007CB7B5 /* main.swift */; }; 725C2EAC2782EF3C007CB7B5 /* ArgumentParser in Frameworks */ = {isa = PBXBuildFile; productRef = 725C2EAB2782EF3C007CB7B5 /* ArgumentParser */; }; 725CB9571C7120410064365A /* SPUUserDriver.h in Headers */ = {isa = PBXBuildFile; fileRef = 725CB9561C7120410064365A /* SPUUserDriver.h */; settings = {ATTRIBUTES = (Public, ); }; }; 725CB95A1C7121830064365A /* SPUStandardUserDriver.h in Headers */ = {isa = PBXBuildFile; fileRef = 725CB9581C7121830064365A /* SPUStandardUserDriver.h */; settings = {ATTRIBUTES = (Public, ); }; }; 725CB95B1C7121830064365A /* SPUStandardUserDriver.m in Sources */ = {isa = PBXBuildFile; fileRef = 725CB9591C7121830064365A /* SPUStandardUserDriver.m */; }; 725EE480277BF13B00D820CE /* SPUXarDeltaArchive.m in Sources */ = {isa = PBXBuildFile; fileRef = 725EE47F277BF13B00D820CE /* SPUXarDeltaArchive.m */; }; 725EE482277BF44A00D820CE /* SPUXarDeltaArchive.m in Sources */ = {isa = PBXBuildFile; fileRef = 725EE47F277BF13B00D820CE /* SPUXarDeltaArchive.m */; }; 725EE483277C767A00D820CE /* SPUXarDeltaArchive.m in Sources */ = {isa = PBXBuildFile; fileRef = 725EE47F277BF13B00D820CE /* SPUXarDeltaArchive.m */; }; 725EE486277D375F00D820CE /* SPUDeltaArchive.m in Sources */ = {isa = PBXBuildFile; fileRef = 725EE485277D375F00D820CE /* SPUDeltaArchive.m */; }; 725EE487277D376000D820CE /* SPUDeltaArchive.m in Sources */ = {isa = PBXBuildFile; fileRef = 725EE485277D375F00D820CE /* SPUDeltaArchive.m */; }; 725EE488277D398100D820CE /* SPUDeltaArchive.m in Sources */ = {isa = PBXBuildFile; fileRef = 725EE485277D375F00D820CE /* SPUDeltaArchive.m */; }; 725EE489277D39B400D820CE /* SPUDeltaArchive.m in Sources */ = {isa = PBXBuildFile; fileRef = 725EE485277D375F00D820CE /* SPUDeltaArchive.m */; }; 725EE48A277D39B400D820CE /* SPUXarDeltaArchive.m in Sources */ = {isa = PBXBuildFile; fileRef = 725EE47F277BF13B00D820CE /* SPUXarDeltaArchive.m */; }; 725F97771C8A62F500265BE4 /* SUAdHocCodeSigning.m in Sources */ = {isa = PBXBuildFile; fileRef = 725F97751C8A62F500265BE4 /* SUAdHocCodeSigning.m */; }; 725F97781C8A65AC00265BE4 /* SUAdHocCodeSigning.m in Sources */ = {isa = PBXBuildFile; fileRef = 725F97751C8A62F500265BE4 /* SUAdHocCodeSigning.m */; }; 725F97841C8AA90000265BE4 /* SUPopUpTitlebarUserDriver.m in Sources */ = {isa = PBXBuildFile; fileRef = 725F97831C8AA90000265BE4 /* SUPopUpTitlebarUserDriver.m */; }; 725F97A51C8B304D00265BE4 /* SUInstallUpdateViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 725F97A31C8B304D00265BE4 /* SUInstallUpdateViewController.m */; }; 725F97A61C8B304D00265BE4 /* SUInstallUpdateViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 725F97A41C8B304D00265BE4 /* SUInstallUpdateViewController.xib */; }; 72666DC62B0B28F4001511B0 /* Secret.swift in Sources */ = {isa = PBXBuildFile; fileRef = 72666DC52B0B28F4001511B0 /* Secret.swift */; }; 72666DC72B0B28F4001511B0 /* Secret.swift in Sources */ = {isa = PBXBuildFile; fileRef = 72666DC52B0B28F4001511B0 /* Secret.swift */; }; 72666DC82B0B28F4001511B0 /* Secret.swift in Sources */ = {isa = PBXBuildFile; fileRef = 72666DC52B0B28F4001511B0 /* Secret.swift */; }; 7267E5751D3D895B00D1BF90 /* SUBinaryDeltaApply.m in Sources */ = {isa = PBXBuildFile; fileRef = 7267E56F1D3D895B00D1BF90 /* SUBinaryDeltaApply.m */; }; 7267E5761D3D895B00D1BF90 /* SUBinaryDeltaApply.m in Sources */ = {isa = PBXBuildFile; fileRef = 7267E56F1D3D895B00D1BF90 /* SUBinaryDeltaApply.m */; }; 7267E5771D3D895B00D1BF90 /* SUBinaryDeltaCommon.m in Sources */ = {isa = PBXBuildFile; fileRef = 7267E5711D3D895B00D1BF90 /* SUBinaryDeltaCommon.m */; }; 7267E5781D3D895B00D1BF90 /* SUBinaryDeltaCommon.m in Sources */ = {isa = PBXBuildFile; fileRef = 7267E5711D3D895B00D1BF90 /* SUBinaryDeltaCommon.m */; }; 7267E5791D3D895B00D1BF90 /* SUBinaryDeltaCreate.m in Sources */ = {isa = PBXBuildFile; fileRef = 7267E5731D3D895B00D1BF90 /* SUBinaryDeltaCreate.m */; }; 7267E57A1D3D895B00D1BF90 /* SUBinaryDeltaCreate.m in Sources */ = {isa = PBXBuildFile; fileRef = 7267E5731D3D895B00D1BF90 /* SUBinaryDeltaCreate.m */; }; 7267E57F1D3D896700D1BF90 /* SUBinaryDeltaUnarchiver.m in Sources */ = {isa = PBXBuildFile; fileRef = 7267E57E1D3D896700D1BF90 /* SUBinaryDeltaUnarchiver.m */; }; 7267E5861D3D89B300D1BF90 /* SUDiskImageUnarchiver.m in Sources */ = {isa = PBXBuildFile; fileRef = 7267E5811D3D89B300D1BF90 /* SUDiskImageUnarchiver.m */; }; 7267E5871D3D89B300D1BF90 /* SUPipedUnarchiver.m in Sources */ = {isa = PBXBuildFile; fileRef = 7267E5831D3D89B300D1BF90 /* SUPipedUnarchiver.m */; }; 7267E5881D3D89B300D1BF90 /* SUUnarchiver.m in Sources */ = {isa = PBXBuildFile; fileRef = 7267E5851D3D89B300D1BF90 /* SUUnarchiver.m */; }; 7267E59C1D3D8A5A00D1BF90 /* SUCodeSigningVerifier.m in Sources */ = {isa = PBXBuildFile; fileRef = 7267E5991D3D8A5A00D1BF90 /* SUCodeSigningVerifier.m */; }; 7267E59D1D3D8A5A00D1BF90 /* SUSignatureVerifier.m in Sources */ = {isa = PBXBuildFile; fileRef = 7267E59B1D3D8A5A00D1BF90 /* SUSignatureVerifier.m */; }; 7267E59F1D3D8A6F00D1BF90 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 7267E59E1D3D8A6F00D1BF90 /* main.m */; }; 7267E5A21D3D8A7E00D1BF90 /* AppInstaller.m in Sources */ = {isa = PBXBuildFile; fileRef = 7267E5A11D3D8A7E00D1BF90 /* AppInstaller.m */; }; 7267E5A81D3D8A9900D1BF90 /* AgentConnection.m in Sources */ = {isa = PBXBuildFile; fileRef = 7267E5A71D3D8A9900D1BF90 /* AgentConnection.m */; }; 7267E5AE1D3D8AB700D1BF90 /* SPUInstallationInputData.m in Sources */ = {isa = PBXBuildFile; fileRef = 7267E5AD1D3D8AB700D1BF90 /* SPUInstallationInputData.m */; }; 7267E5B11D3D8AD500D1BF90 /* SPUMessageTypes.h in Headers */ = {isa = PBXBuildFile; fileRef = 7267E5AF1D3D8AD500D1BF90 /* SPUMessageTypes.h */; }; 7267E5B21D3D8AD500D1BF90 /* SPUMessageTypes.m in Sources */ = {isa = PBXBuildFile; fileRef = 7267E5B01D3D8AD500D1BF90 /* SPUMessageTypes.m */; }; 7267E5B31D3D8AD500D1BF90 /* SPUMessageTypes.m in Sources */ = {isa = PBXBuildFile; fileRef = 7267E5B01D3D8AD500D1BF90 /* SPUMessageTypes.m */; }; 7267E5B61D3D8AEE00D1BF90 /* SPUInstallationInfo.h in Headers */ = {isa = PBXBuildFile; fileRef = 7267E5B41D3D8AEE00D1BF90 /* SPUInstallationInfo.h */; }; 7267E5B71D3D8AEE00D1BF90 /* SPUInstallationInfo.m in Sources */ = {isa = PBXBuildFile; fileRef = 7267E5B51D3D8AEE00D1BF90 /* SPUInstallationInfo.m */; }; 7267E5B81D3D8AEE00D1BF90 /* SPUInstallationInfo.m in Sources */ = {isa = PBXBuildFile; fileRef = 7267E5B51D3D8AEE00D1BF90 /* SPUInstallationInfo.m */; }; 7267E5C21D3D8B2700D1BF90 /* SUGuidedPackageInstaller.m in Sources */ = {isa = PBXBuildFile; fileRef = 7267E5BB1D3D8B2700D1BF90 /* SUGuidedPackageInstaller.m */; }; 7267E5C31D3D8B2700D1BF90 /* SUInstaller.m in Sources */ = {isa = PBXBuildFile; fileRef = 7267E5BD1D3D8B2700D1BF90 /* SUInstaller.m */; }; 7267E5C51D3D8B2700D1BF90 /* SUPlainInstaller.m in Sources */ = {isa = PBXBuildFile; fileRef = 7267E5C11D3D8B2700D1BF90 /* SUPlainInstaller.m */; }; 7267E5C91D3D8C4300D1BF90 /* CoreServices.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 721BC20A1D17A5AD002BC71E /* CoreServices.framework */; }; 7267E5CA1D3D8C4800D1BF90 /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0867D69BFE84028FC02AAC07 /* Foundation.framework */; }; 7267E5CB1D3D8C6400D1BF90 /* SUAppcastItem.m in Sources */ = {isa = PBXBuildFile; fileRef = 61B5FC5409C5182000B25A18 /* SUAppcastItem.m */; }; 7267E5CC1D3D8C6B00D1BF90 /* SUConstants.m in Sources */ = {isa = PBXBuildFile; fileRef = 61299A5F09CA6EB100B7442F /* SUConstants.m */; }; 7267E5CD1D3D8C7200D1BF90 /* SPUSecureCoding.m in Sources */ = {isa = PBXBuildFile; fileRef = 726E075B1CA3A6D6001A286B /* SPUSecureCoding.m */; }; 7267E5CE1D3D8C7500D1BF90 /* SULog.m in Sources */ = {isa = PBXBuildFile; fileRef = 55C14F05136EF6DB00649790 /* SULog.m */; }; 7267E5D51D3D8D2800D1BF90 /* libxar.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = 726E07681CA616A4001A286B /* libxar.tbd */; }; 7267E5D61D3D8D3500D1BF90 /* libbz2.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = 726E076A1CA616B3001A286B /* libbz2.tbd */; }; 7267E5D71D3D8D3F00D1BF90 /* SUHost.m in Sources */ = {isa = PBXBuildFile; fileRef = 61EF67550E25B58D00F754E0 /* SUHost.m */; }; 7267E5D81D3D8D4400D1BF90 /* SUStandardVersionComparator.m in Sources */ = {isa = PBXBuildFile; fileRef = 61A225A30D1C4AC000430CCD /* SUStandardVersionComparator.m */; }; 7267E5DC1D3D8F1E00D1BF90 /* SPUMessageTypes.m in Sources */ = {isa = PBXBuildFile; fileRef = 7267E5B01D3D8AD500D1BF90 /* SPUMessageTypes.m */; }; 7267E5DF1D3D8FFA00D1BF90 /* SPUInstallationInputData.m in Sources */ = {isa = PBXBuildFile; fileRef = 7267E5AD1D3D8AB700D1BF90 /* SPUInstallationInputData.m */; }; 7267E5E11D3D901600D1BF90 /* SPUMessageTypes.m in Sources */ = {isa = PBXBuildFile; fileRef = 7267E5B01D3D8AD500D1BF90 /* SPUMessageTypes.m */; }; 7267E5E51D3D90AA00D1BF90 /* SUFileManager.h in Headers */ = {isa = PBXBuildFile; fileRef = 7267E5E31D3D90AA00D1BF90 /* SUFileManager.h */; }; 7267E5E61D3D90AA00D1BF90 /* SUFileManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 7267E5E41D3D90AA00D1BF90 /* SUFileManager.m */; }; 7267E5E71D3D90AA00D1BF90 /* SUFileManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 7267E5E41D3D90AA00D1BF90 /* SUFileManager.m */; }; 7267E5E81D3D90AA00D1BF90 /* SUFileManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 7267E5E41D3D90AA00D1BF90 /* SUFileManager.m */; }; 7267E5EB1D3D90C200D1BF90 /* SUFileManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 7267E5E41D3D90AA00D1BF90 /* SUFileManager.m */; }; 7267E5EC1D3D912900D1BF90 /* SUInstaller.m in Sources */ = {isa = PBXBuildFile; fileRef = 7267E5BD1D3D8B2700D1BF90 /* SUInstaller.m */; }; 7267E5ED1D3D912E00D1BF90 /* SUUnarchiver.m in Sources */ = {isa = PBXBuildFile; fileRef = 7267E5851D3D89B300D1BF90 /* SUUnarchiver.m */; }; 7267E5EE1D3D915900D1BF90 /* SUBinaryDeltaApply.m in Sources */ = {isa = PBXBuildFile; fileRef = 7267E56F1D3D895B00D1BF90 /* SUBinaryDeltaApply.m */; }; 7267E5EF1D3D915900D1BF90 /* SUBinaryDeltaCommon.m in Sources */ = {isa = PBXBuildFile; fileRef = 7267E5711D3D895B00D1BF90 /* SUBinaryDeltaCommon.m */; }; 7267E5F01D3D915900D1BF90 /* SUBinaryDeltaCreate.m in Sources */ = {isa = PBXBuildFile; fileRef = 7267E5731D3D895B00D1BF90 /* SUBinaryDeltaCreate.m */; }; 7267E5F11D3D917A00D1BF90 /* SUBinaryDeltaUnarchiver.m in Sources */ = {isa = PBXBuildFile; fileRef = 7267E57E1D3D896700D1BF90 /* SUBinaryDeltaUnarchiver.m */; }; 7267E5F21D3D918000D1BF90 /* SUSignatureVerifier.m in Sources */ = {isa = PBXBuildFile; fileRef = 7267E59B1D3D8A5A00D1BF90 /* SUSignatureVerifier.m */; }; 7267E5F41D3D918B00D1BF90 /* SUGuidedPackageInstaller.m in Sources */ = {isa = PBXBuildFile; fileRef = 7267E5BB1D3D8B2700D1BF90 /* SUGuidedPackageInstaller.m */; }; 7267E5F61D3D919000D1BF90 /* SUDiskImageUnarchiver.m in Sources */ = {isa = PBXBuildFile; fileRef = 7267E5811D3D89B300D1BF90 /* SUDiskImageUnarchiver.m */; }; 7267E5F71D3D919600D1BF90 /* SUPlainInstaller.m in Sources */ = {isa = PBXBuildFile; fileRef = 7267E5C11D3D8B2700D1BF90 /* SUPlainInstaller.m */; }; 7267E5F81D3D91A800D1BF90 /* SUPipedUnarchiver.m in Sources */ = {isa = PBXBuildFile; fileRef = 7267E5831D3D89B300D1BF90 /* SUPipedUnarchiver.m */; }; 7267E5F91D3D92DA00D1BF90 /* Autoupdate in Copy Tools */ = {isa = PBXBuildFile; fileRef = 72B398D21D3D879300EE297F /* Autoupdate */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; 7267E5FA1D3DAC3600D1BF90 /* StatusInfo.m in Sources */ = {isa = PBXBuildFile; fileRef = 7267E5A41D3D8A8A00D1BF90 /* StatusInfo.m */; }; 7267E5FD1D3DD1B700D1BF90 /* SPUResumableUpdate.h in Headers */ = {isa = PBXBuildFile; fileRef = 7267E5FB1D3DD1B700D1BF90 /* SPUResumableUpdate.h */; }; 7269E494264798200088C213 /* SPUSkippedUpdate.h in Headers */ = {isa = PBXBuildFile; fileRef = 7269E492264798200088C213 /* SPUSkippedUpdate.h */; }; 7269E496264798200088C213 /* SPUSkippedUpdate.m in Sources */ = {isa = PBXBuildFile; fileRef = 7269E493264798200088C213 /* SPUSkippedUpdate.m */; }; 7269E4982648D3460088C213 /* SPUSkippedUpdate.m in Sources */ = {isa = PBXBuildFile; fileRef = 7269E493264798200088C213 /* SPUSkippedUpdate.m */; }; 7269E49A2648F7C00088C213 /* SPUUserUpdateState.m in Sources */ = {isa = PBXBuildFile; fileRef = 7269E4992648F7C00088C213 /* SPUUserUpdateState.m */; }; 726B20612CF4F1D300E6F7DB /* DevSignedAppVersion2.dmg in Resources */ = {isa = PBXBuildFile; fileRef = 726B20602CF4F1D300E6F7DB /* DevSignedAppVersion2.dmg */; }; 726BA5132E25F60000CE93C3 /* AppKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0867D6A5FE840307C02AAC07 /* AppKit.framework */; }; 726DF88E1C84277600188804 /* SPUUserUpdateState.h in Headers */ = {isa = PBXBuildFile; fileRef = 726DF88D1C84277500188804 /* SPUUserUpdateState.h */; settings = {ATTRIBUTES = (Public, ); }; }; 726E075C1CA3A6D6001A286B /* SPUSecureCoding.h in Headers */ = {isa = PBXBuildFile; fileRef = 726E075A1CA3A6D6001A286B /* SPUSecureCoding.h */; }; 726E075D1CA3A6D6001A286B /* SPUSecureCoding.m in Sources */ = {isa = PBXBuildFile; fileRef = 726E075B1CA3A6D6001A286B /* SPUSecureCoding.m */; }; 726E078D1CA891E9001A286B /* SPUUpdaterSettings.h in Headers */ = {isa = PBXBuildFile; fileRef = 726E078B1CA891E9001A286B /* SPUUpdaterSettings.h */; settings = {ATTRIBUTES = (Public, ); }; }; 726E078E1CA891E9001A286B /* SPUUpdaterSettings.m in Sources */ = {isa = PBXBuildFile; fileRef = 726E078C1CA891E9001A286B /* SPUUpdaterSettings.m */; }; 726E07B21CAF08D6001A286B /* SUInstallerLauncher.m in Sources */ = {isa = PBXBuildFile; fileRef = 726E07B11CAF08D6001A286B /* SUInstallerLauncher.m */; }; 726E07B41CAF08D6001A286B /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 726E07B31CAF08D6001A286B /* main.m */; }; 726E07BF1CAF0C6C001A286B /* SUConstants.m in Sources */ = {isa = PBXBuildFile; fileRef = 61299A5F09CA6EB100B7442F /* SUConstants.m */; }; 726E07C01CAF15B7001A286B /* SULog.m in Sources */ = {isa = PBXBuildFile; fileRef = 55C14F05136EF6DB00649790 /* SULog.m */; }; 726E4A1B1C86C88F00C57C6A /* TestAppHelper.m in Sources */ = {isa = PBXBuildFile; fileRef = 726E4A1A1C86C88F00C57C6A /* TestAppHelper.m */; }; 726E4A1D1C86C88F00C57C6A /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 726E4A1C1C86C88F00C57C6A /* main.m */; }; 726E4A211C86C88F00C57C6A /* TestAppHelper.xpc in Embed XPC Services */ = {isa = PBXBuildFile; fileRef = 726E4A161C86C88F00C57C6A /* TestAppHelper.xpc */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; 726E4A2B1C87D56200C57C6A /* SUTestWebServer.m in Sources */ = {isa = PBXBuildFile; fileRef = A5BF4F1C1BC7668B007A052A /* SUTestWebServer.m */; }; 726E4A301C87DC1700C57C6A /* Sparkle.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 8DC2EF5B0486A6940098B216 /* Sparkle.framework */; }; 726E4A371C89116000C57C6A /* SPUStandardUserDriverDelegate.h in Headers */ = {isa = PBXBuildFile; fileRef = 726E4A361C89116000C57C6A /* SPUStandardUserDriverDelegate.h */; settings = {ATTRIBUTES = (Public, ); }; }; 726F168626747CEB005BEA89 /* Sparkle.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 8DC2EF5B0486A6940098B216 /* Sparkle.framework */; }; 726F168726747CEB005BEA89 /* Sparkle.framework in Copy Sparkle */ = {isa = PBXBuildFile; fileRef = 8DC2EF5B0486A6940098B216 /* Sparkle.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 726F2CE51BC9C33D001971A4 /* SUOperatingSystem.h in Headers */ = {isa = PBXBuildFile; fileRef = 726F2CE31BC9C33D001971A4 /* SUOperatingSystem.h */; }; 726F2CE61BC9C33D001971A4 /* SUOperatingSystem.m in Sources */ = {isa = PBXBuildFile; fileRef = 726F2CE41BC9C33D001971A4 /* SUOperatingSystem.m */; }; 726F2CE81BC9C48F001971A4 /* SUConstants.m in Sources */ = {isa = PBXBuildFile; fileRef = 61299A5F09CA6EB100B7442F /* SUConstants.m */; }; 726F2CEB1BC9C733001971A4 /* SUConstants.m in Sources */ = {isa = PBXBuildFile; fileRef = 61299A5F09CA6EB100B7442F /* SUConstants.m */; }; 726FC0362C1E787A00177986 /* SparkleTestCodeSignApp.aar in Resources */ = {isa = PBXBuildFile; fileRef = 726FC0352C1E787A00177986 /* SparkleTestCodeSignApp.aar */; }; 726FC0382C1E96AA00177986 /* SparkleTestCodeSignApp.enc.aar in Resources */ = {isa = PBXBuildFile; fileRef = 726FC0372C1E96AA00177986 /* SparkleTestCodeSignApp.enc.aar */; }; 727DBAE526B5BBFD00111F0C /* ArgumentParser in Frameworks */ = {isa = PBXBuildFile; productRef = 727DBAE426B5BBFD00111F0C /* ArgumentParser */; }; 727DBAE726B5C47800111F0C /* ArgumentParser in Frameworks */ = {isa = PBXBuildFile; productRef = 727DBAE626B5C47800111F0C /* ArgumentParser */; }; 727DBAE926B5C48A00111F0C /* ArgumentParser in Frameworks */ = {isa = PBXBuildFile; productRef = 727DBAE826B5C48A00111F0C /* ArgumentParser */; }; 727F340B2605321D00020E85 /* SULog+NSError.m in Sources */ = {isa = PBXBuildFile; fileRef = 727F340A2605321D00020E85 /* SULog+NSError.m */; }; 727F340D2605321D00020E85 /* SULog+NSError.m in Sources */ = {isa = PBXBuildFile; fileRef = 727F340A2605321D00020E85 /* SULog+NSError.m */; }; 727F340E2605321D00020E85 /* SULog+NSError.m in Sources */ = {isa = PBXBuildFile; fileRef = 727F340A2605321D00020E85 /* SULog+NSError.m */; }; 728337A61C9E6FF40085AA99 /* SPUProbeInstallStatus.h in Headers */ = {isa = PBXBuildFile; fileRef = 728337A41C9E6FF40085AA99 /* SPUProbeInstallStatus.h */; }; 728337A71C9E6FF40085AA99 /* SPUProbeInstallStatus.m in Sources */ = {isa = PBXBuildFile; fileRef = 728337A51C9E6FF40085AA99 /* SPUProbeInstallStatus.m */; }; 728638ED1CAF50CE00783084 /* SUConstants.m in Sources */ = {isa = PBXBuildFile; fileRef = 61299A5F09CA6EB100B7442F /* SUConstants.m */; }; 7286EE5F28CEC84900163C1D /* SUTextViewReleaseNotesView.h in Headers */ = {isa = PBXBuildFile; fileRef = 7286EE5D28CEC84900163C1D /* SUTextViewReleaseNotesView.h */; }; 7286EE6028CEC84900163C1D /* SUTextViewReleaseNotesView.m in Sources */ = {isa = PBXBuildFile; fileRef = 7286EE5E28CEC84900163C1D /* SUTextViewReleaseNotesView.m */; }; 728ED34A277DA23400D9238F /* SPUSparkleDeltaArchive.m in Sources */ = {isa = PBXBuildFile; fileRef = 728ED349277DA23400D9238F /* SPUSparkleDeltaArchive.m */; }; 728ED34B277DA23400D9238F /* SPUSparkleDeltaArchive.m in Sources */ = {isa = PBXBuildFile; fileRef = 728ED349277DA23400D9238F /* SPUSparkleDeltaArchive.m */; }; 728ED34C277DA23400D9238F /* SPUSparkleDeltaArchive.m in Sources */ = {isa = PBXBuildFile; fileRef = 728ED349277DA23400D9238F /* SPUSparkleDeltaArchive.m */; }; 728ED34D277DA23400D9238F /* SPUSparkleDeltaArchive.m in Sources */ = {isa = PBXBuildFile; fileRef = 728ED349277DA23400D9238F /* SPUSparkleDeltaArchive.m */; }; 729051E41DC82AC0003DEA7F /* SUOperatingSystem.m in Sources */ = {isa = PBXBuildFile; fileRef = 726F2CE41BC9C33D001971A4 /* SUOperatingSystem.m */; }; 7290BF2F2E5A5AFA00D75022 /* SUCodeSigningVerifier.m in Sources */ = {isa = PBXBuildFile; fileRef = 7267E5991D3D8A5A00D1BF90 /* SUCodeSigningVerifier.m */; }; 7290BF302E5A5B0F00D75022 /* SUCodeSigningVerifier.m in Sources */ = {isa = PBXBuildFile; fileRef = 7267E5991D3D8A5A00D1BF90 /* SUCodeSigningVerifier.m */; }; 7290BF312E5A5B2F00D75022 /* Security.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 61B5F8F609C4CEB300B25A18 /* Security.framework */; }; 729743AB279D1BD2009910B2 /* SUCodeSigningVerifier.m in Sources */ = {isa = PBXBuildFile; fileRef = 7267E5991D3D8A5A00D1BF90 /* SUCodeSigningVerifier.m */; }; 729924941DF4A45000DBCDF5 /* SUUpdateValidator.m in Sources */ = {isa = PBXBuildFile; fileRef = 729924931DF4A45000DBCDF5 /* SUUpdateValidator.m */; }; 729C51282E58015600D7365A /* SUUpdatePermissionPrompt.xib in Resources */ = {isa = PBXBuildFile; fileRef = 729C51272E58015600D7365A /* SUUpdatePermissionPrompt.xib */; }; 729F7EAC27366353004592DC /* test-links.xml in Resources */ = {isa = PBXBuildFile; fileRef = 729F7EAB27366353004592DC /* test-links.xml */; }; 729F7EAF273F1840004592DC /* SPUUserAgent+Private.h in Headers */ = {isa = PBXBuildFile; fileRef = 729F7EAD273F1840004592DC /* SPUUserAgent+Private.h */; settings = {ATTRIBUTES = (Private, ); }; }; 729F7EB0273F1840004592DC /* SPUUserAgent+Private.m in Sources */ = {isa = PBXBuildFile; fileRef = 729F7EAE273F1840004592DC /* SPUUserAgent+Private.m */; }; 729F7ECE27409077004592DC /* SparkleTestCodeSignApp_bad_extraneous.zip in Resources */ = {isa = PBXBuildFile; fileRef = 729F7ECD27409076004592DC /* SparkleTestCodeSignApp_bad_extraneous.zip */; }; 72A1C2631CD6849C004CD282 /* SUUpdatePermissionPrompt.h in Headers */ = {isa = PBXBuildFile; fileRef = 612DCBAD0D488BC60015DBEA /* SUUpdatePermissionPrompt.h */; }; 72A450531C69A68900D67EEA /* SUUpdatePermissionResponse.h in Headers */ = {isa = PBXBuildFile; fileRef = 72A450511C69A68900D67EEA /* SUUpdatePermissionResponse.h */; settings = {ATTRIBUTES = (Public, ); }; }; 72A450541C69A68900D67EEA /* SUUpdatePermissionResponse.m in Sources */ = {isa = PBXBuildFile; fileRef = 72A450521C69A68900D67EEA /* SUUpdatePermissionResponse.m */; }; 72A4A2401BB6567D00E7820D /* SUFileManagerTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 72A4A23F1BB6567D00E7820D /* SUFileManagerTest.swift */; }; 72A6F98A1C94E2D6005F404C /* SUUpdaterDelegate.h in Headers */ = {isa = PBXBuildFile; fileRef = 72A6F9891C94E2D6005F404C /* SUUpdaterDelegate.h */; settings = {ATTRIBUTES = (Public, ); }; }; 72AC6B261B9AAC8800F62325 /* SparkleTestCodeSignApp.tar.gz in Resources */ = {isa = PBXBuildFile; fileRef = 72AC6B251B9AAC8800F62325 /* SparkleTestCodeSignApp.tar.gz */; }; 72AC6B281B9AAD6700F62325 /* SparkleTestCodeSignApp.tar in Resources */ = {isa = PBXBuildFile; fileRef = 72AC6B271B9AAD6700F62325 /* SparkleTestCodeSignApp.tar */; }; 72AC6B2A1B9AAF3A00F62325 /* SparkleTestCodeSignApp.tar.bz2 in Resources */ = {isa = PBXBuildFile; fileRef = 72AC6B291B9AAF3A00F62325 /* SparkleTestCodeSignApp.tar.bz2 */; }; 72AC6B2C1B9AB0EE00F62325 /* SparkleTestCodeSignApp.tar.xz in Resources */ = {isa = PBXBuildFile; fileRef = 72AC6B2B1B9AB0EE00F62325 /* SparkleTestCodeSignApp.tar.xz */; }; 72AC6B2E1B9B218C00F62325 /* SparkleTestCodeSignApp.dmg in Resources */ = {isa = PBXBuildFile; fileRef = 72AC6B2D1B9B218C00F62325 /* SparkleTestCodeSignApp.dmg */; }; 72AEB1D429A1A74E0033883E /* SPUStandardVersionDisplay.h in Headers */ = {isa = PBXBuildFile; fileRef = 72AEB1D229A1A74E0033883E /* SPUStandardVersionDisplay.h */; }; 72AEB1D529A1A74E0033883E /* SPUStandardVersionDisplay.m in Sources */ = {isa = PBXBuildFile; fileRef = 72AEB1D329A1A74E0033883E /* SPUStandardVersionDisplay.m */; }; 72AEB1D829A1CB510033883E /* SPUNoUpdateFoundInfo.h in Headers */ = {isa = PBXBuildFile; fileRef = 72AEB1D629A1CB510033883E /* SPUNoUpdateFoundInfo.h */; }; 72AEB1D929A1CB510033883E /* SPUNoUpdateFoundInfo.m in Sources */ = {isa = PBXBuildFile; fileRef = 72AEB1D729A1CB510033883E /* SPUNoUpdateFoundInfo.m */; }; 72B3DEC91E23472200457642 /* SPUDownloadedUpdate.h in Headers */ = {isa = PBXBuildFile; fileRef = 72B3DEC71E23472200457642 /* SPUDownloadedUpdate.h */; }; 72B3DECA1E23472200457642 /* SPUDownloadedUpdate.m in Sources */ = {isa = PBXBuildFile; fileRef = 72B3DEC81E23472200457642 /* SPUDownloadedUpdate.m */; }; 72B3DECD1E23479000457642 /* SPUInformationalUpdate.h in Headers */ = {isa = PBXBuildFile; fileRef = 72B3DECB1E23479000457642 /* SPUInformationalUpdate.h */; }; 72B3DECF1E23479000457642 /* SPUInformationalUpdate.m in Sources */ = {isa = PBXBuildFile; fileRef = 72B3DECC1E23479000457642 /* SPUInformationalUpdate.m */; }; 72B767CA1C9B707000A07552 /* SUAppcastDriver.h in Headers */ = {isa = PBXBuildFile; fileRef = 72B767C81C9B707000A07552 /* SUAppcastDriver.h */; }; 72B767CB1C9B707000A07552 /* SUAppcastDriver.m in Sources */ = {isa = PBXBuildFile; fileRef = 72B767C91C9B707000A07552 /* SUAppcastDriver.m */; }; 72B767CE1C9B924900A07552 /* SPUInstallerDriver.h in Headers */ = {isa = PBXBuildFile; fileRef = 72B767CC1C9B924900A07552 /* SPUInstallerDriver.h */; }; 72B767CF1C9B924900A07552 /* SPUInstallerDriver.m in Sources */ = {isa = PBXBuildFile; fileRef = 72B767CD1C9B924900A07552 /* SPUInstallerDriver.m */; }; 72B767D21C9C7B9300A07552 /* SPUProbingUpdateDriver.h in Headers */ = {isa = PBXBuildFile; fileRef = 72B767D01C9C7B9300A07552 /* SPUProbingUpdateDriver.h */; }; 72B767D31C9C7B9300A07552 /* SPUProbingUpdateDriver.m in Sources */ = {isa = PBXBuildFile; fileRef = 72B767D11C9C7B9300A07552 /* SPUProbingUpdateDriver.m */; }; 72B767D61C9C8B5C00A07552 /* SPUBasicUpdateDriver.h in Headers */ = {isa = PBXBuildFile; fileRef = 72B767D41C9C8B5C00A07552 /* SPUBasicUpdateDriver.h */; }; 72B767D71C9C8B5C00A07552 /* SPUBasicUpdateDriver.m in Sources */ = {isa = PBXBuildFile; fileRef = 72B767D51C9C8B5C00A07552 /* SPUBasicUpdateDriver.m */; }; 72B767DA1C9CD2E400A07552 /* SPUUIBasedUpdateDriver.h in Headers */ = {isa = PBXBuildFile; fileRef = 72B767D81C9CD2E400A07552 /* SPUUIBasedUpdateDriver.h */; }; 72B767DB1C9CD2E400A07552 /* SPUUIBasedUpdateDriver.m in Sources */ = {isa = PBXBuildFile; fileRef = 72B767D91C9CD2E400A07552 /* SPUUIBasedUpdateDriver.m */; }; 72B767DE1C9CDB9700A07552 /* SPUUserInitiatedUpdateDriver.h in Headers */ = {isa = PBXBuildFile; fileRef = 72B767DC1C9CDB9700A07552 /* SPUUserInitiatedUpdateDriver.h */; }; 72B767DF1C9CDB9700A07552 /* SPUUserInitiatedUpdateDriver.m in Sources */ = {isa = PBXBuildFile; fileRef = 72B767DD1C9CDB9700A07552 /* SPUUserInitiatedUpdateDriver.m */; }; 72B767E21C9CE90A00A07552 /* SPUCoreBasedUpdateDriver.h in Headers */ = {isa = PBXBuildFile; fileRef = 72B767E01C9CE90A00A07552 /* SPUCoreBasedUpdateDriver.h */; }; 72B767E31C9CE90A00A07552 /* SPUCoreBasedUpdateDriver.m in Sources */ = {isa = PBXBuildFile; fileRef = 72B767E11C9CE90A00A07552 /* SPUCoreBasedUpdateDriver.m */; }; 72B767E61C9CFD7200A07552 /* SPUAutomaticUpdateDriver.h in Headers */ = {isa = PBXBuildFile; fileRef = 72B767E41C9CFD7200A07552 /* SPUAutomaticUpdateDriver.h */; }; 72B767E71C9CFD7200A07552 /* SPUAutomaticUpdateDriver.m in Sources */ = {isa = PBXBuildFile; fileRef = 72B767E51C9CFD7200A07552 /* SPUAutomaticUpdateDriver.m */; }; 72BC6C3D275027BF0083F14B /* SparkleTestCodeSign_apfs.dmg in Resources */ = {isa = PBXBuildFile; fileRef = 72BC6C3C275027BF0083F14B /* SparkleTestCodeSign_apfs.dmg */; }; 72BEBFEF1D7287570019146B /* SUSpotlightImporterTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 72BEBFEE1D7287560019146B /* SUSpotlightImporterTest.swift */; }; 72C56E042EFB45B10005A484 /* SUSignatureVerifier.m in Sources */ = {isa = PBXBuildFile; fileRef = 7267E59B1D3D8A5A00D1BF90 /* SUSignatureVerifier.m */; }; 72C56E052EFB45BD0005A484 /* SPUVerifierInformation.m in Sources */ = {isa = PBXBuildFile; fileRef = 72D04F3C2B094C8400A6DEAA /* SPUVerifierInformation.m */; }; 72C56E062EFB45C80005A484 /* libed25519.a in Frameworks */ = {isa = PBXBuildFile; fileRef = EA1E282D22B660BE004AA304 /* libed25519.a */; }; 72C56E092EFDFB850005A484 /* SPUExtractSignedFeed.m in Sources */ = {isa = PBXBuildFile; fileRef = 72C56E082EFDFB850005A484 /* SPUExtractSignedFeed.m */; }; 72C56E0A2EFDFB850005A484 /* SPUExtractSignedFeed.m in Sources */ = {isa = PBXBuildFile; fileRef = 72C56E082EFDFB850005A484 /* SPUExtractSignedFeed.m */; }; 72C56E0B2EFDFB850005A484 /* SPUExtractSignedFeed.m in Sources */ = {isa = PBXBuildFile; fileRef = 72C56E082EFDFB850005A484 /* SPUExtractSignedFeed.m */; }; 72C56E0C2EFDFB850005A484 /* SPUExtractSignedFeed.h in Headers */ = {isa = PBXBuildFile; fileRef = 72C56E072EFDFB850005A484 /* SPUExtractSignedFeed.h */; }; 72C56E0D2EFE002D0005A484 /* SPUExtractSignedFeed.m in Sources */ = {isa = PBXBuildFile; fileRef = 72C56E082EFDFB850005A484 /* SPUExtractSignedFeed.m */; }; 72C56E0E2EFEEF8B0005A484 /* SPUExtractSignedFeed.m in Sources */ = {isa = PBXBuildFile; fileRef = 72C56E082EFDFB850005A484 /* SPUExtractSignedFeed.m */; }; 72C56E102EFEF10B0005A484 /* Signing.swift in Sources */ = {isa = PBXBuildFile; fileRef = 72C56E0F2EFEF10B0005A484 /* Signing.swift */; }; 72C56E112EFEF10B0005A484 /* Signing.swift in Sources */ = {isa = PBXBuildFile; fileRef = 72C56E0F2EFEF10B0005A484 /* Signing.swift */; }; 72C56E122EFEF10B0005A484 /* Signing.swift in Sources */ = {isa = PBXBuildFile; fileRef = 72C56E0F2EFEF10B0005A484 /* Signing.swift */; }; 72C56E192F04343D0005A484 /* SPUAppcastSigningValidationStatus.h in Headers */ = {isa = PBXBuildFile; fileRef = 72C56E182F04343D0005A484 /* SPUAppcastSigningValidationStatus.h */; settings = {ATTRIBUTES = (Public, ); }; }; 72C56E1F2F04AC890005A484 /* SUFeedSignatureVerifierTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 72C56E1E2F04AC890005A484 /* SUFeedSignatureVerifierTest.swift */; }; 72C56E202F04B3760005A484 /* Signing.swift in Sources */ = {isa = PBXBuildFile; fileRef = 72C56E0F2EFEF10B0005A484 /* Signing.swift */; }; 72C56E262F04C28B0005A484 /* testreleasenotes.html in Resources */ = {isa = PBXBuildFile; fileRef = 72C56E212F04C28B0005A484 /* testreleasenotes.html */; }; 72CCDEBE27421FD500B53718 /* SparkleTestCodeSignApp_bad_header.zip in Resources */ = {isa = PBXBuildFile; fileRef = 72CCDEBD27421FD500B53718 /* SparkleTestCodeSignApp_bad_header.zip */; }; 72D04F3D2B094C8400A6DEAA /* SPUVerifierInformation.m in Sources */ = {isa = PBXBuildFile; fileRef = 72D04F3C2B094C8400A6DEAA /* SPUVerifierInformation.m */; }; 72D04F3E2B097D4300A6DEAA /* SPUVerifierInformation.m in Sources */ = {isa = PBXBuildFile; fileRef = 72D04F3C2B094C8400A6DEAA /* SPUVerifierInformation.m */; }; 72D60CD928C2BAE900189AB8 /* SPUStandardUserDriver+Private.h in Headers */ = {isa = PBXBuildFile; fileRef = 72D60CD828C2BA2100189AB8 /* SPUStandardUserDriver+Private.h */; settings = {ATTRIBUTES = (Private, ); }; }; 72D954811CBACC35006F28BD /* InstallerProgressAppController.m in Sources */ = {isa = PBXBuildFile; fileRef = 72D954801CBACC35006F28BD /* InstallerProgressAppController.m */; }; 72D954831CBAD34F006F28BD /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 72D954821CBAD34F006F28BD /* main.m */; }; 72D954A21CBB415C006F28BD /* SPUCommandLineDriver.m in Sources */ = {isa = PBXBuildFile; fileRef = 72D954A11CBB415C006F28BD /* SPUCommandLineDriver.m */; }; 72D954A51CBB415C006F28BD /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 72D954A41CBB415C006F28BD /* main.m */; }; 72D954B81CBB467F006F28BD /* SPUCommandLineUserDriver.m in Sources */ = {isa = PBXBuildFile; fileRef = 72D954B71CBB467F006F28BD /* SPUCommandLineUserDriver.m */; }; 72DBA37D1D60CC34002594A8 /* SPUUpdatePermissionRequest.h in Headers */ = {isa = PBXBuildFile; fileRef = 72DBA37B1D60CC34002594A8 /* SPUUpdatePermissionRequest.h */; settings = {ATTRIBUTES = (Public, ); }; }; 72DBA37E1D60CC34002594A8 /* SPUUpdatePermissionRequest.m in Sources */ = {isa = PBXBuildFile; fileRef = 72DBA37C1D60CC34002594A8 /* SPUUpdatePermissionRequest.m */; }; 72DBA37F1D62C23E002594A8 /* SUCodeSigningVerifier.m in Sources */ = {isa = PBXBuildFile; fileRef = 7267E5991D3D8A5A00D1BF90 /* SUCodeSigningVerifier.m */; }; 72E45CF31B640CDD005C701A /* SUTestApplicationDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 72E45CF21B640CDD005C701A /* SUTestApplicationDelegate.m */; }; 72E45CF71B640DAE005C701A /* SUUpdateSettingsWindowController.m in Sources */ = {isa = PBXBuildFile; fileRef = 72E45CF51B640DAE005C701A /* SUUpdateSettingsWindowController.m */; }; 72E45CF81B640DAE005C701A /* SUUpdateSettingsWindowController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 72E45CF61B640DAE005C701A /* SUUpdateSettingsWindowController.xib */; }; 72E45CFC1B641961005C701A /* sparkletestcast.xml in Resources */ = {isa = PBXBuildFile; fileRef = 72E45CFB1B641961005C701A /* sparkletestcast.xml */; }; 72E539121D68C3FA0092CE5E /* SPUDownloadData.m in Sources */ = {isa = PBXBuildFile; fileRef = 72F9EC431D5E9ED8004AC8B6 /* SPUDownloadData.m */; }; 72E6D9712C04DE1A005496E4 /* SparkleTestCodeSign_pkg.dmg in Resources */ = {isa = PBXBuildFile; fileRef = 72E6D9702C04DE19005496E4 /* SparkleTestCodeSign_pkg.dmg */; }; 72E6D9732C0526DC005496E4 /* SparkleTestCodeSignApp.enc.nolicense.dmg in Resources */ = {isa = PBXBuildFile; fileRef = 72E6D9722C0526DC005496E4 /* SparkleTestCodeSignApp.enc.nolicense.dmg */; }; 72EB735F29BE981300FBCEE7 /* DevSignedApp.zip in Resources */ = {isa = PBXBuildFile; fileRef = 72EB735E29BE981300FBCEE7 /* DevSignedApp.zip */; }; 72EB736129BEB36100FBCEE7 /* DevSignedAppVersion2.zip in Resources */ = {isa = PBXBuildFile; fileRef = 72EB736029BEB36100FBCEE7 /* DevSignedAppVersion2.zip */; }; 72EB87EA1CB8798800C37F42 /* ShowInstallerProgress.m in Sources */ = {isa = PBXBuildFile; fileRef = 72EB87E91CB8798800C37F42 /* ShowInstallerProgress.m */; }; 72EB87EB1CB8859100C37F42 /* Updater.app in Copy Tools */ = {isa = PBXBuildFile; fileRef = 721C24451CB753E6005440CB /* Updater.app */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; 72EB87EC1CB8887E00C37F42 /* SUStatus.xib in Resources */ = {isa = PBXBuildFile; fileRef = 55C14BD8136EF00C00649790 /* SUStatus.xib */; }; 72EE17FB26D1CC8800C58B19 /* SUInstallerLauncher+Private.h in Headers */ = {isa = PBXBuildFile; fileRef = 72EE17FA26D1CBC000C58B19 /* SUInstallerLauncher+Private.h */; settings = {ATTRIBUTES = (Private, ); }; }; 72EE17FC26D1CC9D00C58B19 /* SPUInstallationType.h in Headers */ = {isa = PBXBuildFile; fileRef = 7214B8851D45AD9A00CB5CED /* SPUInstallationType.h */; settings = {ATTRIBUTES = (Private, ); }; }; 72EE181926DAE70900C58B19 /* SPUUpdateCheck.h in Headers */ = {isa = PBXBuildFile; fileRef = 72EE181826DAE6E100C58B19 /* SPUUpdateCheck.h */; settings = {ATTRIBUTES = (Public, ); }; }; 72EF30BE2675CF38008CE987 /* SPUAppcastItemStateResolver.h in Headers */ = {isa = PBXBuildFile; fileRef = 72EF30BA2675CF38008CE987 /* SPUAppcastItemStateResolver.h */; settings = {ATTRIBUTES = (Private, ); }; }; 72EF30BF2675CF38008CE987 /* SPUAppcastItemState.m in Sources */ = {isa = PBXBuildFile; fileRef = 72EF30BB2675CF38008CE987 /* SPUAppcastItemState.m */; }; 72EF30C02675CF38008CE987 /* SPUAppcastItemStateResolver.m in Sources */ = {isa = PBXBuildFile; fileRef = 72EF30BC2675CF38008CE987 /* SPUAppcastItemStateResolver.m */; }; 72EF30C12675CF38008CE987 /* SPUAppcastItemState.h in Headers */ = {isa = PBXBuildFile; fileRef = 72EF30BD2675CF38008CE987 /* SPUAppcastItemState.h */; }; 72EF30C22675CF67008CE987 /* SPUAppcastItemState.m in Sources */ = {isa = PBXBuildFile; fileRef = 72EF30BB2675CF38008CE987 /* SPUAppcastItemState.m */; }; 72EF30C32675CF67008CE987 /* SPUAppcastItemStateResolver.m in Sources */ = {isa = PBXBuildFile; fileRef = 72EF30BC2675CF38008CE987 /* SPUAppcastItemStateResolver.m */; }; 72EF30C42675CFA1008CE987 /* SPUAppcastItemState.m in Sources */ = {isa = PBXBuildFile; fileRef = 72EF30BB2675CF38008CE987 /* SPUAppcastItemState.m */; }; 72EF30C52675CFA1008CE987 /* SPUAppcastItemStateResolver.m in Sources */ = {isa = PBXBuildFile; fileRef = 72EF30BC2675CF38008CE987 /* SPUAppcastItemStateResolver.m */; }; 72EF30C7267C716A008CE987 /* SUAppcastItem+Private.h in Headers */ = {isa = PBXBuildFile; fileRef = 72EF30C6267C716A008CE987 /* SUAppcastItem+Private.h */; settings = {ATTRIBUTES = (Private, ); }; }; 72F0EC45278A55CA002A876A /* screenshot.png in Resources */ = {isa = PBXBuildFile; fileRef = 72F0EC44278A55CA002A876A /* screenshot.png */; }; 72F0EC46278A5B87002A876A /* SUBinaryDeltaCommon.m in Sources */ = {isa = PBXBuildFile; fileRef = 7267E5711D3D895B00D1BF90 /* SUBinaryDeltaCommon.m */; }; 72F0EC47278A5B87002A876A /* SUBinaryDeltaCreate.m in Sources */ = {isa = PBXBuildFile; fileRef = 7267E5731D3D895B00D1BF90 /* SUBinaryDeltaCreate.m */; }; 72F0EC48278A5B95002A876A /* SPUDeltaArchive.m in Sources */ = {isa = PBXBuildFile; fileRef = 725EE485277D375F00D820CE /* SPUDeltaArchive.m */; }; 72F0EC49278A5B95002A876A /* SPUSparkleDeltaArchive.m in Sources */ = {isa = PBXBuildFile; fileRef = 728ED349277DA23400D9238F /* SPUSparkleDeltaArchive.m */; }; 72F0EC4A278A5B95002A876A /* SPUXarDeltaArchive.m in Sources */ = {isa = PBXBuildFile; fileRef = 725EE47F277BF13B00D820CE /* SPUXarDeltaArchive.m */; }; 72F0EC4B278A5BB2002A876A /* libxar.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = 726E07681CA616A4001A286B /* libxar.tbd */; }; 72F0EC4C278A5BB9002A876A /* libbz2.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = 726E076A1CA616B3001A286B /* libbz2.tbd */; }; 72F0EC4D278A5BCA002A876A /* libbsdiff.a in Frameworks */ = {isa = PBXBuildFile; fileRef = EA1E280F22B64522004AA304 /* libbsdiff.a */; }; 72F94F581CC44DE1002DEE68 /* SPUXPCServiceInfo.h in Headers */ = {isa = PBXBuildFile; fileRef = 72F94F561CC44DE1002DEE68 /* SPUXPCServiceInfo.h */; }; 72F94F591CC44DE1002DEE68 /* SPUXPCServiceInfo.m in Sources */ = {isa = PBXBuildFile; fileRef = 72F94F571CC44DE1002DEE68 /* SPUXPCServiceInfo.m */; }; 72F94F5A1CC450DE002DEE68 /* SUInstallerLauncher.m in Sources */ = {isa = PBXBuildFile; fileRef = 726E07B11CAF08D6001A286B /* SUInstallerLauncher.m */; }; 72F9EBE21D517E2F004AC8B6 /* SUUpdater.h in Headers */ = {isa = PBXBuildFile; fileRef = 72F9EBE01D517E2F004AC8B6 /* SUUpdater.h */; settings = {ATTRIBUTES = (Public, ); }; }; 72F9EBE31D517E2F004AC8B6 /* SUUpdater.m in Sources */ = {isa = PBXBuildFile; fileRef = 72F9EBE11D517E2F004AC8B6 /* SUUpdater.m */; }; 72F9EC3F1D5E823F004AC8B6 /* SPUUpdaterTimer.h in Headers */ = {isa = PBXBuildFile; fileRef = 72F9EC3D1D5E823F004AC8B6 /* SPUUpdaterTimer.h */; }; 72F9EC401D5E823F004AC8B6 /* SPUUpdaterTimer.m in Sources */ = {isa = PBXBuildFile; fileRef = 72F9EC3E1D5E823F004AC8B6 /* SPUUpdaterTimer.m */; }; 72F9EC441D5E9ED8004AC8B6 /* SPUDownloadData.h in Headers */ = {isa = PBXBuildFile; fileRef = 72F9EC421D5E9ED8004AC8B6 /* SPUDownloadData.h */; settings = {ATTRIBUTES = (Public, ); }; }; 72F9EC451D5E9ED8004AC8B6 /* SPUDownloadData.m in Sources */ = {isa = PBXBuildFile; fileRef = 72F9EC431D5E9ED8004AC8B6 /* SPUDownloadData.m */; }; 72F9EC481D5EA904004AC8B6 /* SPUUpdaterDelegate.h in Headers */ = {isa = PBXBuildFile; fileRef = 72F9EC471D5EA7D3004AC8B6 /* SPUUpdaterDelegate.h */; settings = {ATTRIBUTES = (Public, ); }; }; 72FE54422E56A14100227A91 /* SUUpdateAlert.xib in Resources */ = {isa = PBXBuildFile; fileRef = 72FE54412E56A14100227A91 /* SUUpdateAlert.xib */; }; C23E885B1BE7B24F0050BB73 /* SparkleTestCodeSignApp.enc.dmg in Resources */ = {isa = PBXBuildFile; fileRef = C23E88591BE7AF890050BB73 /* SparkleTestCodeSignApp.enc.dmg */; }; EA1E281722B645AE004AA304 /* libbsdiff.a in Frameworks */ = {isa = PBXBuildFile; fileRef = EA1E280F22B64522004AA304 /* libbsdiff.a */; }; EA1E281822B645CE004AA304 /* libbsdiff.a in Frameworks */ = {isa = PBXBuildFile; fileRef = EA1E280F22B64522004AA304 /* libbsdiff.a */; }; EA1E281922B645D9004AA304 /* libbsdiff.a in Frameworks */ = {isa = PBXBuildFile; fileRef = EA1E280F22B64522004AA304 /* libbsdiff.a */; }; EA1E281A22B645F4004AA304 /* libbsdiff.a in Frameworks */ = {isa = PBXBuildFile; fileRef = EA1E280F22B64522004AA304 /* libbsdiff.a */; }; EA1E282122B64677004AA304 /* bscommon.c in Sources */ = {isa = PBXBuildFile; fileRef = 72B09CE91CEA18900052EF9E /* bscommon.c */; }; EA1E282222B64677004AA304 /* bsdiff.c in Sources */ = {isa = PBXBuildFile; fileRef = 5D06E8DB0FD68CB9005AE3F6 /* bsdiff.c */; }; EA1E282322B64677004AA304 /* bspatch.c in Sources */ = {isa = PBXBuildFile; fileRef = 5D06E8DC0FD68CB9005AE3F6 /* bspatch.c */; }; EA1E282422B64677004AA304 /* sais.c in Sources */ = {isa = PBXBuildFile; fileRef = 7223E7611AD1AEFF008E3161 /* sais.c */; }; EA1E282622B64693004AA304 /* bscommon.h in Copy Headers */ = {isa = PBXBuildFile; fileRef = 72B09CEA1CEA18900052EF9E /* bscommon.h */; }; EA1E282722B64694004AA304 /* bspatch.h in Copy Headers */ = {isa = PBXBuildFile; fileRef = 611142E810FB1BE5009810AA /* bspatch.h */; }; EA1E282822B64694004AA304 /* sais.h in Copy Headers */ = {isa = PBXBuildFile; fileRef = 7223E7621AD1AEFF008E3161 /* sais.h */; }; EA1E284522B660ED004AA304 /* seed.c in Sources */ = {isa = PBXBuildFile; fileRef = EA1E283522B660ED004AA304 /* seed.c */; }; EA1E284622B660ED004AA304 /* fe.c in Sources */ = {isa = PBXBuildFile; fileRef = EA1E283622B660ED004AA304 /* fe.c */; }; EA1E284722B660ED004AA304 /* verify.c in Sources */ = {isa = PBXBuildFile; fileRef = EA1E283722B660ED004AA304 /* verify.c */; }; EA1E284822B660ED004AA304 /* ge.c in Sources */ = {isa = PBXBuildFile; fileRef = EA1E283822B660ED004AA304 /* ge.c */; }; EA1E284922B660ED004AA304 /* sc.c in Sources */ = {isa = PBXBuildFile; fileRef = EA1E283922B660ED004AA304 /* sc.c */; }; EA1E284A22B660ED004AA304 /* sign.c in Sources */ = {isa = PBXBuildFile; fileRef = EA1E283A22B660ED004AA304 /* sign.c */; }; EA1E284B22B660ED004AA304 /* key_exchange.c in Sources */ = {isa = PBXBuildFile; fileRef = EA1E283C22B660ED004AA304 /* key_exchange.c */; }; EA1E284C22B660ED004AA304 /* add_scalar.c in Sources */ = {isa = PBXBuildFile; fileRef = EA1E283D22B660ED004AA304 /* add_scalar.c */; }; EA1E284D22B660ED004AA304 /* keypair.c in Sources */ = {isa = PBXBuildFile; fileRef = EA1E284022B660ED004AA304 /* keypair.c */; }; EA1E284E22B660ED004AA304 /* sha512.c in Sources */ = {isa = PBXBuildFile; fileRef = EA1E284222B660ED004AA304 /* sha512.c */; }; EA1E284F22B66115004AA304 /* precomp_data.h in Copy Headers */ = {isa = PBXBuildFile; fileRef = EA1E283422B660ED004AA304 /* precomp_data.h */; }; EA1E285022B66115004AA304 /* sha512.h in Copy Headers */ = {isa = PBXBuildFile; fileRef = EA1E283B22B660ED004AA304 /* sha512.h */; }; EA1E285122B66115004AA304 /* ed25519.h in Copy Headers */ = {isa = PBXBuildFile; fileRef = EA1E283E22B660ED004AA304 /* ed25519.h */; }; EA1E285222B66115004AA304 /* fe.h in Copy Headers */ = {isa = PBXBuildFile; fileRef = EA1E283F22B660ED004AA304 /* fe.h */; }; EA1E285322B66115004AA304 /* fixedint.h in Copy Headers */ = {isa = PBXBuildFile; fileRef = EA1E284122B660ED004AA304 /* fixedint.h */; }; EA1E285422B66115004AA304 /* sc.h in Copy Headers */ = {isa = PBXBuildFile; fileRef = EA1E284322B660ED004AA304 /* sc.h */; }; EA1E285522B66115004AA304 /* ge.h in Copy Headers */ = {isa = PBXBuildFile; fileRef = EA1E284422B660ED004AA304 /* ge.h */; }; EA1E285622B6619B004AA304 /* libed25519.a in Frameworks */ = {isa = PBXBuildFile; fileRef = EA1E282D22B660BE004AA304 /* libed25519.a */; }; EA1E286122B66487004AA304 /* main.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA1E286022B66487004AA304 /* main.swift */; }; EA1E286A22B6653D004AA304 /* libed25519.a in Frameworks */ = {isa = PBXBuildFile; fileRef = EA1E282D22B660BE004AA304 /* libed25519.a */; }; EA1E286E22B665F0004AA304 /* SUSignatures.h in Headers */ = {isa = PBXBuildFile; fileRef = EA1E286C22B665E8004AA304 /* SUSignatures.h */; }; EA1E287022B66621004AA304 /* SUSignatures.m in Sources */ = {isa = PBXBuildFile; fileRef = EA1E286D22B665E8004AA304 /* SUSignatures.m */; }; EA1E287922B666EB004AA304 /* main.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA1E287822B666EB004AA304 /* main.swift */; }; EA1E287E22B66705004AA304 /* libed25519.a in Frameworks */ = {isa = PBXBuildFile; fileRef = EA1E282D22B660BE004AA304 /* libed25519.a */; }; F8761EB11ADC5068000C9034 /* SUCodeSigningVerifierTest.m in Sources */ = {isa = PBXBuildFile; fileRef = F8761EB01ADC5068000C9034 /* SUCodeSigningVerifierTest.m */; }; F8761EB31ADC50EB000C9034 /* SparkleTestCodeSignApp.zip in Resources */ = {isa = PBXBuildFile; fileRef = F8761EB21ADC50EB000C9034 /* SparkleTestCodeSignApp.zip */; }; FA30773D24CBC295007BA37D /* testlocalizedreleasenotesappcast.xml in Resources */ = {isa = PBXBuildFile; fileRef = FA30773C24CBC295007BA37D /* testlocalizedreleasenotesappcast.xml */; }; FA30773F24CBC3E9007BA37D /* URL+Hashing.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA30773E24CBC3E9007BA37D /* URL+Hashing.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ 1454BA1519637EDB00344E57 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 0867D690FE84028FC02AAC07 /* Project object */; proxyType = 1; remoteGlobalIDString = 61B5F90109C4CEE200B25A18; remoteInfo = "Sparkle Test App"; }; 14732BCA1960F73500593899 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 0867D690FE84028FC02AAC07 /* Project object */; proxyType = 1; remoteGlobalIDString = 8DC2EF4F0486A6940098B216; remoteInfo = Sparkle; }; 14732BCE1960F73500593899 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 0867D690FE84028FC02AAC07 /* Project object */; proxyType = 1; remoteGlobalIDString = 5D06E8CF0FD68C7C005AE3F6; remoteInfo = BinaryDelta; }; 14950063195FB8A600BC5B5B /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 0867D690FE84028FC02AAC07 /* Project object */; proxyType = 1; remoteGlobalIDString = 8DC2EF4F0486A6940098B216; remoteInfo = Sparkle; }; 14950065195FB8A600BC5B5B /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 0867D690FE84028FC02AAC07 /* Project object */; proxyType = 1; remoteGlobalIDString = 61B5F90109C4CEE200B25A18; remoteInfo = "Sparkle Test App"; }; 14950067195FB8A600BC5B5B /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 0867D690FE84028FC02AAC07 /* Project object */; proxyType = 1; remoteGlobalIDString = 612279D80DB5470200AB99EA; remoteInfo = "Sparkle Unit Tests"; }; 14950069195FB8A600BC5B5B /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 0867D690FE84028FC02AAC07 /* Project object */; proxyType = 1; remoteGlobalIDString = 5D06E8CF0FD68C7C005AE3F6; remoteInfo = BinaryDelta; }; 376769AE23442F320077B8F7 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 0867D690FE84028FC02AAC07 /* Project object */; proxyType = 1; remoteGlobalIDString = EA1E280E22B64522004AA304; remoteInfo = bsdiff; }; 376769B023442F490077B8F7 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 0867D690FE84028FC02AAC07 /* Project object */; proxyType = 1; remoteGlobalIDString = EA1E280E22B64522004AA304; remoteInfo = bsdiff; }; 37DC0CE52340667000501A67 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 0867D690FE84028FC02AAC07 /* Project object */; proxyType = 1; remoteGlobalIDString = EA1E280E22B64522004AA304; remoteInfo = bsdiff; }; 5A06357123FE332300478A72 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 0867D690FE84028FC02AAC07 /* Project object */; proxyType = 1; remoteGlobalIDString = EA1E282C22B660BE004AA304; remoteInfo = ed25519; }; 61B5F91B09C4CF7200B25A18 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 0867D690FE84028FC02AAC07 /* Project object */; proxyType = 1; remoteGlobalIDString = 8DC2EF4F0486A6940098B216; remoteInfo = Sparkle; }; 61FA528C0E2D9EB200EF58AD /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 0867D690FE84028FC02AAC07 /* Project object */; proxyType = 1; remoteGlobalIDString = 8DC2EF4F0486A6940098B216; remoteInfo = Sparkle; }; 72045CD726FEE471004F96E5 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 0867D690FE84028FC02AAC07 /* Project object */; proxyType = 1; remoteGlobalIDString = 726E07EE1CAF37BD001A286B; remoteInfo = SparkleDownloader; }; 72045CD926FEE471004F96E5 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 0867D690FE84028FC02AAC07 /* Project object */; proxyType = 1; remoteGlobalIDString = 724BB36B1D31D0B7005D534A; remoteInfo = SparkleInstallerConnection; }; 72045CDB26FEE471004F96E5 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 0867D690FE84028FC02AAC07 /* Project object */; proxyType = 1; remoteGlobalIDString = 726E07AC1CAF08D6001A286B; remoteInfo = SparkleInstallerLauncher; }; 72045CDD26FEE471004F96E5 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 0867D690FE84028FC02AAC07 /* Project object */; proxyType = 1; remoteGlobalIDString = 724BB3921D333832005D534A; remoteInfo = SparkleInstallerStatus; }; 7205C4681E1306FB00E370AE /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 0867D690FE84028FC02AAC07 /* Project object */; proxyType = 1; remoteGlobalIDString = 7205C43D1E13049400E370AE; remoteInfo = generate_appcast; }; 7205C46A1E13070300E370AE /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 0867D690FE84028FC02AAC07 /* Project object */; proxyType = 1; remoteGlobalIDString = 7205C43D1E13049400E370AE; remoteInfo = generate_appcast; }; 7218EC322623ED94008FECF3 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 0867D690FE84028FC02AAC07 /* Project object */; proxyType = 1; remoteGlobalIDString = 724BB36B1D31D0B7005D534A; remoteInfo = SparkleInstallerConnection; }; 7218EC342623ED97008FECF3 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 0867D690FE84028FC02AAC07 /* Project object */; proxyType = 1; remoteGlobalIDString = 724BB3921D333832005D534A; remoteInfo = SparkleInstallerStatus; }; 725148E8266D918900247C9C /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 0867D690FE84028FC02AAC07 /* Project object */; proxyType = 1; remoteGlobalIDString = 726E07EE1CAF37BD001A286B; remoteInfo = SparkleDownloader; }; 726B2B621C645FC900388755 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 0867D690FE84028FC02AAC07 /* Project object */; proxyType = 1; remoteGlobalIDString = 61B5F90109C4CEE200B25A18; remoteInfo = "Sparkle Test App"; }; 726E4A1F1C86C88F00C57C6A /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 0867D690FE84028FC02AAC07 /* Project object */; proxyType = 1; remoteGlobalIDString = 726E4A151C86C88F00C57C6A; remoteInfo = TestAppHelper; }; 726F168826747CEB005BEA89 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 0867D690FE84028FC02AAC07 /* Project object */; proxyType = 1; remoteGlobalIDString = 8DC2EF4F0486A6940098B216; remoteInfo = Sparkle; }; 72A5D5AB1D6929260009E5AC /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 0867D690FE84028FC02AAC07 /* Project object */; proxyType = 1; remoteGlobalIDString = 72B398D11D3D879300EE297F; remoteInfo = Autoupdate; }; 72CCCC14287B3D1900E7156B /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 0867D690FE84028FC02AAC07 /* Project object */; proxyType = 1; remoteGlobalIDString = 8DC2EF4F0486A6940098B216; remoteInfo = Sparkle; }; 72CCCC16287B3D3400E7156B /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 0867D690FE84028FC02AAC07 /* Project object */; proxyType = 1; remoteGlobalIDString = EA1E280E22B64522004AA304; remoteInfo = bsdiff; }; 72CCCC18287B3D3D00E7156B /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 0867D690FE84028FC02AAC07 /* Project object */; proxyType = 1; remoteGlobalIDString = EA1E282C22B660BE004AA304; remoteInfo = ed25519; }; 72CCCC1A287B3D9000E7156B /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 0867D690FE84028FC02AAC07 /* Project object */; proxyType = 1; remoteGlobalIDString = EA1E282C22B660BE004AA304; remoteInfo = ed25519; }; 72CCCC1C287B3D9600E7156B /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 0867D690FE84028FC02AAC07 /* Project object */; proxyType = 1; remoteGlobalIDString = EA1E282C22B660BE004AA304; remoteInfo = ed25519; }; 72CCCC1E287B3DA100E7156B /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 0867D690FE84028FC02AAC07 /* Project object */; proxyType = 1; remoteGlobalIDString = EA1E282C22B660BE004AA304; remoteInfo = ed25519; }; 72CCCC20287B3DA400E7156B /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 0867D690FE84028FC02AAC07 /* Project object */; proxyType = 1; remoteGlobalIDString = EA1E280E22B64522004AA304; remoteInfo = bsdiff; }; 72D954B91CBB6E27006F28BD /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 0867D690FE84028FC02AAC07 /* Project object */; proxyType = 1; remoteGlobalIDString = 721C24441CB753E6005440CB; remoteInfo = "Installer Progress"; }; 72EF30B826747E39008CE987 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 0867D690FE84028FC02AAC07 /* Project object */; proxyType = 1; remoteGlobalIDString = 8DC2EF4F0486A6940098B216; remoteInfo = Sparkle; }; 72F94F6A1CC4A0C8002DEE68 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 0867D690FE84028FC02AAC07 /* Project object */; proxyType = 1; remoteGlobalIDString = 726E07AC1CAF08D6001A286B; remoteInfo = SparkleInstallerLauncher; }; 895C5DC624D78F700058A82D /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 0867D690FE84028FC02AAC07 /* Project object */; proxyType = 1; remoteGlobalIDString = 895C5DC024D78E210058A82D; remoteInfo = XCFrameworks; }; FA344BA424C8699E00B2A401 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 0867D690FE84028FC02AAC07 /* Project object */; proxyType = 1; remoteGlobalIDString = EA1E287522B666EB004AA304; remoteInfo = sign_update; }; FA344BA624C869A300B2A401 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 0867D690FE84028FC02AAC07 /* Project object */; proxyType = 1; remoteGlobalIDString = EA1E285D22B66487004AA304; remoteInfo = generate_keys; }; /* End PBXContainerItemProxy section */ /* Begin PBXCopyFilesBuildPhase section */ 142E0E0119A6A13300E4312B /* Copy Files */ = { isa = PBXCopyFilesBuildPhase; buildActionMask = 2147483647; dstPath = ""; dstSubfolderSpec = 10; files = ( 142E0E0219A6A14700E4312B /* Sparkle.framework in Copy Files */, ); name = "Copy Files"; runOnlyForDeploymentPostprocessing = 0; }; 142E0E0319A6A24100E4312B /* Copy Tools */ = { isa = PBXCopyFilesBuildPhase; buildActionMask = 2147483647; dstPath = ""; dstSubfolderSpec = 6; files = ( 7267E5F91D3D92DA00D1BF90 /* Autoupdate in Copy Tools */, 72EB87EB1CB8859100C37F42 /* Updater.app in Copy Tools */, ); name = "Copy Tools"; runOnlyForDeploymentPostprocessing = 0; }; 61B5FB4D09C4E9FA00B25A18 /* Copy Frameworks */ = { isa = PBXCopyFilesBuildPhase; buildActionMask = 2147483647; dstPath = ""; dstSubfolderSpec = 10; files = ( 142E0E0019A6954400E4312B /* Sparkle.framework in Copy Frameworks */, ); name = "Copy Frameworks"; runOnlyForDeploymentPostprocessing = 0; }; 72045CDF26FEE51D004F96E5 /* Copy XPC Services */ = { isa = PBXCopyFilesBuildPhase; buildActionMask = 2147483647; dstPath = "$(CONTENTS_FOLDER_PATH)/XPCServices"; dstSubfolderSpec = 16; files = ( 72045CE026FEE535004F96E5 /* Downloader.xpc in Copy XPC Services */, 72045CE126FEE535004F96E5 /* InstallerConnection.xpc in Copy XPC Services */, 72045CE226FEE535004F96E5 /* Installer.xpc in Copy XPC Services */, 72045CE326FEE535004F96E5 /* InstallerStatus.xpc in Copy XPC Services */, ); name = "Copy XPC Services"; runOnlyForDeploymentPostprocessing = 0; }; 7205C43C1E13049400E370AE /* CopyFiles */ = { isa = PBXCopyFilesBuildPhase; buildActionMask = 2147483647; dstPath = /usr/share/man/man1/; dstSubfolderSpec = 0; files = ( ); runOnlyForDeploymentPostprocessing = 1; }; 726E4A261C86C88F00C57C6A /* Embed XPC Services */ = { isa = PBXCopyFilesBuildPhase; buildActionMask = 2147483647; dstPath = "$(CONTENTS_FOLDER_PATH)/XPCServices"; dstSubfolderSpec = 16; files = ( 726E4A211C86C88F00C57C6A /* TestAppHelper.xpc in Embed XPC Services */, ); name = "Embed XPC Services"; runOnlyForDeploymentPostprocessing = 0; }; 72B398D01D3D879300EE297F /* CopyFiles */ = { isa = PBXCopyFilesBuildPhase; buildActionMask = 2147483647; dstPath = /usr/share/man/man1/; dstSubfolderSpec = 0; files = ( ); runOnlyForDeploymentPostprocessing = 1; }; 72D954B41CBB4469006F28BD /* Copy Sparkle */ = { isa = PBXCopyFilesBuildPhase; buildActionMask = 2147483647; dstPath = ""; dstSubfolderSpec = 10; files = ( 726F168726747CEB005BEA89 /* Sparkle.framework in Copy Sparkle */, ); name = "Copy Sparkle"; runOnlyForDeploymentPostprocessing = 0; }; EA1E282522B6467A004AA304 /* Copy Headers */ = { isa = PBXCopyFilesBuildPhase; buildActionMask = 2147483647; dstPath = "include/$(PRODUCT_NAME)"; dstSubfolderSpec = 16; files = ( EA1E282622B64693004AA304 /* bscommon.h in Copy Headers */, EA1E282722B64694004AA304 /* bspatch.h in Copy Headers */, EA1E282822B64694004AA304 /* sais.h in Copy Headers */, ); name = "Copy Headers"; runOnlyForDeploymentPostprocessing = 0; }; EA1E283222B660C6004AA304 /* Copy Headers */ = { isa = PBXCopyFilesBuildPhase; buildActionMask = 2147483647; dstPath = "include/$(PRODUCT_NAME)"; dstSubfolderSpec = 16; files = ( EA1E284F22B66115004AA304 /* precomp_data.h in Copy Headers */, EA1E285022B66115004AA304 /* sha512.h in Copy Headers */, EA1E285122B66115004AA304 /* ed25519.h in Copy Headers */, EA1E285222B66115004AA304 /* fe.h in Copy Headers */, EA1E285322B66115004AA304 /* fixedint.h in Copy Headers */, EA1E285422B66115004AA304 /* sc.h in Copy Headers */, EA1E285522B66115004AA304 /* ge.h in Copy Headers */, ); name = "Copy Headers"; runOnlyForDeploymentPostprocessing = 0; }; EA1E285C22B66487004AA304 /* CopyFiles */ = { isa = PBXCopyFilesBuildPhase; buildActionMask = 2147483647; dstPath = /usr/share/man/man1/; dstSubfolderSpec = 0; files = ( ); runOnlyForDeploymentPostprocessing = 1; }; EA1E287422B666EB004AA304 /* CopyFiles */ = { isa = PBXCopyFilesBuildPhase; buildActionMask = 2147483647; dstPath = /usr/share/man/man1/; dstSubfolderSpec = 0; files = ( ); runOnlyForDeploymentPostprocessing = 1; }; /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ 004A8652192A492B00C9730D /* ro */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ro; path = ro.lproj/Sparkle.strings; sourceTree = "<group>"; }; 0263187214FEBB31005EBF43 /* uk */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.strings; name = uk; path = uk.lproj/Sparkle.strings; sourceTree = "<group>"; }; 0867D69BFE84028FC02AAC07 /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = System/Library/Frameworks/Foundation.framework; sourceTree = SDKROOT; }; 0867D6A5FE840307C02AAC07 /* AppKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AppKit.framework; path = System/Library/Frameworks/AppKit.framework; sourceTree = SDKROOT; }; 142E0E0819A83AAC00E4312B /* SUBinaryDeltaTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SUBinaryDeltaTest.m; sourceTree = "<group>"; }; 14652F7919A93E5F00959E44 /* set-git-version-info.sh */ = {isa = PBXFileReference; lastKnownFileType = text.script.sh; path = "set-git-version-info.sh"; sourceTree = "<group>"; }; 14652F8319A9759F00959E44 /* SUExport.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SUExport.h; sourceTree = "<group>"; }; 146EC84E19A68CF8004A50C5 /* Sparkle.podspec */ = {isa = PBXFileReference; lastKnownFileType = text; path = Sparkle.podspec; sourceTree = SOURCE_ROOT; }; 14732BB1195FF6B700593899 /* .clang-format */ = {isa = PBXFileReference; lastKnownFileType = text; path = ".clang-format"; sourceTree = SOURCE_ROOT; }; 14732BB91960EEEE00593899 /* SampleAppcast.xml */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = SampleAppcast.xml; sourceTree = "<group>"; }; 14732BBA1960EF7100593899 /* CHANGELOG */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = CHANGELOG; sourceTree = SOURCE_ROOT; }; 14732BBC1960EFB500593899 /* README.markdown */ = {isa = PBXFileReference; lastKnownFileType = text; path = README.markdown; sourceTree = SOURCE_ROOT; }; 14732BC11960F3B200593899 /* Makefile */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.make; path = Makefile; sourceTree = SOURCE_ROOT; usesTabs = 1; }; 14732BC31960F3FF00593899 /* generate_keys */ = {isa = PBXFileReference; lastKnownFileType = text.script.sh; path = generate_keys; sourceTree = "<group>"; }; 14732BC41960F3FF00593899 /* sign_update */ = {isa = PBXFileReference; lastKnownFileType = text.script.sh; path = sign_update; sourceTree = "<group>"; }; 14732BC91960F70A00593899 /* make-release-package.sh */ = {isa = PBXFileReference; lastKnownFileType = text.script.sh; path = "make-release-package.sh"; sourceTree = "<group>"; }; 14732BD219610A1800593899 /* XCTest.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = XCTest.framework; path = Library/Frameworks/XCTest.framework; sourceTree = DEVELOPER_DIR; }; 147D6D9E1B66DC3C006607AB /* CheckLocalizations.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = CheckLocalizations.swift; path = Sparkle/CheckLocalizations.swift; sourceTree = SOURCE_ROOT; }; 147D6DA71B66EC1C006607AB /* ca */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ca; path = ca.lproj/Sparkle.strings; sourceTree = "<group>"; }; 147D6DA91B66EC22006607AB /* fi */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fi; path = fi.lproj/Sparkle.strings; sourceTree = "<group>"; }; 147D6DAA1B66EC25006607AB /* he */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = he; path = he.lproj/Sparkle.strings; sourceTree = "<group>"; }; 14950074195FDF5900BC5B5B /* SUUpdaterTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SUUpdaterTest.m; sourceTree = "<group>"; usesTabs = 0; }; 14958C6B19AEBC530061B14F /* signed-test-file.txt */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = "signed-test-file.txt"; sourceTree = "<group>"; }; 14958C6C19AEBC610061B14F /* test-pubkey.pem */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = "test-pubkey.pem"; sourceTree = "<group>"; }; 149B78631B7D3A0C00D7D62C /* ConfigCommonCoverage.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = ConfigCommonCoverage.xcconfig; sourceTree = "<group>"; }; 149B78641B7D3A4800D7D62C /* ConfigUnitTestCoverage.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = ConfigUnitTestCoverage.xcconfig; sourceTree = "<group>"; }; 1EAA4C8A2132C7BF00604473 /* ReleaseNotesColorStyle.css */ = {isa = PBXFileReference; lastKnownFileType = text.css; path = ReleaseNotesColorStyle.css; sourceTree = "<group>"; }; 3772FEA813DE0B6B00F79537 /* SUVersionDisplayProtocol.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SUVersionDisplayProtocol.h; sourceTree = "<group>"; }; 37A5F28528E9219000891504 /* zh_HK */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = zh_HK; path = zh_HK.lproj/Sparkle.strings; sourceTree = "<group>"; }; 37A5F28828E9219000891504 /* zh_HK */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = zh_HK; path = zh_HK.lproj/MainMenu.strings; sourceTree = "<group>"; }; 4607BEA21948443800EF8DA4 /* nb */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = nb; path = nb.lproj/Sparkle.strings; sourceTree = "<group>"; }; 525A278F133D6AE900FD8D70 /* Cocoa.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Cocoa.framework; path = System/Library/Frameworks/Cocoa.framework; sourceTree = SDKROOT; }; 555CF29A196C52330000B31E /* el */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = el; path = el.lproj/Sparkle.strings; sourceTree = "<group>"; }; 55C14BD8136EF00C00649790 /* SUStatus.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = SUStatus.xib; sourceTree = "<group>"; }; 55C14F04136EF6DB00649790 /* SULog.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SULog.h; sourceTree = "<group>"; }; 55C14F05136EF6DB00649790 /* SULog.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SULog.m; sourceTree = "<group>"; }; 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 = "<group>"; }; 5A5DD400249585E70045EB3E /* SUUpdateValidatorTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SUUpdateValidatorTest.swift; sourceTree = "<group>"; }; 5A5DD40324958AFF0045EB3E /* SUUpdateValidatorTest */ = {isa = PBXFileReference; lastKnownFileType = folder; path = SUUpdateValidatorTest; sourceTree = "<group>"; }; 5A5DD41B249F0F4B0045EB3E /* test-relative-urls.xml */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xml; path = "test-relative-urls.xml"; sourceTree = "<group>"; }; 5AA4DCD01C73E5510078F128 /* SUAppcastTest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SUAppcastTest.swift; sourceTree = "<group>"; }; 5AD0FA7E1C73F2E2004BCEFF /* testappcast.xml */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xml; path = testappcast.xml; sourceTree = "<group>"; }; 5AEF45D9189D1CC90030D7DC /* tr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = tr; path = tr.lproj/Sparkle.strings; sourceTree = "<group>"; }; 5AF6C74C1AEA40760014A3AB /* SUInstallerTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SUInstallerTest.m; sourceTree = "<group>"; }; 5AF6C74E1AEA46D10014A3AB /* test.pkg */ = {isa = PBXFileReference; lastKnownFileType = file; path = test.pkg; sourceTree = "<group>"; }; 5AF9DC3B1981DBEE001EA135 /* SUSignatureVerifierTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SUSignatureVerifierTest.m; sourceTree = "<group>"; }; 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 = "<group>"; }; 5D06E8DC0FD68CB9005AE3F6 /* bspatch.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = bspatch.c; sourceTree = "<group>"; }; 5D06E8FB0FD68D61005AE3F6 /* libbz2.dylib */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.dylib"; name = libbz2.dylib; path = usr/lib/libbz2.dylib; sourceTree = SDKROOT; }; 5D1AF5890FD7678C0065DB48 /* libxar.1.dylib */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.dylib"; name = libxar.1.dylib; path = usr/lib/libxar.1.dylib; sourceTree = SDKROOT; }; 5D1AF58F0FD767AD0065DB48 /* libxml2.dylib */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.dylib"; name = libxml2.dylib; path = usr/lib/libxml2.dylib; sourceTree = SDKROOT; }; 5D1AF5990FD767E50065DB48 /* libz.dylib */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.dylib"; name = libz.dylib; path = usr/lib/libz.dylib; sourceTree = SDKROOT; }; 5F1510A11C96E591006E1629 /* testnamespaces.xml */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xml; path = testnamespaces.xml; sourceTree = "<group>"; }; 611142E810FB1BE5009810AA /* bspatch.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = bspatch.h; sourceTree = "<group>"; }; 61131A050F846CE600E97AF6 /* da */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.strings; name = da; path = da.lproj/Sparkle.strings; sourceTree = "<group>"; }; 61131A090F846D0A00E97AF6 /* zh_CN */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.strings; name = zh_CN; path = zh_CN.lproj/Sparkle.strings; sourceTree = "<group>"; }; 61131A0A0F846D1100E97AF6 /* zh_TW */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.strings; name = zh_TW; path = zh_TW.lproj/Sparkle.strings; sourceTree = "<group>"; }; 6117796E0D1112E000749C97 /* IOKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = IOKit.framework; path = System/Library/Frameworks/IOKit.framework; sourceTree = SDKROOT; }; 611A904210240DD300CC659E /* pl */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.strings; name = pl; path = pl.lproj/Sparkle.strings; sourceTree = "<group>"; }; 611A904610240DF700CC659E /* ja */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.strings; name = ja; path = ja.lproj/Sparkle.strings; sourceTree = "<group>"; }; 612279D90DB5470200AB99EA /* Sparkle Unit Tests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "Sparkle Unit Tests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; 612279DA0DB5470200AB99EA /* SparkleTests-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "SparkleTests-Info.plist"; sourceTree = "<group>"; }; 61227A150DB548B800AB99EA /* SUVersionComparisonTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SUVersionComparisonTest.m; sourceTree = "<group>"; }; 61299A5B09CA6D4500B7442F /* SUConstants.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SUConstants.h; sourceTree = "<group>"; }; 61299A5F09CA6EB100B7442F /* SUConstants.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SUConstants.m; sourceTree = "<group>"; }; 61299B3509CB04E000B7442F /* Sparkle.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = Sparkle.h; sourceTree = "<group>"; }; 612DCBAD0D488BC60015DBEA /* SUUpdatePermissionPrompt.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SUUpdatePermissionPrompt.h; sourceTree = "<group>"; }; 612DCBAE0D488BC60015DBEA /* SUUpdatePermissionPrompt.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SUUpdatePermissionPrompt.m; sourceTree = "<group>"; }; 613151B20FB4946A000DCD59 /* is */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.strings; name = is; path = is.lproj/Sparkle.strings; sourceTree = "<group>"; }; 615409C4103BBC4000125AF1 /* cs */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.strings; name = cs; path = cs.lproj/Sparkle.strings; sourceTree = "<group>"; }; 6186554310D7484E00B1E074 /* pt-PT */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.strings; name = "pt-PT"; path = "pt-PT.lproj/Sparkle.strings"; sourceTree = "<group>"; }; 618915730E35937600B5E981 /* sv */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.strings; name = sv; path = sv.lproj/Sparkle.strings; sourceTree = "<group>"; }; 6195D4920E404AD700D41A50 /* ru */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.strings; name = ru; path = ru.lproj/Sparkle.strings; sourceTree = "<group>"; }; 6196CFE309C71ADE000DC222 /* SUStatusController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SUStatusController.h; sourceTree = "<group>"; }; 6196CFE409C71ADE000DC222 /* SUStatusController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SUStatusController.m; sourceTree = "<group>"; }; 619B17200E1E9D0800E72754 /* de */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.strings; name = de; path = de.lproj/Sparkle.strings; sourceTree = "<group>"; }; 61A2259C0D1C495D00430CCD /* SUVersionComparisonProtocol.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SUVersionComparisonProtocol.h; sourceTree = "<group>"; }; 61A225A20D1C4AC000430CCD /* SUStandardVersionComparator.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SUStandardVersionComparator.h; sourceTree = "<group>"; }; 61A225A30D1C4AC000430CCD /* SUStandardVersionComparator.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SUStandardVersionComparator.m; sourceTree = "<group>"; }; 61A2279A0D1CEE7600430CCD /* SUSystemProfiler.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SUSystemProfiler.h; sourceTree = "<group>"; }; 61A2279B0D1CEE7600430CCD /* SUSystemProfiler.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SUSystemProfiler.m; sourceTree = "<group>"; }; 61AAE84F0A321AF700D8810D /* es */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.strings; name = es; path = es.lproj/Sparkle.strings; sourceTree = "<group>"; }; 61AAE8590A321B0400D8810D /* fr */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.strings; name = fr; path = fr.lproj/Sparkle.strings; sourceTree = "<group>"; }; 61AAE8710A321F7700D8810D /* nl */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.strings; name = nl; path = nl.lproj/Sparkle.strings; sourceTree = "<group>"; }; 61B5F8E309C4CE3C00B25A18 /* SPUUpdater.h */ = {isa = PBXFileReference; fileEncoding = 30; lastKnownFileType = sourcecode.c.h; path = SPUUpdater.h; sourceTree = "<group>"; }; 61B5F8E409C4CE3C00B25A18 /* SPUUpdater.m */ = {isa = PBXFileReference; fileEncoding = 30; lastKnownFileType = sourcecode.c.objc; path = SPUUpdater.m; sourceTree = "<group>"; }; 61B5F8F609C4CEB300B25A18 /* Security.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Security.framework; path = System/Library/Frameworks/Security.framework; sourceTree = SDKROOT; }; 61B5F90209C4CEE200B25A18 /* Sparkle Test App.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Sparkle Test App.app"; sourceTree = BUILT_PRODUCTS_DIR; }; 61B5F90409C4CEE200B25A18 /* TestApplication-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "TestApplication-Info.plist"; sourceTree = "<group>"; }; 61B5F92409C4CFC900B25A18 /* main.m */ = {isa = PBXFileReference; fileEncoding = 30; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = "<group>"; }; 61B5FB9409C4F04600B25A18 /* SUAppcast.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SUAppcast.h; sourceTree = "<group>"; }; 61B5FB9509C4F04600B25A18 /* SUAppcast.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SUAppcast.m; sourceTree = "<group>"; }; 61B5FC3F09C4FD4000B25A18 /* WebKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = WebKit.framework; path = System/Library/Frameworks/WebKit.framework; sourceTree = SDKROOT; }; 61B5FC5309C5182000B25A18 /* SUAppcastItem.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SUAppcastItem.h; sourceTree = "<group>"; }; 61B5FC5409C5182000B25A18 /* SUAppcastItem.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SUAppcastItem.m; sourceTree = "<group>"; }; 61B5FCA009C5228F00B25A18 /* SUUpdateAlert.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SUUpdateAlert.h; sourceTree = "<group>"; }; 61B5FCA109C5228F00B25A18 /* SUUpdateAlert.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SUUpdateAlert.m; sourceTree = "<group>"; }; 61BA66CC14BDFA0400D02D86 /* sl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = sl; path = sl.lproj/Sparkle.strings; sourceTree = "<group>"; }; 61C268090E2DB5D000175E6C /* LICENSE */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = LICENSE; sourceTree = SOURCE_ROOT; }; 61E31A80103299500051D188 /* pt-BR */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.strings; name = "pt-BR"; path = "pt-BR.lproj/Sparkle.strings"; sourceTree = "<group>"; }; 61EF67550E25B58D00F754E0 /* SUHost.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SUHost.m; sourceTree = "<group>"; }; 61EF67580E25C5B400F754E0 /* SUHost.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SUHost.h; sourceTree = "<group>"; }; 61F3AC1215C22D4A00260CA2 /* th */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = th; path = th.lproj/Sparkle.strings; sourceTree = "<group>"; }; 61F614540E24A12D009F47E7 /* it */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.strings; name = it; path = it.lproj/Sparkle.strings; sourceTree = "<group>"; }; 654F352629B154AE00B10EEB /* ImageIO.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = ImageIO.framework; path = System/Library/Frameworks/ImageIO.framework; sourceTree = SDKROOT; }; 7202DC99269ABD3500737EC4 /* testappcast_phasedRollout.xml */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xml; path = testappcast_phasedRollout.xml; sourceTree = "<group>"; }; 72045CE426FEE708004F96E5 /* strip-framework.sh */ = {isa = PBXFileReference; lastKnownFileType = text.script.sh; path = "strip-framework.sh"; sourceTree = "<group>"; }; 7205C43E1E13049400E370AE /* generate_appcast */ = {isa = PBXFileReference; explicitFileType = "compiled.mach-o.executable"; includeInIndex = 0; path = generate_appcast; sourceTree = BUILT_PRODUCTS_DIR; }; 7205C4401E13049400E370AE /* main.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = main.swift; sourceTree = "<group>"; }; 7205C4461E1304C300E370AE /* Bridging-Header.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "Bridging-Header.h"; sourceTree = "<group>"; }; 7205C4471E1304CE00E370AE /* Appcast.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Appcast.swift; sourceTree = "<group>"; }; 7205C4481E1304CE00E370AE /* ArchiveItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ArchiveItem.swift; sourceTree = "<group>"; }; 7205C44A1E1304CE00E370AE /* FeedXML.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FeedXML.swift; sourceTree = "<group>"; }; 7205C44B1E1304CE00E370AE /* Unarchive.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Unarchive.swift; sourceTree = "<group>"; }; 7205C4511E13053500E370AE /* ConfigSwiftDebug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = ConfigSwiftDebug.xcconfig; sourceTree = "<group>"; }; 7205C4521E13053500E370AE /* ConfigSwiftRelease.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = ConfigSwiftRelease.xcconfig; sourceTree = "<group>"; }; 7205C46C1E13244800E370AE /* ConfigUnitTestDebug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = ConfigUnitTestDebug.xcconfig; sourceTree = "<group>"; }; 7205C46D1E13245600E370AE /* ConfigUnitTestRelease.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = ConfigUnitTestRelease.xcconfig; sourceTree = "<group>"; }; 7205C46E1E13254500E370AE /* ConfigUITestDebug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = ConfigUITestDebug.xcconfig; sourceTree = "<group>"; }; 7205C46F1E13255900E370AE /* ConfigUITestRelease.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = ConfigUITestRelease.xcconfig; sourceTree = "<group>"; }; 720767D11E2EB86200F9A850 /* AppKitPrevention.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AppKitPrevention.h; sourceTree = "<group>"; }; 720B16421C66433D006985FB /* UITests-Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = "UITests-Info.plist"; path = "UITests/UITests-Info.plist"; sourceTree = SOURCE_ROOT; }; 720B16431C66433D006985FB /* SUTestApplicationTest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = SUTestApplicationTest.swift; path = UITests/SUTestApplicationTest.swift; sourceTree = SOURCE_ROOT; }; 720B4C2325EBFAFD005A0592 /* link-tools.sh */ = {isa = PBXFileReference; lastKnownFileType = text.script.sh; path = "link-tools.sh"; sourceTree = "<group>"; }; 720C192827127A3800740C8E /* release-move-tag.sh */ = {isa = PBXFileReference; lastKnownFileType = text.script.sh; path = "release-move-tag.sh"; sourceTree = "<group>"; }; 720DC50327A51A6500DFF3EC /* testappcast_minimumAutoupdateVersionSkipping.xml */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xml; path = testappcast_minimumAutoupdateVersionSkipping.xml; sourceTree = "<group>"; }; 720DC50527A62CDC00DFF3EC /* testappcast_minimumAutoupdateVersionSkipping2.xml */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xml; path = testappcast_minimumAutoupdateVersionSkipping2.xml; sourceTree = "<group>"; }; 720E21791D0D00BF003A311C /* SPUUpdaterCycle.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SPUUpdaterCycle.h; sourceTree = "<group>"; }; 720E217A1D0D00BF003A311C /* SPUUpdaterCycle.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SPUUpdaterCycle.m; sourceTree = "<group>"; }; 7210C7671B9A9A1500EB90AC /* SUUnarchiverTest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SUUnarchiverTest.swift; sourceTree = "<group>"; }; 7214B8851D45AD9A00CB5CED /* SPUInstallationType.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SPUInstallationType.h; sourceTree = "<group>"; }; 72162B071C82C9600013C1C5 /* SULocalizations.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SULocalizations.h; sourceTree = "<group>"; }; 721652671D3C8FED00FD13D8 /* SUInstallerLauncherStatus.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = SUInstallerLauncherStatus.h; path = InstallerLauncher/SUInstallerLauncherStatus.h; sourceTree = SOURCE_ROOT; }; 721AB11626C777D900D34A86 /* SPUDownloadDataPrivate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SPUDownloadDataPrivate.h; sourceTree = "<group>"; }; 721BC2061D17A532002BC71E /* CoreFoundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreFoundation.framework; path = System/Library/Frameworks/CoreFoundation.framework; sourceTree = SDKROOT; }; 721BC2081D17A553002BC71E /* Carbon.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Carbon.framework; path = System/Library/Frameworks/Carbon.framework; sourceTree = SDKROOT; }; 721BC20A1D17A5AD002BC71E /* CoreServices.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreServices.framework; path = System/Library/Frameworks/CoreServices.framework; sourceTree = SDKROOT; }; 721BC20C1D1CDE55002BC71E /* SPULocalCacheDirectory.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SPULocalCacheDirectory.h; sourceTree = "<group>"; }; 721BC20D1D1CDE55002BC71E /* SPULocalCacheDirectory.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SPULocalCacheDirectory.m; sourceTree = "<group>"; }; 721C24451CB753E6005440CB /* Updater.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Updater.app; sourceTree = BUILT_PRODUCTS_DIR; }; 721C24571CB754E8005440CB /* ConfigInstallerProgress.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = ConfigInstallerProgress.xcconfig; sourceTree = "<group>"; }; 721C24581CB7567D005440CB /* InstallerProgress-Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = "InstallerProgress-Info.plist"; path = "Sparkle/InstallerProgress/InstallerProgress-Info.plist"; sourceTree = SOURCE_ROOT; }; 721D588B25BE59F900D23BEA /* SUPhasedUpdateGroupInfo.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SUPhasedUpdateGroupInfo.h; sourceTree = "<group>"; }; 721D588C25BE59F900D23BEA /* SUPhasedUpdateGroupInfo.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SUPhasedUpdateGroupInfo.m; sourceTree = "<group>"; }; 721D5A8325C65D3F00D23BEA /* SUFlatPackageUnarchiver.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = SUFlatPackageUnarchiver.h; path = Autoupdate/SUFlatPackageUnarchiver.h; sourceTree = SOURCE_ROOT; }; 721D5A8425C65D3F00D23BEA /* SUFlatPackageUnarchiver.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; name = SUFlatPackageUnarchiver.m; path = Autoupdate/SUFlatPackageUnarchiver.m; sourceTree = SOURCE_ROOT; }; 721D8A881D4D272E0032E472 /* Installation.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = Installation.md; sourceTree = "<group>"; }; 722194511D3BF987004C34FF /* SPUInstallerAgentProtocol.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = SPUInstallerAgentProtocol.h; path = Sparkle/InstallerProgress/SPUInstallerAgentProtocol.h; sourceTree = SOURCE_ROOT; }; 722194521D3BFEB7004C34FF /* SUInstallerAgentInitiationProtocol.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = SUInstallerAgentInitiationProtocol.h; path = Sparkle/InstallerProgress/SUInstallerAgentInitiationProtocol.h; sourceTree = SOURCE_ROOT; }; 7223E7611AD1AEFF008E3161 /* sais.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = sais.c; sourceTree = "<group>"; }; 7223E7621AD1AEFF008E3161 /* sais.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = sais.h; sourceTree = "<group>"; }; 722545B526805FF80036465C /* testappcast_info_updates.xml */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xml; path = testappcast_info_updates.xml; sourceTree = "<group>"; }; 722545B72680699D0036465C /* SPUAppcastItemStateResolver+Private.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "SPUAppcastItemStateResolver+Private.h"; sourceTree = "<group>"; }; 722589B51E0A24B9005EA0B9 /* Security.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = Security.md; sourceTree = "<group>"; }; 722589B61E0A24C6005EA0B9 /* Design Practices.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = "Design Practices.md"; sourceTree = "<group>"; }; 7229E1B51C97C91100CB50D0 /* SPUUpdateDriver.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SPUUpdateDriver.h; sourceTree = "<group>"; }; 7229E1B71C97CC4D00CB50D0 /* SPUScheduledUpdateDriver.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SPUScheduledUpdateDriver.h; sourceTree = "<group>"; }; 7229E1B81C97CC4D00CB50D0 /* SPUScheduledUpdateDriver.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SPUScheduledUpdateDriver.m; sourceTree = "<group>"; }; 7229E1BB1C98EFF200CB50D0 /* SPUDownloadDriver.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SPUDownloadDriver.h; sourceTree = "<group>"; }; 7229E1BC1C98EFF200CB50D0 /* SPUDownloadDriver.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SPUDownloadDriver.m; sourceTree = "<group>"; }; 722C9538286EA54A0033908A /* SPUGentleUserDriverReminders.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SPUGentleUserDriverReminders.h; sourceTree = "<group>"; }; 722FB7CA1DD69897001D40CE /* ConfigSwift.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = ConfigSwift.xcconfig; sourceTree = "<group>"; }; 722FB7E3260EE53F00EB571C /* SUNormalization.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SUNormalization.h; sourceTree = "<group>"; }; 722FB7E4260EE53F00EB571C /* SUNormalization.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SUNormalization.m; sourceTree = "<group>"; }; 7230BCDD2E22E2BC00B71297 /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Images.xcassets; sourceTree = "<group>"; }; 72316BD11E0DA8430039EFD9 /* SUUnarchiverNotifier.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = SUUnarchiverNotifier.h; path = Autoupdate/SUUnarchiverNotifier.h; sourceTree = SOURCE_ROOT; }; 72316BD21E0DA8430039EFD9 /* SUUnarchiverNotifier.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = SUUnarchiverNotifier.m; path = Autoupdate/SUUnarchiverNotifier.m; sourceTree = SOURCE_ROOT; }; 723414162EBFB482009727E0 /* testappcast_arm64HardwareRequirement.xml */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = testappcast_arm64HardwareRequirement.xml; sourceTree = "<group>"; }; 7235B75E29DFC0120081FE4E /* make-xcframework.sh */ = {isa = PBXFileReference; lastKnownFileType = text.script.sh; path = "make-xcframework.sh"; sourceTree = "<group>"; }; 72366CCC2DC2D1FC003CE62B /* Sparkle.private.modulemap */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.module-map"; path = Sparkle.private.modulemap; sourceTree = "<group>"; }; 723ABCD7259A9A9D00BDB4FA /* Base */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = Base; path = Base.lproj/Sparkle.strings; sourceTree = "<group>"; }; 723ABDDA259A9E8600BDB4FA /* Base */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = Base; path = Base.lproj/InfoPlist.strings; sourceTree = "<group>"; }; 723ABE01259A9E9E00BDB4FA /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = "<group>"; }; 723ABE15259A9EB000BDB4FA /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/MainMenu.strings; sourceTree = "<group>"; }; 723ABE16259A9EBE00BDB4FA /* ar */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ar; path = ar.lproj/MainMenu.strings; sourceTree = "<group>"; }; 723ABE17259A9ECA00BDB4FA /* ca */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ca; path = ca.lproj/MainMenu.strings; sourceTree = "<group>"; }; 723ABE18259A9ECF00BDB4FA /* zh_CN */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = zh_CN; path = zh_CN.lproj/MainMenu.strings; sourceTree = "<group>"; }; 723ABE19259A9ED200BDB4FA /* zh_TW */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = zh_TW; path = zh_TW.lproj/MainMenu.strings; sourceTree = "<group>"; }; 723ABE1A259A9ED600BDB4FA /* cs */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = cs; path = cs.lproj/MainMenu.strings; sourceTree = "<group>"; }; 723ABE1B259A9ED800BDB4FA /* da */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = da; path = da.lproj/MainMenu.strings; sourceTree = "<group>"; }; 723ABE1C259A9EDB00BDB4FA /* nl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = nl; path = nl.lproj/MainMenu.strings; sourceTree = "<group>"; }; 723ABE1D259A9EDE00BDB4FA /* fi */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fi; path = fi.lproj/MainMenu.strings; sourceTree = "<group>"; }; 723ABE1E259A9EE100BDB4FA /* fr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fr; path = fr.lproj/MainMenu.strings; sourceTree = "<group>"; }; 723ABE21259A9EEB00BDB4FA /* de */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = de; path = de.lproj/MainMenu.strings; sourceTree = "<group>"; }; 723ABE22259A9EEE00BDB4FA /* el */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = el; path = el.lproj/MainMenu.strings; sourceTree = "<group>"; }; 723ABE23259A9EF200BDB4FA /* he */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = he; path = he.lproj/MainMenu.strings; sourceTree = "<group>"; }; 723ABE24259A9EF600BDB4FA /* hu */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = hu; path = hu.lproj/MainMenu.strings; sourceTree = "<group>"; }; 723ABE25259A9EF800BDB4FA /* is */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = is; path = is.lproj/MainMenu.strings; sourceTree = "<group>"; }; 723ABE28259A9F0100BDB4FA /* it */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = it; path = it.lproj/MainMenu.strings; sourceTree = "<group>"; }; 723ABE29259A9F0300BDB4FA /* ja */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ja; path = ja.lproj/MainMenu.strings; sourceTree = "<group>"; }; 723ABE2A259A9F0600BDB4FA /* ko */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ko; path = ko.lproj/MainMenu.strings; sourceTree = "<group>"; }; 723ABE2B259A9F0800BDB4FA /* nb */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = nb; path = nb.lproj/MainMenu.strings; sourceTree = "<group>"; }; 723ABE2C259A9F0B00BDB4FA /* pl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = pl; path = pl.lproj/MainMenu.strings; sourceTree = "<group>"; }; 723ABE2D259A9F0F00BDB4FA /* pt-BR */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "pt-BR"; path = "pt-BR.lproj/MainMenu.strings"; sourceTree = "<group>"; }; 723ABE2E259A9F1100BDB4FA /* pt-PT */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "pt-PT"; path = "pt-PT.lproj/MainMenu.strings"; sourceTree = "<group>"; }; 723ABE2F259A9F1400BDB4FA /* ro */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ro; path = ro.lproj/MainMenu.strings; sourceTree = "<group>"; }; 723ABE30259A9F1700BDB4FA /* ru */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ru; path = ru.lproj/MainMenu.strings; sourceTree = "<group>"; }; 723ABE31259A9F1A00BDB4FA /* sk */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = sk; path = sk.lproj/MainMenu.strings; sourceTree = "<group>"; }; 723ABE34259A9F2100BDB4FA /* sl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = sl; path = sl.lproj/MainMenu.strings; sourceTree = "<group>"; }; 723ABE35259A9F2400BDB4FA /* es */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = es; path = es.lproj/MainMenu.strings; sourceTree = "<group>"; }; 723ABE36259A9F2600BDB4FA /* sv */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = sv; path = sv.lproj/MainMenu.strings; sourceTree = "<group>"; }; 723ABE37259A9F2900BDB4FA /* th */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = th; path = th.lproj/MainMenu.strings; sourceTree = "<group>"; }; 723ABE38259A9F2B00BDB4FA /* tr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = tr; path = tr.lproj/MainMenu.strings; sourceTree = "<group>"; }; 723ABE39259A9F2E00BDB4FA /* uk */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = uk; path = uk.lproj/MainMenu.strings; sourceTree = "<group>"; }; 723ABF1A259D055E00BDB4FA /* SUReleaseNotesView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SUReleaseNotesView.h; sourceTree = "<group>"; }; 723ABF2E259D062F00BDB4FA /* SULegacyWebView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SULegacyWebView.h; sourceTree = "<group>"; }; 723ABF2F259D062F00BDB4FA /* SULegacyWebView.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SULegacyWebView.m; sourceTree = "<group>"; }; 723ABFC2259D4CB300BDB4FA /* SUWKWebView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SUWKWebView.h; sourceTree = "<group>"; }; 723ABFC3259D4CB300BDB4FA /* SUWKWebView.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SUWKWebView.m; sourceTree = "<group>"; }; 723AC00E259DBDAA00BDB4FA /* SUReleaseNotesCommon.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SUReleaseNotesCommon.h; sourceTree = "<group>"; }; 723AC00F259DBDAA00BDB4FA /* SUReleaseNotesCommon.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SUReleaseNotesCommon.m; sourceTree = "<group>"; }; 723AD12E29922B5F006BB02F /* test-dangerous-link.xml */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xml; path = "test-dangerous-link.xml"; sourceTree = "<group>"; }; 723B5D9F1CF7AB0100365F95 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = Info.plist; path = Downloader/Info.plist; sourceTree = SOURCE_ROOT; }; 723B5DA01CF7AB0100365F95 /* main.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = main.m; path = Downloader/main.m; sourceTree = SOURCE_ROOT; }; 723B5DA21CF7AB0100365F95 /* SPUDownloader.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = SPUDownloader.h; path = Downloader/SPUDownloader.h; sourceTree = SOURCE_ROOT; }; 723B5DA31CF7AB0100365F95 /* SPUDownloader.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = SPUDownloader.m; path = Downloader/SPUDownloader.m; sourceTree = SOURCE_ROOT; }; 723B5DA41CF7AB0100365F95 /* SPUDownloaderDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = SPUDownloaderDelegate.h; path = Downloader/SPUDownloaderDelegate.h; sourceTree = SOURCE_ROOT; }; 723B5DA51CF7AB0100365F95 /* SPUDownloaderProtocol.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = SPUDownloaderProtocol.h; path = Downloader/SPUDownloaderProtocol.h; sourceTree = SOURCE_ROOT; }; 723C8A521E2D60DB00C14942 /* SUTouchBarButtonGroup.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SUTouchBarButtonGroup.h; sourceTree = "<group>"; }; 723C8A531E2D60DB00C14942 /* SUTouchBarButtonGroup.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SUTouchBarButtonGroup.m; sourceTree = "<group>"; }; 723DFF962B3DFF6E00628E6C /* Package.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Package.swift; sourceTree = "<group>"; }; 723EDC3E26885A8E000BCBA4 /* testappcast_channels.xml */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xml; path = testappcast_channels.xml; sourceTree = "<group>"; }; 7240852D2CA11C1400ED2FCD /* libz.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libz.tbd; path = usr/lib/libz.tbd; sourceTree = SDKROOT; }; 7245495B2ECA648000ABA991 /* SPUUpdaterSettings+Debug.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "SPUUpdaterSettings+Debug.h"; sourceTree = "<group>"; }; 7246E0A11C83B685003B4E75 /* SPUStandardUpdaterController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SPUStandardUpdaterController.h; sourceTree = "<group>"; }; 7246E0A21C83B685003B4E75 /* SPUStandardUpdaterController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SPUStandardUpdaterController.m; sourceTree = "<group>"; }; 724BB36C1D31D0B7005D534A /* InstallerConnection.xpc */ = {isa = PBXFileReference; explicitFileType = "wrapper.xpc-service"; includeInIndex = 0; path = InstallerConnection.xpc; sourceTree = BUILT_PRODUCTS_DIR; }; 724BB36E1D31D0B7005D534A /* SUInstallerConnectionProtocol.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SUInstallerConnectionProtocol.h; sourceTree = "<group>"; }; 724BB36F1D31D0B7005D534A /* SUInstallerConnection.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SUInstallerConnection.h; sourceTree = "<group>"; }; 724BB3701D31D0B7005D534A /* SUInstallerConnection.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SUInstallerConnection.m; sourceTree = "<group>"; }; 724BB3721D31D0B7005D534A /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = "<group>"; }; 724BB3741D31D0B7005D534A /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; }; 724BB37E1D31D1EA005D534A /* ConfigInstallerConnection.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = ConfigInstallerConnection.xcconfig; sourceTree = "<group>"; }; 724BB37F1D31D1EA005D534A /* ConfigInstallerConnectionDebug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = ConfigInstallerConnectionDebug.xcconfig; sourceTree = "<group>"; }; 724BB3801D31F186005D534A /* SUInstallerCommunicationProtocol.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SUInstallerCommunicationProtocol.h; sourceTree = "<group>"; }; 724BB3851D32A167005D534A /* SUXPCInstallerConnection.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SUXPCInstallerConnection.h; sourceTree = "<group>"; }; 724BB3861D32A167005D534A /* SUXPCInstallerConnection.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SUXPCInstallerConnection.m; sourceTree = "<group>"; }; 724BB3931D333832005D534A /* InstallerStatus.xpc */ = {isa = PBXFileReference; explicitFileType = "wrapper.xpc-service"; includeInIndex = 0; path = InstallerStatus.xpc; sourceTree = BUILT_PRODUCTS_DIR; }; 724BB3951D333832005D534A /* SUInstallerStatusProtocol.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SUInstallerStatusProtocol.h; sourceTree = "<group>"; }; 724BB3961D333832005D534A /* SUInstallerStatus.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SUInstallerStatus.h; sourceTree = "<group>"; }; 724BB3971D333832005D534A /* SUInstallerStatus.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SUInstallerStatus.m; sourceTree = "<group>"; }; 724BB3991D333832005D534A /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = "<group>"; }; 724BB39B1D333832005D534A /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; }; 724BB3A41D3338C8005D534A /* ConfigInstallerStatus.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = ConfigInstallerStatus.xcconfig; sourceTree = "<group>"; }; 724BB3A51D3338C8005D534A /* ConfigInstallerStatusDebug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = ConfigInstallerStatusDebug.xcconfig; sourceTree = "<group>"; }; 724BB3A61D33461B005D534A /* SUXPCInstallerStatus.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SUXPCInstallerStatus.h; sourceTree = "<group>"; }; 724BB3A71D33461B005D534A /* SUXPCInstallerStatus.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SUXPCInstallerStatus.m; sourceTree = "<group>"; }; 724BB3B51D35AAC3005D534A /* ServiceManagement.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = ServiceManagement.framework; path = System/Library/Frameworks/ServiceManagement.framework; sourceTree = SDKROOT; }; 72526BE52EF3349D005791ED /* testappcast_minimumUpdateVersion.xml */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = testappcast_minimumUpdateVersion.xml; sourceTree = "<group>"; }; 725453542C04DB3700362123 /* SparkleTestCodeSign_apfs_lzma_aux_files_adhoc.dmg */ = {isa = PBXFileReference; lastKnownFileType = file; path = SparkleTestCodeSign_apfs_lzma_aux_files_adhoc.dmg; sourceTree = "<group>"; }; 725602D31C83551C00DAA70E /* SUApplicationInfo.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SUApplicationInfo.h; sourceTree = "<group>"; }; 725602D41C83551C00DAA70E /* SUApplicationInfo.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SUApplicationInfo.m; sourceTree = "<group>"; }; 72563CA9272E1C5400AF39F0 /* .github */ = {isa = PBXFileReference; lastKnownFileType = folder; path = .github; sourceTree = "<group>"; }; 725B3A81263FBF0C0041AB8E /* testappcast_minimumAutoupdateVersion.xml */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xml; path = testappcast_minimumAutoupdateVersion.xml; sourceTree = "<group>"; }; 725B81F92781AEA40041746F /* libcompression.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libcompression.tbd; path = usr/lib/libcompression.tbd; sourceTree = SDKROOT; }; 725C2EA82782EC44007CB7B5 /* Bridging-Header.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = "Bridging-Header.h"; path = "BinaryDelta/Bridging-Header.h"; sourceTree = SOURCE_ROOT; }; 725C2EA92782EC61007CB7B5 /* main.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = main.swift; path = BinaryDelta/main.swift; sourceTree = SOURCE_ROOT; }; 725C2EAD278386A8007CB7B5 /* SPUDeltaCompressionMode.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = SPUDeltaCompressionMode.h; path = Autoupdate/SPUDeltaCompressionMode.h; sourceTree = SOURCE_ROOT; }; 725CB9561C7120410064365A /* SPUUserDriver.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SPUUserDriver.h; sourceTree = "<group>"; }; 725CB9581C7121830064365A /* SPUStandardUserDriver.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SPUStandardUserDriver.h; sourceTree = "<group>"; }; 725CB9591C7121830064365A /* SPUStandardUserDriver.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SPUStandardUserDriver.m; sourceTree = "<group>"; }; 725DED72263D10C400E7FA8F /* SUAppcast+Private.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "SUAppcast+Private.h"; sourceTree = "<group>"; }; 725EE47E277BF13A00D820CE /* SPUXarDeltaArchive.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = SPUXarDeltaArchive.h; path = Autoupdate/SPUXarDeltaArchive.h; sourceTree = SOURCE_ROOT; }; 725EE47F277BF13B00D820CE /* SPUXarDeltaArchive.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; name = SPUXarDeltaArchive.m; path = Autoupdate/SPUXarDeltaArchive.m; sourceTree = SOURCE_ROOT; }; 725EE481277BF17A00D820CE /* SPUDeltaArchiveProtocol.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = SPUDeltaArchiveProtocol.h; path = Autoupdate/SPUDeltaArchiveProtocol.h; sourceTree = SOURCE_ROOT; }; 725EE484277D375F00D820CE /* SPUDeltaArchive.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = SPUDeltaArchive.h; path = Autoupdate/SPUDeltaArchive.h; sourceTree = SOURCE_ROOT; }; 725EE485277D375F00D820CE /* SPUDeltaArchive.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; name = SPUDeltaArchive.m; path = Autoupdate/SPUDeltaArchive.m; sourceTree = SOURCE_ROOT; }; 725F97741C8A62F500265BE4 /* SUAdHocCodeSigning.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SUAdHocCodeSigning.h; sourceTree = "<group>"; }; 725F97751C8A62F500265BE4 /* SUAdHocCodeSigning.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SUAdHocCodeSigning.m; sourceTree = "<group>"; }; 725F97821C8AA90000265BE4 /* SUPopUpTitlebarUserDriver.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SUPopUpTitlebarUserDriver.h; sourceTree = "<group>"; }; 725F97831C8AA90000265BE4 /* SUPopUpTitlebarUserDriver.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SUPopUpTitlebarUserDriver.m; sourceTree = "<group>"; }; 725F97A21C8B304D00265BE4 /* SUInstallUpdateViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SUInstallUpdateViewController.h; sourceTree = "<group>"; }; 725F97A31C8B304D00265BE4 /* SUInstallUpdateViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SUInstallUpdateViewController.m; sourceTree = "<group>"; }; 725F97A41C8B304D00265BE4 /* SUInstallUpdateViewController.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = SUInstallUpdateViewController.xib; sourceTree = "<group>"; }; 72666DC52B0B28F4001511B0 /* Secret.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Secret.swift; sourceTree = "<group>"; }; 7267E56E1D3D895B00D1BF90 /* SUBinaryDeltaApply.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = SUBinaryDeltaApply.h; path = Autoupdate/SUBinaryDeltaApply.h; sourceTree = SOURCE_ROOT; }; 7267E56F1D3D895B00D1BF90 /* SUBinaryDeltaApply.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = SUBinaryDeltaApply.m; path = Autoupdate/SUBinaryDeltaApply.m; sourceTree = SOURCE_ROOT; }; 7267E5701D3D895B00D1BF90 /* SUBinaryDeltaCommon.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = SUBinaryDeltaCommon.h; path = Autoupdate/SUBinaryDeltaCommon.h; sourceTree = SOURCE_ROOT; }; 7267E5711D3D895B00D1BF90 /* SUBinaryDeltaCommon.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = SUBinaryDeltaCommon.m; path = Autoupdate/SUBinaryDeltaCommon.m; sourceTree = SOURCE_ROOT; }; 7267E5721D3D895B00D1BF90 /* SUBinaryDeltaCreate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = SUBinaryDeltaCreate.h; path = Autoupdate/SUBinaryDeltaCreate.h; sourceTree = SOURCE_ROOT; }; 7267E5731D3D895B00D1BF90 /* SUBinaryDeltaCreate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = SUBinaryDeltaCreate.m; path = Autoupdate/SUBinaryDeltaCreate.m; sourceTree = SOURCE_ROOT; }; 7267E57D1D3D896700D1BF90 /* SUBinaryDeltaUnarchiver.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = SUBinaryDeltaUnarchiver.h; path = Autoupdate/SUBinaryDeltaUnarchiver.h; sourceTree = SOURCE_ROOT; }; 7267E57E1D3D896700D1BF90 /* SUBinaryDeltaUnarchiver.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = SUBinaryDeltaUnarchiver.m; path = Autoupdate/SUBinaryDeltaUnarchiver.m; sourceTree = SOURCE_ROOT; }; 7267E5801D3D89B300D1BF90 /* SUDiskImageUnarchiver.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = SUDiskImageUnarchiver.h; path = Autoupdate/SUDiskImageUnarchiver.h; sourceTree = SOURCE_ROOT; }; 7267E5811D3D89B300D1BF90 /* SUDiskImageUnarchiver.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = SUDiskImageUnarchiver.m; path = Autoupdate/SUDiskImageUnarchiver.m; sourceTree = SOURCE_ROOT; }; 7267E5821D3D89B300D1BF90 /* SUPipedUnarchiver.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = SUPipedUnarchiver.h; path = Autoupdate/SUPipedUnarchiver.h; sourceTree = SOURCE_ROOT; }; 7267E5831D3D89B300D1BF90 /* SUPipedUnarchiver.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = SUPipedUnarchiver.m; path = Autoupdate/SUPipedUnarchiver.m; sourceTree = SOURCE_ROOT; }; 7267E5841D3D89B300D1BF90 /* SUUnarchiver.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = SUUnarchiver.h; path = Autoupdate/SUUnarchiver.h; sourceTree = SOURCE_ROOT; }; 7267E5851D3D89B300D1BF90 /* SUUnarchiver.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = SUUnarchiver.m; path = Autoupdate/SUUnarchiver.m; sourceTree = SOURCE_ROOT; }; 7267E5891D3D89CA00D1BF90 /* SUUnarchiverProtocol.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = SUUnarchiverProtocol.h; path = Autoupdate/SUUnarchiverProtocol.h; sourceTree = SOURCE_ROOT; }; 7267E5981D3D8A5A00D1BF90 /* SUCodeSigningVerifier.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = SUCodeSigningVerifier.h; path = Autoupdate/SUCodeSigningVerifier.h; sourceTree = SOURCE_ROOT; }; 7267E5991D3D8A5A00D1BF90 /* SUCodeSigningVerifier.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = SUCodeSigningVerifier.m; path = Autoupdate/SUCodeSigningVerifier.m; sourceTree = SOURCE_ROOT; }; 7267E59A1D3D8A5A00D1BF90 /* SUSignatureVerifier.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = SUSignatureVerifier.h; path = Autoupdate/SUSignatureVerifier.h; sourceTree = SOURCE_ROOT; }; 7267E59B1D3D8A5A00D1BF90 /* SUSignatureVerifier.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = SUSignatureVerifier.m; path = Autoupdate/SUSignatureVerifier.m; sourceTree = SOURCE_ROOT; }; 7267E59E1D3D8A6F00D1BF90 /* main.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = main.m; path = Autoupdate/main.m; sourceTree = SOURCE_ROOT; }; 7267E5A01D3D8A7E00D1BF90 /* AppInstaller.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = AppInstaller.h; path = Autoupdate/AppInstaller.h; sourceTree = SOURCE_ROOT; }; 7267E5A11D3D8A7E00D1BF90 /* AppInstaller.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = AppInstaller.m; path = Autoupdate/AppInstaller.m; sourceTree = SOURCE_ROOT; }; 7267E5A31D3D8A8A00D1BF90 /* StatusInfo.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = StatusInfo.h; path = Autoupdate/StatusInfo.h; sourceTree = SOURCE_ROOT; }; 7267E5A41D3D8A8A00D1BF90 /* StatusInfo.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = StatusInfo.m; path = Autoupdate/StatusInfo.m; sourceTree = SOURCE_ROOT; }; 7267E5A61D3D8A9900D1BF90 /* AgentConnection.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = AgentConnection.h; path = Autoupdate/AgentConnection.h; sourceTree = SOURCE_ROOT; }; 7267E5A71D3D8A9900D1BF90 /* AgentConnection.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = AgentConnection.m; path = Autoupdate/AgentConnection.m; sourceTree = SOURCE_ROOT; }; 7267E5AC1D3D8AB700D1BF90 /* SPUInstallationInputData.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = SPUInstallationInputData.h; path = Autoupdate/SPUInstallationInputData.h; sourceTree = SOURCE_ROOT; }; 7267E5AD1D3D8AB700D1BF90 /* SPUInstallationInputData.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = SPUInstallationInputData.m; path = Autoupdate/SPUInstallationInputData.m; sourceTree = SOURCE_ROOT; }; 7267E5AF1D3D8AD500D1BF90 /* SPUMessageTypes.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = SPUMessageTypes.h; path = Autoupdate/SPUMessageTypes.h; sourceTree = SOURCE_ROOT; }; 7267E5B01D3D8AD500D1BF90 /* SPUMessageTypes.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = SPUMessageTypes.m; path = Autoupdate/SPUMessageTypes.m; sourceTree = SOURCE_ROOT; }; 7267E5B41D3D8AEE00D1BF90 /* SPUInstallationInfo.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = SPUInstallationInfo.h; path = Autoupdate/SPUInstallationInfo.h; sourceTree = SOURCE_ROOT; }; 7267E5B51D3D8AEE00D1BF90 /* SPUInstallationInfo.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = SPUInstallationInfo.m; path = Autoupdate/SPUInstallationInfo.m; sourceTree = SOURCE_ROOT; }; 7267E5B91D3D8B0B00D1BF90 /* SUInstallerProtocol.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = SUInstallerProtocol.h; path = Autoupdate/SUInstallerProtocol.h; sourceTree = SOURCE_ROOT; }; 7267E5BA1D3D8B2700D1BF90 /* SUGuidedPackageInstaller.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = SUGuidedPackageInstaller.h; path = Autoupdate/SUGuidedPackageInstaller.h; sourceTree = SOURCE_ROOT; }; 7267E5BB1D3D8B2700D1BF90 /* SUGuidedPackageInstaller.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = SUGuidedPackageInstaller.m; path = Autoupdate/SUGuidedPackageInstaller.m; sourceTree = SOURCE_ROOT; }; 7267E5BC1D3D8B2700D1BF90 /* SUInstaller.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = SUInstaller.h; path = Autoupdate/SUInstaller.h; sourceTree = SOURCE_ROOT; }; 7267E5BD1D3D8B2700D1BF90 /* SUInstaller.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = SUInstaller.m; path = Autoupdate/SUInstaller.m; sourceTree = SOURCE_ROOT; }; 7267E5C01D3D8B2700D1BF90 /* SUPlainInstaller.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = SUPlainInstaller.h; path = Autoupdate/SUPlainInstaller.h; sourceTree = SOURCE_ROOT; }; 7267E5C11D3D8B2700D1BF90 /* SUPlainInstaller.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = SUPlainInstaller.m; path = Autoupdate/SUPlainInstaller.m; sourceTree = SOURCE_ROOT; }; 7267E5CF1D3D8C8500D1BF90 /* SecurityFoundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SecurityFoundation.framework; path = System/Library/Frameworks/SecurityFoundation.framework; sourceTree = SDKROOT; }; 7267E5DD1D3D8F5A00D1BF90 /* SUStatusInfoProtocol.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = SUStatusInfoProtocol.h; path = Autoupdate/SUStatusInfoProtocol.h; sourceTree = SOURCE_ROOT; }; 7267E5E31D3D90AA00D1BF90 /* SUFileManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SUFileManager.h; sourceTree = "<group>"; }; 7267E5E41D3D90AA00D1BF90 /* SUFileManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SUFileManager.m; sourceTree = "<group>"; }; 7267E5FB1D3DD1B700D1BF90 /* SPUResumableUpdate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SPUResumableUpdate.h; sourceTree = "<group>"; }; 7269E492264798200088C213 /* SPUSkippedUpdate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SPUSkippedUpdate.h; sourceTree = "<group>"; }; 7269E493264798200088C213 /* SPUSkippedUpdate.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SPUSkippedUpdate.m; sourceTree = "<group>"; }; 7269E4992648F7C00088C213 /* SPUUserUpdateState.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SPUUserUpdateState.m; sourceTree = "<group>"; }; 7269E49C2648FC6C0088C213 /* SPUUserUpdateState+Private.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "SPUUserUpdateState+Private.h"; sourceTree = "<group>"; }; 726B20602CF4F1D300E6F7DB /* DevSignedAppVersion2.dmg */ = {isa = PBXFileReference; lastKnownFileType = file; path = DevSignedAppVersion2.dmg; sourceTree = "<group>"; }; 726B2B5D1C645FC900388755 /* UI Tests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "UI Tests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; 726DF88D1C84277500188804 /* SPUUserUpdateState.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SPUUserUpdateState.h; sourceTree = "<group>"; }; 726E075A1CA3A6D6001A286B /* SPUSecureCoding.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = SPUSecureCoding.h; path = Sparkle/SPUSecureCoding.h; sourceTree = SOURCE_ROOT; }; 726E075B1CA3A6D6001A286B /* SPUSecureCoding.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = SPUSecureCoding.m; path = Sparkle/SPUSecureCoding.m; sourceTree = SOURCE_ROOT; }; 726E07681CA616A4001A286B /* libxar.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libxar.tbd; path = usr/lib/libxar.tbd; sourceTree = SDKROOT; }; 726E076A1CA616B3001A286B /* libbz2.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libbz2.tbd; path = usr/lib/libbz2.tbd; sourceTree = SDKROOT; }; 726E078B1CA891E9001A286B /* SPUUpdaterSettings.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SPUUpdaterSettings.h; sourceTree = "<group>"; }; 726E078C1CA891E9001A286B /* SPUUpdaterSettings.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SPUUpdaterSettings.m; sourceTree = "<group>"; }; 726E07AD1CAF08D6001A286B /* Installer.xpc */ = {isa = PBXFileReference; explicitFileType = "wrapper.xpc-service"; includeInIndex = 0; path = Installer.xpc; sourceTree = BUILT_PRODUCTS_DIR; }; 726E07AF1CAF08D6001A286B /* SUInstallerLauncherProtocol.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SUInstallerLauncherProtocol.h; sourceTree = "<group>"; }; 726E07B01CAF08D6001A286B /* SUInstallerLauncher.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SUInstallerLauncher.h; sourceTree = "<group>"; }; 726E07B11CAF08D6001A286B /* SUInstallerLauncher.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SUInstallerLauncher.m; sourceTree = "<group>"; }; 726E07B31CAF08D6001A286B /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = "<group>"; }; 726E07B51CAF08D6001A286B /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; }; 726E07C11CAF1E79001A286B /* ConfigInstallerLauncher.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = ConfigInstallerLauncher.xcconfig; sourceTree = "<group>"; }; 726E07EF1CAF37BD001A286B /* Downloader.xpc */ = {isa = PBXFileReference; explicitFileType = "wrapper.xpc-service"; includeInIndex = 0; path = Downloader.xpc; sourceTree = BUILT_PRODUCTS_DIR; }; 726E4A111C86C44A00C57C6A /* Sparkle-Test-App.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = "Sparkle-Test-App.entitlements"; sourceTree = "<group>"; }; 726E4A161C86C88F00C57C6A /* TestAppHelper.xpc */ = {isa = PBXFileReference; explicitFileType = "wrapper.xpc-service"; includeInIndex = 0; path = TestAppHelper.xpc; sourceTree = BUILT_PRODUCTS_DIR; }; 726E4A181C86C88F00C57C6A /* TestAppHelperProtocol.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TestAppHelperProtocol.h; sourceTree = "<group>"; }; 726E4A191C86C88F00C57C6A /* TestAppHelper.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TestAppHelper.h; sourceTree = "<group>"; }; 726E4A1A1C86C88F00C57C6A /* TestAppHelper.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = TestAppHelper.m; sourceTree = "<group>"; }; 726E4A1C1C86C88F00C57C6A /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = "<group>"; }; 726E4A1E1C86C88F00C57C6A /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; }; 726E4A281C86CAB500C57C6A /* ConfigTestAppHelper.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = ConfigTestAppHelper.xcconfig; sourceTree = "<group>"; }; 726E4A361C89116000C57C6A /* SPUStandardUserDriverDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SPUStandardUserDriverDelegate.h; sourceTree = "<group>"; }; 726F2CE31BC9C33D001971A4 /* SUOperatingSystem.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SUOperatingSystem.h; sourceTree = "<group>"; }; 726F2CE41BC9C33D001971A4 /* SUOperatingSystem.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SUOperatingSystem.m; sourceTree = "<group>"; }; 726FC0352C1E787A00177986 /* SparkleTestCodeSignApp.aar */ = {isa = PBXFileReference; lastKnownFileType = file; path = SparkleTestCodeSignApp.aar; sourceTree = "<group>"; }; 726FC0372C1E96AA00177986 /* SparkleTestCodeSignApp.enc.aar */ = {isa = PBXFileReference; lastKnownFileType = file; path = SparkleTestCodeSignApp.enc.aar; sourceTree = "<group>"; }; 726FD2CB25F4BE5F00123BC6 /* fa */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fa; path = fa.lproj/MainMenu.strings; sourceTree = "<group>"; }; 726FD2CC25F4BE5F00123BC6 /* fa */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fa; path = fa.lproj/Sparkle.strings; sourceTree = "<group>"; }; 727F340A2605321D00020E85 /* SULog+NSError.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "SULog+NSError.m"; sourceTree = "<group>"; }; 727F34212605323500020E85 /* SULog+NSError.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "SULog+NSError.h"; sourceTree = "<group>"; }; 728337A41C9E6FF40085AA99 /* SPUProbeInstallStatus.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SPUProbeInstallStatus.h; sourceTree = "<group>"; }; 728337A51C9E6FF40085AA99 /* SPUProbeInstallStatus.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SPUProbeInstallStatus.m; sourceTree = "<group>"; }; 728638EE1CAF589C00783084 /* ConfigDownloader.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = ConfigDownloader.xcconfig; sourceTree = "<group>"; }; 7286EE5D28CEC84900163C1D /* SUTextViewReleaseNotesView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SUTextViewReleaseNotesView.h; sourceTree = "<group>"; }; 7286EE5E28CEC84900163C1D /* SUTextViewReleaseNotesView.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SUTextViewReleaseNotesView.m; sourceTree = "<group>"; }; 728ED348277DA23400D9238F /* SPUSparkleDeltaArchive.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = SPUSparkleDeltaArchive.h; path = Autoupdate/SPUSparkleDeltaArchive.h; sourceTree = SOURCE_ROOT; }; 728ED349277DA23400D9238F /* SPUSparkleDeltaArchive.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; name = SPUSparkleDeltaArchive.m; path = Autoupdate/SPUSparkleDeltaArchive.m; sourceTree = SOURCE_ROOT; }; 728FBA1B2BB5013300651EDF /* nn */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = nn; path = nn.lproj/MainMenu.strings; sourceTree = "<group>"; }; 728FBA1C2BB5013300651EDF /* nn */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = nn; path = nn.lproj/Sparkle.strings; sourceTree = "<group>"; }; 729924921DF4A45000DBCDF5 /* SUUpdateValidator.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = SUUpdateValidator.h; path = Sparkle/SUUpdateValidator.h; sourceTree = SOURCE_ROOT; }; 729924931DF4A45000DBCDF5 /* SUUpdateValidator.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = SUUpdateValidator.m; path = Sparkle/SUUpdateValidator.m; sourceTree = SOURCE_ROOT; }; 729BB3D11D503826007C4276 /* Downloader.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; name = Downloader.entitlements; path = Downloader/Downloader.entitlements; sourceTree = SOURCE_ROOT; }; 729C51272E58015600D7365A /* SUUpdatePermissionPrompt.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = SUUpdatePermissionPrompt.xib; sourceTree = "<group>"; }; 729F10FD1C65A9B500DFCCC5 /* ConfigUITest.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = ConfigUITest.xcconfig; sourceTree = "<group>"; }; 729F10FE1C65A9B500DFCCC5 /* ConfigUITestCoverage.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = ConfigUITestCoverage.xcconfig; sourceTree = "<group>"; }; 729F7EAB27366353004592DC /* test-links.xml */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xml; path = "test-links.xml"; sourceTree = "<group>"; }; 729F7EAD273F1840004592DC /* SPUUserAgent+Private.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "SPUUserAgent+Private.h"; sourceTree = "<group>"; }; 729F7EAE273F1840004592DC /* SPUUserAgent+Private.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "SPUUserAgent+Private.m"; sourceTree = "<group>"; }; 729F7ECD27409076004592DC /* SparkleTestCodeSignApp_bad_extraneous.zip */ = {isa = PBXFileReference; lastKnownFileType = archive.zip; path = SparkleTestCodeSignApp_bad_extraneous.zip; sourceTree = "<group>"; }; 72A40F082956120D007C7DD5 /* ConfigFrameworkRelease.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = ConfigFrameworkRelease.xcconfig; sourceTree = "<group>"; }; 72A450511C69A68900D67EEA /* SUUpdatePermissionResponse.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SUUpdatePermissionResponse.h; sourceTree = "<group>"; }; 72A450521C69A68900D67EEA /* SUUpdatePermissionResponse.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SUUpdatePermissionResponse.m; sourceTree = "<group>"; }; 72A4A23F1BB6567D00E7820D /* SUFileManagerTest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SUFileManagerTest.swift; sourceTree = "<group>"; }; 72A6F9891C94E2D6005F404C /* SUUpdaterDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SUUpdaterDelegate.h; sourceTree = "<group>"; }; 72AC6B251B9AAC8800F62325 /* SparkleTestCodeSignApp.tar.gz */ = {isa = PBXFileReference; lastKnownFileType = archive.gzip; path = SparkleTestCodeSignApp.tar.gz; sourceTree = "<group>"; }; 72AC6B271B9AAD6700F62325 /* SparkleTestCodeSignApp.tar */ = {isa = PBXFileReference; lastKnownFileType = archive.tar; path = SparkleTestCodeSignApp.tar; sourceTree = "<group>"; }; 72AC6B291B9AAF3A00F62325 /* SparkleTestCodeSignApp.tar.bz2 */ = {isa = PBXFileReference; lastKnownFileType = file; path = SparkleTestCodeSignApp.tar.bz2; sourceTree = "<group>"; }; 72AC6B2B1B9AB0EE00F62325 /* SparkleTestCodeSignApp.tar.xz */ = {isa = PBXFileReference; lastKnownFileType = file; path = SparkleTestCodeSignApp.tar.xz; sourceTree = "<group>"; }; 72AC6B2D1B9B218C00F62325 /* SparkleTestCodeSignApp.dmg */ = {isa = PBXFileReference; lastKnownFileType = file; path = SparkleTestCodeSignApp.dmg; sourceTree = "<group>"; }; 72AEB1D229A1A74E0033883E /* SPUStandardVersionDisplay.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SPUStandardVersionDisplay.h; sourceTree = "<group>"; }; 72AEB1D329A1A74E0033883E /* SPUStandardVersionDisplay.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SPUStandardVersionDisplay.m; sourceTree = "<group>"; }; 72AEB1D629A1CB510033883E /* SPUNoUpdateFoundInfo.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SPUNoUpdateFoundInfo.h; sourceTree = "<group>"; }; 72AEB1D729A1CB510033883E /* SPUNoUpdateFoundInfo.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SPUNoUpdateFoundInfo.m; sourceTree = "<group>"; }; 72AFC6121B9A944200F6B565 /* Sparkle Unit Tests-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Sparkle Unit Tests-Bridging-Header.h"; sourceTree = "<group>"; }; 72B09CE91CEA18900052EF9E /* bscommon.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = bscommon.c; sourceTree = "<group>"; }; 72B09CEA1CEA18900052EF9E /* bscommon.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = bscommon.h; sourceTree = "<group>"; }; 72B398D21D3D879300EE297F /* Autoupdate */ = {isa = PBXFileReference; explicitFileType = "compiled.mach-o.executable"; includeInIndex = 0; path = Autoupdate; sourceTree = BUILT_PRODUCTS_DIR; }; 72B3DEC71E23472200457642 /* SPUDownloadedUpdate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SPUDownloadedUpdate.h; sourceTree = "<group>"; }; 72B3DEC81E23472200457642 /* SPUDownloadedUpdate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SPUDownloadedUpdate.m; sourceTree = "<group>"; }; 72B3DECB1E23479000457642 /* SPUInformationalUpdate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SPUInformationalUpdate.h; sourceTree = "<group>"; }; 72B3DECC1E23479000457642 /* SPUInformationalUpdate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SPUInformationalUpdate.m; sourceTree = "<group>"; }; 72B767C81C9B707000A07552 /* SUAppcastDriver.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SUAppcastDriver.h; sourceTree = "<group>"; }; 72B767C91C9B707000A07552 /* SUAppcastDriver.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SUAppcastDriver.m; sourceTree = "<group>"; }; 72B767CC1C9B924900A07552 /* SPUInstallerDriver.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SPUInstallerDriver.h; sourceTree = "<group>"; }; 72B767CD1C9B924900A07552 /* SPUInstallerDriver.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SPUInstallerDriver.m; sourceTree = "<group>"; }; 72B767D01C9C7B9300A07552 /* SPUProbingUpdateDriver.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SPUProbingUpdateDriver.h; sourceTree = "<group>"; }; 72B767D11C9C7B9300A07552 /* SPUProbingUpdateDriver.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SPUProbingUpdateDriver.m; sourceTree = "<group>"; }; 72B767D41C9C8B5C00A07552 /* SPUBasicUpdateDriver.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SPUBasicUpdateDriver.h; sourceTree = "<group>"; }; 72B767D51C9C8B5C00A07552 /* SPUBasicUpdateDriver.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SPUBasicUpdateDriver.m; sourceTree = "<group>"; }; 72B767D81C9CD2E400A07552 /* SPUUIBasedUpdateDriver.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SPUUIBasedUpdateDriver.h; sourceTree = "<group>"; }; 72B767D91C9CD2E400A07552 /* SPUUIBasedUpdateDriver.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SPUUIBasedUpdateDriver.m; sourceTree = "<group>"; }; 72B767DC1C9CDB9700A07552 /* SPUUserInitiatedUpdateDriver.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SPUUserInitiatedUpdateDriver.h; sourceTree = "<group>"; }; 72B767DD1C9CDB9700A07552 /* SPUUserInitiatedUpdateDriver.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SPUUserInitiatedUpdateDriver.m; sourceTree = "<group>"; }; 72B767E01C9CE90A00A07552 /* SPUCoreBasedUpdateDriver.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SPUCoreBasedUpdateDriver.h; sourceTree = "<group>"; }; 72B767E11C9CE90A00A07552 /* SPUCoreBasedUpdateDriver.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SPUCoreBasedUpdateDriver.m; sourceTree = "<group>"; }; 72B767E41C9CFD7200A07552 /* SPUAutomaticUpdateDriver.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SPUAutomaticUpdateDriver.h; sourceTree = "<group>"; }; 72B767E51C9CFD7200A07552 /* SPUAutomaticUpdateDriver.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SPUAutomaticUpdateDriver.m; sourceTree = "<group>"; }; 72BC6C3C275027BF0083F14B /* SparkleTestCodeSign_apfs.dmg */ = {isa = PBXFileReference; lastKnownFileType = file; path = SparkleTestCodeSign_apfs.dmg; sourceTree = "<group>"; }; 72BEBFEE1D7287560019146B /* SUSpotlightImporterTest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SUSpotlightImporterTest.swift; sourceTree = "<group>"; }; 72C56E072EFDFB850005A484 /* SPUExtractSignedFeed.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SPUExtractSignedFeed.h; sourceTree = "<group>"; }; 72C56E082EFDFB850005A484 /* SPUExtractSignedFeed.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SPUExtractSignedFeed.m; sourceTree = "<group>"; }; 72C56E0F2EFEF10B0005A484 /* Signing.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Signing.swift; sourceTree = "<group>"; }; 72C56E182F04343D0005A484 /* SPUAppcastSigningValidationStatus.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SPUAppcastSigningValidationStatus.h; sourceTree = "<group>"; }; 72C56E1E2F04AC890005A484 /* SUFeedSignatureVerifierTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SUFeedSignatureVerifierTest.swift; sourceTree = "<group>"; }; 72C56E212F04C28B0005A484 /* testreleasenotes.html */ = {isa = PBXFileReference; lastKnownFileType = text.html; path = testreleasenotes.html; sourceTree = "<group>"; }; 72CCDEBD27421FD500B53718 /* SparkleTestCodeSignApp_bad_header.zip */ = {isa = PBXFileReference; lastKnownFileType = archive.zip; path = SparkleTestCodeSignApp_bad_header.zip; sourceTree = "<group>"; }; 72D04F3B2B094C8400A6DEAA /* SPUVerifierInformation.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SPUVerifierInformation.h; sourceTree = "<group>"; }; 72D04F3C2B094C8400A6DEAA /* SPUVerifierInformation.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SPUVerifierInformation.m; sourceTree = "<group>"; }; 72D60CD828C2BA2100189AB8 /* SPUStandardUserDriver+Private.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "SPUStandardUserDriver+Private.h"; sourceTree = "<group>"; }; 72D9547F1CBACC35006F28BD /* InstallerProgressAppController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = InstallerProgressAppController.h; path = Sparkle/InstallerProgress/InstallerProgressAppController.h; sourceTree = SOURCE_ROOT; }; 72D954801CBACC35006F28BD /* InstallerProgressAppController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = InstallerProgressAppController.m; path = Sparkle/InstallerProgress/InstallerProgressAppController.m; sourceTree = SOURCE_ROOT; }; 72D954821CBAD34F006F28BD /* main.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = main.m; path = Sparkle/InstallerProgress/main.m; sourceTree = SOURCE_ROOT; }; 72D954841CBAD418006F28BD /* ShowInstallerProgress.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = ShowInstallerProgress.h; path = Sparkle/InstallerProgress/ShowInstallerProgress.h; sourceTree = SOURCE_ROOT; }; 72D954851CBAD4AE006F28BD /* InstallerProgressDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = InstallerProgressDelegate.h; path = Sparkle/InstallerProgress/InstallerProgressDelegate.h; sourceTree = SOURCE_ROOT; }; 72D9549E1CBB415B006F28BD /* sparkle.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = sparkle.app; sourceTree = BUILT_PRODUCTS_DIR; }; 72D954A01CBB415C006F28BD /* SPUCommandLineDriver.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SPUCommandLineDriver.h; sourceTree = "<group>"; }; 72D954A11CBB415C006F28BD /* SPUCommandLineDriver.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SPUCommandLineDriver.m; sourceTree = "<group>"; }; 72D954A41CBB415C006F28BD /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = "<group>"; }; 72D954AB1CBB415C006F28BD /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; }; 72D954B01CBB41E2006F28BD /* ConfigSparkleTool.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = ConfigSparkleTool.xcconfig; sourceTree = "<group>"; }; 72D954B61CBB467F006F28BD /* SPUCommandLineUserDriver.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SPUCommandLineUserDriver.h; sourceTree = "<group>"; }; 72D954B71CBB467F006F28BD /* SPUCommandLineUserDriver.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SPUCommandLineUserDriver.m; sourceTree = "<group>"; }; 72DBA37B1D60CC34002594A8 /* SPUUpdatePermissionRequest.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SPUUpdatePermissionRequest.h; sourceTree = "<group>"; }; 72DBA37C1D60CC34002594A8 /* SPUUpdatePermissionRequest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SPUUpdatePermissionRequest.m; sourceTree = "<group>"; }; 72E1DAB725B3E8AE0001BA6D /* hr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = hr; path = hr.lproj/MainMenu.strings; sourceTree = "<group>"; }; 72E1DACB25B3E8DF0001BA6D /* hr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = hr; path = hr.lproj/Sparkle.strings; sourceTree = "<group>"; }; 72E45CF11B640CDD005C701A /* SUTestApplicationDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SUTestApplicationDelegate.h; sourceTree = "<group>"; }; 72E45CF21B640CDD005C701A /* SUTestApplicationDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SUTestApplicationDelegate.m; sourceTree = "<group>"; }; 72E45CF41B640DAE005C701A /* SUUpdateSettingsWindowController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SUUpdateSettingsWindowController.h; sourceTree = "<group>"; }; 72E45CF51B640DAE005C701A /* SUUpdateSettingsWindowController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SUUpdateSettingsWindowController.m; sourceTree = "<group>"; }; 72E45CF61B640DAE005C701A /* SUUpdateSettingsWindowController.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = SUUpdateSettingsWindowController.xib; sourceTree = "<group>"; }; 72E45CFB1B641961005C701A /* sparkletestcast.xml */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xml; path = sparkletestcast.xml; sourceTree = "<group>"; }; 72E6D9702C04DE19005496E4 /* SparkleTestCodeSign_pkg.dmg */ = {isa = PBXFileReference; lastKnownFileType = file; path = SparkleTestCodeSign_pkg.dmg; sourceTree = "<group>"; }; 72E6D9722C0526DC005496E4 /* SparkleTestCodeSignApp.enc.nolicense.dmg */ = {isa = PBXFileReference; lastKnownFileType = file; path = SparkleTestCodeSignApp.enc.nolicense.dmg; sourceTree = "<group>"; }; 72EB735E29BE981300FBCEE7 /* DevSignedApp.zip */ = {isa = PBXFileReference; lastKnownFileType = archive.zip; path = DevSignedApp.zip; sourceTree = "<group>"; }; 72EB736029BEB36100FBCEE7 /* DevSignedAppVersion2.zip */ = {isa = PBXFileReference; lastKnownFileType = archive.zip; path = DevSignedAppVersion2.zip; sourceTree = "<group>"; }; 72EB87E91CB8798800C37F42 /* ShowInstallerProgress.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = ShowInstallerProgress.m; path = Sparkle/InstallerProgress/ShowInstallerProgress.m; sourceTree = SOURCE_ROOT; }; 72EE17FA26D1CBC000C58B19 /* SUInstallerLauncher+Private.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "SUInstallerLauncher+Private.h"; sourceTree = "<group>"; }; 72EE181826DAE6E100C58B19 /* SPUUpdateCheck.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SPUUpdateCheck.h; sourceTree = "<group>"; }; 72EF30BA2675CF38008CE987 /* SPUAppcastItemStateResolver.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SPUAppcastItemStateResolver.h; sourceTree = "<group>"; }; 72EF30BB2675CF38008CE987 /* SPUAppcastItemState.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SPUAppcastItemState.m; sourceTree = "<group>"; }; 72EF30BC2675CF38008CE987 /* SPUAppcastItemStateResolver.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SPUAppcastItemStateResolver.m; sourceTree = "<group>"; }; 72EF30BD2675CF38008CE987 /* SPUAppcastItemState.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SPUAppcastItemState.h; sourceTree = "<group>"; }; 72EF30C6267C716A008CE987 /* SUAppcastItem+Private.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "SUAppcastItem+Private.h"; sourceTree = "<group>"; }; 72F0EC44278A55CA002A876A /* screenshot.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = screenshot.png; sourceTree = "<group>"; }; 72F94ED51CC3441A002DEE68 /* ConfigInstallerLauncherDebug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = ConfigInstallerLauncherDebug.xcconfig; sourceTree = "<group>"; }; 72F94ED61CC344A7002DEE68 /* ConfigDownloaderDebug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = ConfigDownloaderDebug.xcconfig; sourceTree = "<group>"; }; 72F94EDA1CC36C37002DEE68 /* ConfigTestAppDebug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = ConfigTestAppDebug.xcconfig; sourceTree = "<group>"; }; 72F94EDB1CC36C6F002DEE68 /* ConfigTestAppHelperDebug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = ConfigTestAppHelperDebug.xcconfig; sourceTree = "<group>"; }; 72F94F561CC44DE1002DEE68 /* SPUXPCServiceInfo.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SPUXPCServiceInfo.h; sourceTree = "<group>"; }; 72F94F571CC44DE1002DEE68 /* SPUXPCServiceInfo.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SPUXPCServiceInfo.m; sourceTree = "<group>"; }; 72F9EBE01D517E2F004AC8B6 /* SUUpdater.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SUUpdater.h; sourceTree = "<group>"; }; 72F9EBE11D517E2F004AC8B6 /* SUUpdater.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SUUpdater.m; sourceTree = "<group>"; }; 72F9EC3D1D5E823F004AC8B6 /* SPUUpdaterTimer.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SPUUpdaterTimer.h; sourceTree = "<group>"; }; 72F9EC3E1D5E823F004AC8B6 /* SPUUpdaterTimer.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SPUUpdaterTimer.m; sourceTree = "<group>"; }; 72F9EC421D5E9ED8004AC8B6 /* SPUDownloadData.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SPUDownloadData.h; sourceTree = "<group>"; }; 72F9EC431D5E9ED8004AC8B6 /* SPUDownloadData.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SPUDownloadData.m; sourceTree = "<group>"; }; 72F9EC471D5EA7D3004AC8B6 /* SPUUpdaterDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SPUUpdaterDelegate.h; sourceTree = "<group>"; }; 72FE54412E56A14100227A91 /* SUUpdateAlert.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = SUUpdateAlert.xib; sourceTree = "<group>"; }; 8DC2EF5A0486A6940098B216 /* Sparkle-Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = "Sparkle-Info.plist"; sourceTree = "<group>"; }; 8DC2EF5B0486A6940098B216 /* Sparkle.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Sparkle.framework; sourceTree = BUILT_PRODUCTS_DIR; }; A5BF4F1B1BC7668B007A052A /* SUTestWebServer.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SUTestWebServer.h; sourceTree = "<group>"; }; A5BF4F1C1BC7668B007A052A /* SUTestWebServer.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SUTestWebServer.m; sourceTree = "<group>"; }; C23E88591BE7AF890050BB73 /* SparkleTestCodeSignApp.enc.dmg */ = {isa = PBXFileReference; lastKnownFileType = file; path = SparkleTestCodeSignApp.enc.dmg; sourceTree = "<group>"; }; E0949FCF2EFC4CFC0039C748 /* vi */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = vi; path = vi.lproj/MainMenu.strings; sourceTree = "<group>"; }; E0949FD02EFC4CFD0039C748 /* vi */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = vi; path = vi.lproj/Sparkle.strings; sourceTree = "<group>"; }; EA1E280F22B64522004AA304 /* libbsdiff.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libbsdiff.a; sourceTree = BUILT_PRODUCTS_DIR; }; EA1E281422B64548004AA304 /* bsdiff-Debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "bsdiff-Debug.xcconfig"; sourceTree = "<group>"; }; EA1E281522B64549004AA304 /* bsdiff-Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "bsdiff-Release.xcconfig"; sourceTree = "<group>"; }; EA1E281622B64549004AA304 /* bsdiff-Shared.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "bsdiff-Shared.xcconfig"; sourceTree = "<group>"; }; EA1E282D22B660BE004AA304 /* libed25519.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libed25519.a; sourceTree = BUILT_PRODUCTS_DIR; }; EA1E283422B660ED004AA304 /* precomp_data.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = precomp_data.h; sourceTree = "<group>"; }; EA1E283522B660ED004AA304 /* seed.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = seed.c; sourceTree = "<group>"; }; EA1E283622B660ED004AA304 /* fe.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = fe.c; sourceTree = "<group>"; }; EA1E283722B660ED004AA304 /* verify.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = verify.c; sourceTree = "<group>"; }; EA1E283822B660ED004AA304 /* ge.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = ge.c; sourceTree = "<group>"; }; EA1E283922B660ED004AA304 /* sc.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = sc.c; sourceTree = "<group>"; }; EA1E283A22B660ED004AA304 /* sign.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = sign.c; sourceTree = "<group>"; }; EA1E283B22B660ED004AA304 /* sha512.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = sha512.h; sourceTree = "<group>"; }; EA1E283C22B660ED004AA304 /* key_exchange.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = key_exchange.c; sourceTree = "<group>"; }; EA1E283D22B660ED004AA304 /* add_scalar.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = add_scalar.c; sourceTree = "<group>"; }; EA1E283E22B660ED004AA304 /* ed25519.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ed25519.h; sourceTree = "<group>"; }; EA1E283F22B660ED004AA304 /* fe.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = fe.h; sourceTree = "<group>"; }; EA1E284022B660ED004AA304 /* keypair.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = keypair.c; sourceTree = "<group>"; }; EA1E284122B660ED004AA304 /* fixedint.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = fixedint.h; sourceTree = "<group>"; }; EA1E284222B660ED004AA304 /* sha512.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = sha512.c; sourceTree = "<group>"; }; EA1E284322B660ED004AA304 /* sc.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = sc.h; sourceTree = "<group>"; }; EA1E284422B660ED004AA304 /* ge.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ge.h; sourceTree = "<group>"; }; EA1E285722B6622E004AA304 /* ed25519-Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "ed25519-Release.xcconfig"; sourceTree = "<group>"; }; EA1E285822B6622E004AA304 /* ed25519-Debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "ed25519-Debug.xcconfig"; sourceTree = "<group>"; }; EA1E285922B6622E004AA304 /* ed25519-Shared.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "ed25519-Shared.xcconfig"; sourceTree = "<group>"; }; EA1E285E22B66487004AA304 /* generate_keys */ = {isa = PBXFileReference; explicitFileType = "compiled.mach-o.executable"; includeInIndex = 0; path = generate_keys; sourceTree = BUILT_PRODUCTS_DIR; }; EA1E286022B66487004AA304 /* main.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = main.swift; sourceTree = "<group>"; }; EA1E286622B664A5004AA304 /* Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Bridging-Header.h"; sourceTree = "<group>"; }; EA1E286722B664FD004AA304 /* CommandLineTool-Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "CommandLineTool-Release.xcconfig"; sourceTree = "<group>"; }; EA1E286822B664FD004AA304 /* CommandLineTool-Debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "CommandLineTool-Debug.xcconfig"; sourceTree = "<group>"; }; EA1E286922B664FD004AA304 /* CommandLineTool-Shared.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "CommandLineTool-Shared.xcconfig"; sourceTree = "<group>"; }; EA1E286C22B665E8004AA304 /* SUSignatures.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SUSignatures.h; sourceTree = "<group>"; }; EA1E286D22B665E8004AA304 /* SUSignatures.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SUSignatures.m; sourceTree = "<group>"; }; EA1E287622B666EB004AA304 /* sign_update */ = {isa = PBXFileReference; explicitFileType = "compiled.mach-o.executable"; includeInIndex = 0; path = sign_update; sourceTree = BUILT_PRODUCTS_DIR; }; EA1E287822B666EB004AA304 /* main.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = main.swift; sourceTree = "<group>"; }; EA1E287F22B667D8004AA304 /* Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Bridging-Header.h"; sourceTree = "<group>"; }; F67C9B7B281410B600740813 /* hu */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = hu; path = hu.lproj/Sparkle.strings; sourceTree = "<group>"; }; F6D399502814387000A8C0DA /* UniformTypeIdentifiers.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = UniformTypeIdentifiers.framework; path = System/Library/Frameworks/UniformTypeIdentifiers.framework; sourceTree = SDKROOT; }; F6E11510281410D10003736C /* ar */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ar; path = ar.lproj/Sparkle.strings; sourceTree = "<group>"; }; F8761EB01ADC5068000C9034 /* SUCodeSigningVerifierTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SUCodeSigningVerifierTest.m; sourceTree = "<group>"; }; F8761EB21ADC50EB000C9034 /* SparkleTestCodeSignApp.zip */ = {isa = PBXFileReference; lastKnownFileType = archive.zip; path = SparkleTestCodeSignApp.zip; sourceTree = "<group>"; }; FA1941CA0D94A70100DD942E /* ConfigFrameworkDebug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = ConfigFrameworkDebug.xcconfig; sourceTree = "<group>"; }; FA1941CC0D94A70100DD942E /* ConfigCommonRelease.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = ConfigCommonRelease.xcconfig; sourceTree = "<group>"; }; FA1941CD0D94A70100DD942E /* ConfigTestApp.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = ConfigTestApp.xcconfig; sourceTree = "<group>"; }; FA1941CE0D94A70100DD942E /* ConfigRelaunch.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = ConfigRelaunch.xcconfig; sourceTree = "<group>"; }; FA1941CF0D94A70100DD942E /* ConfigCommonDebug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = ConfigCommonDebug.xcconfig; sourceTree = "<group>"; }; FA1941D00D94A70100DD942E /* ConfigCommon.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = ConfigCommon.xcconfig; sourceTree = "<group>"; }; FA1941D10D94A70100DD942E /* ConfigFramework.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = ConfigFramework.xcconfig; sourceTree = "<group>"; }; FA30773C24CBC295007BA37D /* testlocalizedreleasenotesappcast.xml */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xml; path = testlocalizedreleasenotesappcast.xml; sourceTree = "<group>"; }; FA30773E24CBC3E9007BA37D /* URL+Hashing.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "URL+Hashing.swift"; sourceTree = "<group>"; }; FA3AAF3B1050B273004B3130 /* ConfigUnitTest.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = ConfigUnitTest.xcconfig; sourceTree = "<group>"; }; FE5536F517A2C6A7007CB333 /* ko */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ko; path = ko.lproj/Sparkle.strings; sourceTree = "<group>"; }; FE5536F617A2C6AB007CB333 /* sk */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = sk; path = sk.lproj/Sparkle.strings; sourceTree = "<group>"; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ 5D06E8CE0FD68C7C005AE3F6 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( EA1E281922B645D9004AA304 /* libbsdiff.a in Frameworks */, 5D06E9050FD68D7D005AE3F6 /* Foundation.framework in Frameworks */, 725C2EAC2782EF3C007CB7B5 /* ArgumentParser in Frameworks */, 5D06E8FF0FD68D6D005AE3F6 /* libbz2.dylib in Frameworks */, 5D1AF58B0FD7678C0065DB48 /* libxar.1.dylib in Frameworks */, 5D1AF5900FD767AD0065DB48 /* libxml2.dylib in Frameworks */, 5D1AF59A0FD767E50065DB48 /* libz.dylib in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; 612279D60DB5470200AB99EA /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( 725B81FB2781CD930041746F /* libcompression.tbd in Frameworks */, 5A06357323FE333600478A72 /* libed25519.a in Frameworks */, EA1E281822B645CE004AA304 /* libbsdiff.a in Frameworks */, 14732BD119610A1200593899 /* AppKit.framework in Frameworks */, 14732BD019610A0D00593899 /* Foundation.framework in Frameworks */, 721CF1AB1AD764EB00D9AC09 /* libbz2.dylib in Frameworks */, 721CF1AA1AD7647000D9AC09 /* libxar.1.dylib in Frameworks */, 14652F8019A9740F00959E44 /* Security.framework in Frameworks */, 14732BD319610A1800593899 /* XCTest.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; 61B5F90009C4CEE200B25A18 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( 72F0EC4D278A5BCA002A876A /* libbsdiff.a in Frameworks */, 72F0EC4C278A5BB9002A876A /* libbz2.tbd in Frameworks */, 72F0EC4B278A5BB2002A876A /* libxar.tbd in Frameworks */, 5A06357423FE33A400478A72 /* libed25519.a in Frameworks */, 14950073195FCE4E00BC5B5B /* AppKit.framework in Frameworks */, 14950072195FCE4B00BC5B5B /* Foundation.framework in Frameworks */, 61B5F90F09C4CF3A00B25A18 /* Sparkle.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; 7205C43B1E13049400E370AE /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( EA1E285622B6619B004AA304 /* libed25519.a in Frameworks */, EA1E281A22B645F4004AA304 /* libbsdiff.a in Frameworks */, 7205C4641E1306BE00E370AE /* libbz2.tbd in Frameworks */, 727DBAE526B5BBFD00111F0C /* ArgumentParser in Frameworks */, 7205C4631E1306B500E370AE /* libxar.tbd in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; 721C24421CB753E6005440CB /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( 721C245A1CB75756005440CB /* Cocoa.framework in Frameworks */, 724BB3B71D35ABA8005D534A /* Security.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; 724BB3691D31D0B7005D534A /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( ); runOnlyForDeploymentPostprocessing = 0; }; 724BB3901D333832005D534A /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( ); runOnlyForDeploymentPostprocessing = 0; }; 726B2B5A1C645FC900388755 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( ); runOnlyForDeploymentPostprocessing = 0; }; 726E07AA1CAF08D6001A286B /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( 726BA5132E25F60000CE93C3 /* AppKit.framework in Frameworks */, 654F352D29B1551000B10EEB /* CoreServices.framework in Frameworks */, 654F352929B154BB00B10EEB /* ServiceManagement.framework in Frameworks */, 654F352829B154B500B10EEB /* SystemConfiguration.framework in Frameworks */, 654F352529B1548700B10EEB /* Security.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; 726E07EC1CAF37BD001A286B /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( 7290BF312E5A5B2F00D75022 /* Security.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; 726E4A131C86C88F00C57C6A /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( 726E4A301C87DC1700C57C6A /* Sparkle.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; 72B398CF1D3D879300EE297F /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( 7240852E2CA11C1400ED2FCD /* libz.tbd in Frameworks */, EA1E281722B645AE004AA304 /* libbsdiff.a in Frameworks */, 7267E5C91D3D8C4300D1BF90 /* CoreServices.framework in Frameworks */, 7267E5CA1D3D8C4800D1BF90 /* Foundation.framework in Frameworks */, 725B81FA2781AEAF0041746F /* libcompression.tbd in Frameworks */, 5A06357023FE332300478A72 /* libed25519.a in Frameworks */, 7267E5D61D3D8D3500D1BF90 /* libbz2.tbd in Frameworks */, 7267E5D51D3D8D2800D1BF90 /* libxar.tbd in Frameworks */, 721D8A611D48413B0032E472 /* Security.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; 72D9549B1CBB415B006F28BD /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( 724F76F91D6EAD0D00ECD062 /* Cocoa.framework in Frameworks */, 726F168626747CEB005BEA89 /* Sparkle.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; 8DC2EF560486A6940098B216 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( 72C56E062EFB45C80005A484 /* libed25519.a in Frameworks */, 725B81FC2781CDC20041746F /* libcompression.tbd in Frameworks */, 1495006F195FCE1800BC5B5B /* Foundation.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; EA1E282B22B660BE004AA304 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( ); runOnlyForDeploymentPostprocessing = 0; }; EA1E285B22B66487004AA304 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( 727DBAE726B5C47800111F0C /* ArgumentParser in Frameworks */, EA1E286A22B6653D004AA304 /* libed25519.a in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; EA1E287322B666EB004AA304 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( 727DBAE926B5C48A00111F0C /* ArgumentParser in Frameworks */, EA1E287E22B66705004AA304 /* libed25519.a in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ 034768DFFF38A50411DB9C8B /* Products */ = { isa = PBXGroup; children = ( 72B398D21D3D879300EE297F /* Autoupdate */, 5D06E8D00FD68C7C005AE3F6 /* BinaryDelta */, 7205C43E1E13049400E370AE /* generate_appcast */, 726E07EF1CAF37BD001A286B /* Downloader.xpc */, 724BB36C1D31D0B7005D534A /* InstallerConnection.xpc */, 726E07AD1CAF08D6001A286B /* Installer.xpc */, 724BB3931D333832005D534A /* InstallerStatus.xpc */, 61B5F90209C4CEE200B25A18 /* Sparkle Test App.app */, 612279D90DB5470200AB99EA /* Sparkle Unit Tests.xctest */, 72D9549E1CBB415B006F28BD /* sparkle.app */, 8DC2EF5B0486A6940098B216 /* Sparkle.framework */, 726E4A161C86C88F00C57C6A /* TestAppHelper.xpc */, 726B2B5D1C645FC900388755 /* UI Tests.xctest */, 721C24451CB753E6005440CB /* Updater.app */, EA1E280F22B64522004AA304 /* libbsdiff.a */, EA1E282D22B660BE004AA304 /* libed25519.a */, EA1E285E22B66487004AA304 /* generate_keys */, EA1E287622B666EB004AA304 /* sign_update */, ); name = Products; sourceTree = "<group>"; }; 0867D691FE84028FC02AAC07 /* Sparkle */ = { isa = PBXGroup; children = ( 723DFF962B3DFF6E00628E6C /* Package.swift */, 1495006D195FBBBD00BC5B5B /* Sparkle */, EA1E288122B6688A004AA304 /* Command Line Tools */, 14732BB81960EEB600593899 /* Resources */, 61227A100DB5484000AB99EA /* Tests */, 61B5F91D09C4CF7F00B25A18 /* Test Application */, 14732BB51960ECBA00593899 /* Vendor */, 1420DE391962322200203BB0 /* Documentation */, FA1941C40D94A6EA00DD942E /* Configurations */, 72563CA9272E1C5400AF39F0 /* .github */, 726B2B5E1C645FC900388755 /* UI Tests */, 0867D69AFE84028FC02AAC07 /* Frameworks */, 034768DFFF38A50411DB9C8B /* Products */, ); indentWidth = 4; name = Sparkle; sourceTree = "<group>"; tabWidth = 4; usesTabs = 0; }; 0867D69AFE84028FC02AAC07 /* Frameworks */ = { isa = PBXGroup; children = ( 7240852D2CA11C1400ED2FCD /* libz.tbd */, 654F352629B154AE00B10EEB /* ImageIO.framework */, 725B81F92781AEA40041746F /* libcompression.tbd */, 0867D6A5FE840307C02AAC07 /* AppKit.framework */, 721BC2081D17A553002BC71E /* Carbon.framework */, 525A278F133D6AE900FD8D70 /* Cocoa.framework */, 721BC2061D17A532002BC71E /* CoreFoundation.framework */, 721BC20A1D17A5AD002BC71E /* CoreServices.framework */, 0867D69BFE84028FC02AAC07 /* Foundation.framework */, 6117796E0D1112E000749C97 /* IOKit.framework */, 5D06E8FB0FD68D61005AE3F6 /* libbz2.dylib */, 726E076A1CA616B3001A286B /* libbz2.tbd */, 5D1AF5890FD7678C0065DB48 /* libxar.1.dylib */, 726E07681CA616A4001A286B /* libxar.tbd */, 5D1AF58F0FD767AD0065DB48 /* libxml2.dylib */, 5D1AF5990FD767E50065DB48 /* libz.dylib */, 61B5F8F609C4CEB300B25A18 /* Security.framework */, 7267E5CF1D3D8C8500D1BF90 /* SecurityFoundation.framework */, 724BB3B51D35AAC3005D534A /* ServiceManagement.framework */, 55C14F31136EFC2400649790 /* SystemConfiguration.framework */, F6D399502814387000A8C0DA /* UniformTypeIdentifiers.framework */, 61B5FC3F09C4FD4000B25A18 /* WebKit.framework */, 14732BD219610A1800593899 /* XCTest.framework */, ); name = Frameworks; sourceTree = "<group>"; }; 089C1665FE841158C02AAC07 /* Framework Resources */ = { isa = PBXGroup; children = ( 8DC2EF5A0486A6940098B216 /* Sparkle-Info.plist */, 61AAE8220A321A7F00D8810D /* Sparkle.strings */, 55C14BD8136EF00C00649790 /* SUStatus.xib */, 729C51272E58015600D7365A /* SUUpdatePermissionPrompt.xib */, 72FE54412E56A14100227A91 /* SUUpdateAlert.xib */, ); name = "Framework Resources"; sourceTree = "<group>"; }; 1420DE391962322200203BB0 /* Documentation */ = { isa = PBXGroup; children = ( 721D8A891D4D273E0032E472 /* Internal */, ); path = Documentation; sourceTree = "<group>"; }; 14732BB51960ECBA00593899 /* Vendor */ = { isa = PBXGroup; children = ( 14732BB61960ECE800593899 /* bsdiff */, EA1E283322B660ED004AA304 /* ed25519 */, ); path = Vendor; sourceTree = "<group>"; }; 14732BB61960ECE800593899 /* bsdiff */ = { isa = PBXGroup; children = ( 72B09CE91CEA18900052EF9E /* bscommon.c */, 72B09CEA1CEA18900052EF9E /* bscommon.h */, 5D06E8DB0FD68CB9005AE3F6 /* bsdiff.c */, 5D06E8DC0FD68CB9005AE3F6 /* bspatch.c */, 611142E810FB1BE5009810AA /* bspatch.h */, 7223E7611AD1AEFF008E3161 /* sais.c */, 7223E7621AD1AEFF008E3161 /* sais.h */, ); path = bsdiff; sourceTree = "<group>"; }; 14732BB81960EEB600593899 /* Resources */ = { isa = PBXGroup; children = ( 14732BC21960F3FF00593899 /* bin */, 14732BBA1960EF7100593899 /* CHANGELOG */, 61C268090E2DB5D000175E6C /* LICENSE */, 14732BC11960F3B200593899 /* Makefile */, 147D6D9E1B66DC3C006607AB /* CheckLocalizations.swift */, 14732BBC1960EFB500593899 /* README.markdown */, 14732BB91960EEEE00593899 /* SampleAppcast.xml */, 1EAA4C8A2132C7BF00604473 /* ReleaseNotesColorStyle.css */, 7230BCDD2E22E2BC00B71297 /* Images.xcassets */, ); path = Resources; sourceTree = "<group>"; }; 14732BC21960F3FF00593899 /* bin */ = { isa = PBXGroup; children = ( 14732BC31960F3FF00593899 /* generate_keys */, 14732BC41960F3FF00593899 /* sign_update */, ); path = bin; sourceTree = SOURCE_ROOT; }; 1495006D195FBBBD00BC5B5B /* Sparkle */ = { isa = PBXGroup; children = ( 720767D11E2EB86200F9A850 /* AppKitPrevention.h */, 72366CCC2DC2D1FC003CE62B /* Sparkle.private.modulemap */, 61299B3909CB055000B7442F /* Appcast Support */, 55C14BD5136EEFD000649790 /* Autoupdate */, 72F9EBE41D519425004AC8B6 /* Deprecated */, 089C1665FE841158C02AAC07 /* Framework Resources */, 721C24461CB753E6005440CB /* Installer Progress */, 729746481C91483B00FC134E /* IPC */, 61B5F8F309C4CE5900B25A18 /* Other Sources */, EA1E286B22B665DB004AA304 /* Signatures */, 61F83F6E0DBFE07A006FDD30 /* Update Control */, 61299B3A09CB056100B7442F /* User Interface */, 72F94F551CC43D99002DEE68 /* XPC Services */, ); path = Sparkle; sourceTree = "<group>"; }; 14958C7019AEBE350061B14F /* Resources */ = { isa = PBXGroup; children = ( 14958C6B19AEBC530061B14F /* signed-test-file.txt */, 72AC6B2D1B9B218C00F62325 /* SparkleTestCodeSignApp.dmg */, C23E88591BE7AF890050BB73 /* SparkleTestCodeSignApp.enc.dmg */, 72E6D9722C0526DC005496E4 /* SparkleTestCodeSignApp.enc.nolicense.dmg */, 72AC6B271B9AAD6700F62325 /* SparkleTestCodeSignApp.tar */, 72BC6C3C275027BF0083F14B /* SparkleTestCodeSign_apfs.dmg */, 725453542C04DB3700362123 /* SparkleTestCodeSign_apfs_lzma_aux_files_adhoc.dmg */, 72E6D9702C04DE19005496E4 /* SparkleTestCodeSign_pkg.dmg */, 72AC6B291B9AAF3A00F62325 /* SparkleTestCodeSignApp.tar.bz2 */, 72AC6B251B9AAC8800F62325 /* SparkleTestCodeSignApp.tar.gz */, 72AC6B2B1B9AB0EE00F62325 /* SparkleTestCodeSignApp.tar.xz */, 729F7ECD27409076004592DC /* SparkleTestCodeSignApp_bad_extraneous.zip */, 72CCDEBD27421FD500B53718 /* SparkleTestCodeSignApp_bad_header.zip */, F8761EB21ADC50EB000C9034 /* SparkleTestCodeSignApp.zip */, 726FC0352C1E787A00177986 /* SparkleTestCodeSignApp.aar */, 726FC0372C1E96AA00177986 /* SparkleTestCodeSignApp.enc.aar */, 72EB735E29BE981300FBCEE7 /* DevSignedApp.zip */, 72EB736029BEB36100FBCEE7 /* DevSignedAppVersion2.zip */, 726B20602CF4F1D300E6F7DB /* DevSignedAppVersion2.dmg */, 14958C6C19AEBC610061B14F /* test-pubkey.pem */, 5AF6C74E1AEA46D10014A3AB /* test.pkg */, 5AD0FA7E1C73F2E2004BCEFF /* testappcast.xml */, 723EDC3E26885A8E000BCBA4 /* testappcast_channels.xml */, 72526BE52EF3349D005791ED /* testappcast_minimumUpdateVersion.xml */, 725B3A81263FBF0C0041AB8E /* testappcast_minimumAutoupdateVersion.xml */, 720DC50327A51A6500DFF3EC /* testappcast_minimumAutoupdateVersionSkipping.xml */, 720DC50527A62CDC00DFF3EC /* testappcast_minimumAutoupdateVersionSkipping2.xml */, 723414162EBFB482009727E0 /* testappcast_arm64HardwareRequirement.xml */, 7202DC99269ABD3500737EC4 /* testappcast_phasedRollout.xml */, 722545B526805FF80036465C /* testappcast_info_updates.xml */, 5F1510A11C96E591006E1629 /* testnamespaces.xml */, FA30773C24CBC295007BA37D /* testlocalizedreleasenotesappcast.xml */, 5A5DD41B249F0F4B0045EB3E /* test-relative-urls.xml */, 729F7EAB27366353004592DC /* test-links.xml */, 723AD12E29922B5F006BB02F /* test-dangerous-link.xml */, 72C56E212F04C28B0005A484 /* testreleasenotes.html */, 5A5DD40324958AFF0045EB3E /* SUUpdateValidatorTest */, ); path = Resources; sourceTree = "<group>"; }; 55C14BD5136EEFD000649790 /* Autoupdate */ = { isa = PBXGroup; children = ( 618FA6DB0DB485440026945C /* Installation */, 6101354A0DD25B7F0049ACDF /* Unarchiving */, 61CFB2C10E384958007A1735 /* Verifiers */, 7267E5A61D3D8A9900D1BF90 /* AgentConnection.h */, 7267E5A71D3D8A9900D1BF90 /* AgentConnection.m */, 7267E5A01D3D8A7E00D1BF90 /* AppInstaller.h */, 7267E5A11D3D8A7E00D1BF90 /* AppInstaller.m */, 7267E59E1D3D8A6F00D1BF90 /* main.m */, 7267E5AC1D3D8AB700D1BF90 /* SPUInstallationInputData.h */, 7267E5AD1D3D8AB700D1BF90 /* SPUInstallationInputData.m */, 729924921DF4A45000DBCDF5 /* SUUpdateValidator.h */, 729924931DF4A45000DBCDF5 /* SUUpdateValidator.m */, ); path = Autoupdate; sourceTree = "<group>"; }; 5D06E8D90FD68C95005AE3F6 /* Binary Delta */ = { isa = PBXGroup; children = ( 7267E56E1D3D895B00D1BF90 /* SUBinaryDeltaApply.h */, 7267E56F1D3D895B00D1BF90 /* SUBinaryDeltaApply.m */, 7267E5701D3D895B00D1BF90 /* SUBinaryDeltaCommon.h */, 7267E5711D3D895B00D1BF90 /* SUBinaryDeltaCommon.m */, 7267E5721D3D895B00D1BF90 /* SUBinaryDeltaCreate.h */, 7267E5731D3D895B00D1BF90 /* SUBinaryDeltaCreate.m */, 725EE481277BF17A00D820CE /* SPUDeltaArchiveProtocol.h */, 725C2EAD278386A8007CB7B5 /* SPUDeltaCompressionMode.h */, 725EE484277D375F00D820CE /* SPUDeltaArchive.h */, 725EE485277D375F00D820CE /* SPUDeltaArchive.m */, 728ED348277DA23400D9238F /* SPUSparkleDeltaArchive.h */, 728ED349277DA23400D9238F /* SPUSparkleDeltaArchive.m */, 725EE47E277BF13A00D820CE /* SPUXarDeltaArchive.h */, 725EE47F277BF13B00D820CE /* SPUXarDeltaArchive.m */, 7267E57D1D3D896700D1BF90 /* SUBinaryDeltaUnarchiver.h */, 7267E57E1D3D896700D1BF90 /* SUBinaryDeltaUnarchiver.m */, 725C2EA92782EC61007CB7B5 /* main.swift */, 725C2EA82782EC44007CB7B5 /* Bridging-Header.h */, ); name = "Binary Delta"; sourceTree = "<group>"; }; 6101354A0DD25B7F0049ACDF /* Unarchiving */ = { isa = PBXGroup; children = ( 5D06E8D90FD68C95005AE3F6 /* Binary Delta */, 7267E5801D3D89B300D1BF90 /* SUDiskImageUnarchiver.h */, 7267E5811D3D89B300D1BF90 /* SUDiskImageUnarchiver.m */, 7267E5821D3D89B300D1BF90 /* SUPipedUnarchiver.h */, 7267E5831D3D89B300D1BF90 /* SUPipedUnarchiver.m */, 721D5A8325C65D3F00D23BEA /* SUFlatPackageUnarchiver.h */, 721D5A8425C65D3F00D23BEA /* SUFlatPackageUnarchiver.m */, 7267E5841D3D89B300D1BF90 /* SUUnarchiver.h */, 7267E5851D3D89B300D1BF90 /* SUUnarchiver.m */, 72316BD11E0DA8430039EFD9 /* SUUnarchiverNotifier.h */, 72316BD21E0DA8430039EFD9 /* SUUnarchiverNotifier.m */, 7267E5891D3D89CA00D1BF90 /* SUUnarchiverProtocol.h */, ); name = Unarchiving; path = ..; sourceTree = "<group>"; }; 61227A100DB5484000AB99EA /* Tests */ = { isa = PBXGroup; children = ( 14958C7019AEBE350061B14F /* Resources */, 72AFC6121B9A944200F6B565 /* Sparkle Unit Tests-Bridging-Header.h */, 612279DA0DB5470200AB99EA /* SparkleTests-Info.plist */, 5AA4DCD01C73E5510078F128 /* SUAppcastTest.swift */, 142E0E0819A83AAC00E4312B /* SUBinaryDeltaTest.m */, F8761EB01ADC5068000C9034 /* SUCodeSigningVerifierTest.m */, 5AF9DC3B1981DBEE001EA135 /* SUSignatureVerifierTest.m */, 72C56E1E2F04AC890005A484 /* SUFeedSignatureVerifierTest.swift */, 72A4A23F1BB6567D00E7820D /* SUFileManagerTest.swift */, 5AF6C74C1AEA40760014A3AB /* SUInstallerTest.m */, 72BEBFEE1D7287560019146B /* SUSpotlightImporterTest.swift */, 7210C7671B9A9A1500EB90AC /* SUUnarchiverTest.swift */, 5A5DD400249585E70045EB3E /* SUUpdateValidatorTest.swift */, 14950074195FDF5900BC5B5B /* SUUpdaterTest.m */, 61227A150DB548B800AB99EA /* SUVersionComparisonTest.m */, ); path = Tests; sourceTree = "<group>"; }; 61299B3909CB055000B7442F /* Appcast Support */ = { isa = PBXGroup; children = ( 72F9EC421D5E9ED8004AC8B6 /* SPUDownloadData.h */, 721AB11626C777D900D34A86 /* SPUDownloadDataPrivate.h */, 72F9EC431D5E9ED8004AC8B6 /* SPUDownloadData.m */, 7214B8851D45AD9A00CB5CED /* SPUInstallationType.h */, 72C56E182F04343D0005A484 /* SPUAppcastSigningValidationStatus.h */, 61B5FB9409C4F04600B25A18 /* SUAppcast.h */, 725DED72263D10C400E7FA8F /* SUAppcast+Private.h */, 61B5FB9509C4F04600B25A18 /* SUAppcast.m */, 61B5FC5309C5182000B25A18 /* SUAppcastItem.h */, 72EF30C6267C716A008CE987 /* SUAppcastItem+Private.h */, 61B5FC5409C5182000B25A18 /* SUAppcastItem.m */, 72EF30BD2675CF38008CE987 /* SPUAppcastItemState.h */, 72EF30BB2675CF38008CE987 /* SPUAppcastItemState.m */, 72EF30BA2675CF38008CE987 /* SPUAppcastItemStateResolver.h */, 722545B72680699D0036465C /* SPUAppcastItemStateResolver+Private.h */, 72EF30BC2675CF38008CE987 /* SPUAppcastItemStateResolver.m */, 721D588B25BE59F900D23BEA /* SUPhasedUpdateGroupInfo.h */, 721D588C25BE59F900D23BEA /* SUPhasedUpdateGroupInfo.m */, 61A225A20D1C4AC000430CCD /* SUStandardVersionComparator.h */, 61A225A30D1C4AC000430CCD /* SUStandardVersionComparator.m */, 61A2279A0D1CEE7600430CCD /* SUSystemProfiler.h */, 61A2279B0D1CEE7600430CCD /* SUSystemProfiler.m */, 61A2259C0D1C495D00430CCD /* SUVersionComparisonProtocol.h */, ); name = "Appcast Support"; sourceTree = "<group>"; }; 61299B3A09CB056100B7442F /* User Interface */ = { isa = PBXGroup; children = ( 725F97811C8A997300265BE4 /* User Driver */, 725602D31C83551C00DAA70E /* SUApplicationInfo.h */, 725602D41C83551C00DAA70E /* SUApplicationInfo.m */, 6196CFE309C71ADE000DC222 /* SUStatusController.h */, 6196CFE409C71ADE000DC222 /* SUStatusController.m */, 61B5FCA009C5228F00B25A18 /* SUUpdateAlert.h */, 61B5FCA109C5228F00B25A18 /* SUUpdateAlert.m */, 723AC036259DBF1B00BDB4FA /* Release Notes Views */, 612DCBAD0D488BC60015DBEA /* SUUpdatePermissionPrompt.h */, 612DCBAE0D488BC60015DBEA /* SUUpdatePermissionPrompt.m */, 723C8A521E2D60DB00C14942 /* SUTouchBarButtonGroup.h */, 723C8A531E2D60DB00C14942 /* SUTouchBarButtonGroup.m */, ); name = "User Interface"; sourceTree = "<group>"; }; 618FA6DB0DB485440026945C /* Installation */ = { isa = PBXGroup; children = ( 7267E5BA1D3D8B2700D1BF90 /* SUGuidedPackageInstaller.h */, 7267E5BB1D3D8B2700D1BF90 /* SUGuidedPackageInstaller.m */, 7267E5BC1D3D8B2700D1BF90 /* SUInstaller.h */, 7267E5BD1D3D8B2700D1BF90 /* SUInstaller.m */, 722FB7E3260EE53F00EB571C /* SUNormalization.h */, 722FB7E4260EE53F00EB571C /* SUNormalization.m */, 7267E5B91D3D8B0B00D1BF90 /* SUInstallerProtocol.h */, 7267E5C01D3D8B2700D1BF90 /* SUPlainInstaller.h */, 7267E5C11D3D8B2700D1BF90 /* SUPlainInstaller.m */, ); name = Installation; path = ..; sourceTree = "<group>"; }; 61B5F8F309C4CE5900B25A18 /* Other Sources */ = { isa = PBXGroup; children = ( 61299B3509CB04E000B7442F /* Sparkle.h */, 721BC20C1D1CDE55002BC71E /* SPULocalCacheDirectory.h */, 721BC20D1D1CDE55002BC71E /* SPULocalCacheDirectory.m */, 729F7EAD273F1840004592DC /* SPUUserAgent+Private.h */, 729F7EAE273F1840004592DC /* SPUUserAgent+Private.m */, 61299A5B09CA6D4500B7442F /* SUConstants.h */, 61299A5F09CA6EB100B7442F /* SUConstants.m */, 55E6F33219EC9F6C00005E76 /* SUErrors.h */, 14652F8319A9759F00959E44 /* SUExport.h */, 7267E5E31D3D90AA00D1BF90 /* SUFileManager.h */, 7267E5E41D3D90AA00D1BF90 /* SUFileManager.m */, 61EF67580E25C5B400F754E0 /* SUHost.h */, 61EF67550E25B58D00F754E0 /* SUHost.m */, 7269E492264798200088C213 /* SPUSkippedUpdate.h */, 7269E493264798200088C213 /* SPUSkippedUpdate.m */, 72162B071C82C9600013C1C5 /* SULocalizations.h */, 55C14F04136EF6DB00649790 /* SULog.h */, 55C14F05136EF6DB00649790 /* SULog.m */, 727F34212605323500020E85 /* SULog+NSError.h */, 727F340A2605321D00020E85 /* SULog+NSError.m */, 726F2CE31BC9C33D001971A4 /* SUOperatingSystem.h */, 726F2CE41BC9C33D001971A4 /* SUOperatingSystem.m */, 3772FEA813DE0B6B00F79537 /* SUVersionDisplayProtocol.h */, 72AEB1D229A1A74E0033883E /* SPUStandardVersionDisplay.h */, 72AEB1D329A1A74E0033883E /* SPUStandardVersionDisplay.m */, 72AEB1D629A1CB510033883E /* SPUNoUpdateFoundInfo.h */, 72AEB1D729A1CB510033883E /* SPUNoUpdateFoundInfo.m */, ); includeInIndex = 1; name = "Other Sources"; sourceTree = "<group>"; }; 61B5F91D09C4CF7F00B25A18 /* Test Application */ = { isa = PBXGroup; children = ( 726E4A171C86C88F00C57C6A /* TestAppHelper */, 61B5F92409C4CFC900B25A18 /* main.m */, 723ABDD9259A9E8600BDB4FA /* InfoPlist.strings */, 723ABE00259A9E9E00BDB4FA /* MainMenu.xib */, 726E4A111C86C44A00C57C6A /* Sparkle-Test-App.entitlements */, 72E45CFB1B641961005C701A /* sparkletestcast.xml */, 725F97741C8A62F500265BE4 /* SUAdHocCodeSigning.h */, 725F97751C8A62F500265BE4 /* SUAdHocCodeSigning.m */, 725F97A21C8B304D00265BE4 /* SUInstallUpdateViewController.h */, 725F97A31C8B304D00265BE4 /* SUInstallUpdateViewController.m */, 725F97A41C8B304D00265BE4 /* SUInstallUpdateViewController.xib */, 725F97821C8AA90000265BE4 /* SUPopUpTitlebarUserDriver.h */, 725F97831C8AA90000265BE4 /* SUPopUpTitlebarUserDriver.m */, 72E45CF11B640CDD005C701A /* SUTestApplicationDelegate.h */, 72E45CF21B640CDD005C701A /* SUTestApplicationDelegate.m */, A5BF4F1B1BC7668B007A052A /* SUTestWebServer.h */, A5BF4F1C1BC7668B007A052A /* SUTestWebServer.m */, 72E45CF41B640DAE005C701A /* SUUpdateSettingsWindowController.h */, 72E45CF51B640DAE005C701A /* SUUpdateSettingsWindowController.m */, 72E45CF61B640DAE005C701A /* SUUpdateSettingsWindowController.xib */, 61B5F90409C4CEE200B25A18 /* TestApplication-Info.plist */, 72F0EC44278A55CA002A876A /* screenshot.png */, ); name = "Test Application"; path = TestApplication; sourceTree = "<group>"; }; 61CFB2C10E384958007A1735 /* Verifiers */ = { isa = PBXGroup; children = ( 7267E5981D3D8A5A00D1BF90 /* SUCodeSigningVerifier.h */, 7267E5991D3D8A5A00D1BF90 /* SUCodeSigningVerifier.m */, 7267E59A1D3D8A5A00D1BF90 /* SUSignatureVerifier.h */, 7267E59B1D3D8A5A00D1BF90 /* SUSignatureVerifier.m */, 72C56E072EFDFB850005A484 /* SPUExtractSignedFeed.h */, 72C56E082EFDFB850005A484 /* SPUExtractSignedFeed.m */, 72D04F3B2B094C8400A6DEAA /* SPUVerifierInformation.h */, 72D04F3C2B094C8400A6DEAA /* SPUVerifierInformation.m */, ); name = Verifiers; path = ..; sourceTree = "<group>"; }; 61CFB2C20E38496B007A1735 /* Drivers */ = { isa = PBXGroup; children = ( 72B767E81C9D114F00A07552 /* Update Drivers */, 72B767D41C9C8B5C00A07552 /* SPUBasicUpdateDriver.h */, 72B767D51C9C8B5C00A07552 /* SPUBasicUpdateDriver.m */, 72B767E01C9CE90A00A07552 /* SPUCoreBasedUpdateDriver.h */, 72B767E11C9CE90A00A07552 /* SPUCoreBasedUpdateDriver.m */, 7229E1BB1C98EFF200CB50D0 /* SPUDownloadDriver.h */, 7229E1BC1C98EFF200CB50D0 /* SPUDownloadDriver.m */, 7267E5FB1D3DD1B700D1BF90 /* SPUResumableUpdate.h */, 72B3DEC71E23472200457642 /* SPUDownloadedUpdate.h */, 72B3DEC81E23472200457642 /* SPUDownloadedUpdate.m */, 72B3DECB1E23479000457642 /* SPUInformationalUpdate.h */, 72B3DECC1E23479000457642 /* SPUInformationalUpdate.m */, 72B767CC1C9B924900A07552 /* SPUInstallerDriver.h */, 72B767CD1C9B924900A07552 /* SPUInstallerDriver.m */, 728337A41C9E6FF40085AA99 /* SPUProbeInstallStatus.h */, 728337A51C9E6FF40085AA99 /* SPUProbeInstallStatus.m */, 72B767D81C9CD2E400A07552 /* SPUUIBasedUpdateDriver.h */, 72B767D91C9CD2E400A07552 /* SPUUIBasedUpdateDriver.m */, 72B767C81C9B707000A07552 /* SUAppcastDriver.h */, 72B767C91C9B707000A07552 /* SUAppcastDriver.m */, ); name = Drivers; sourceTree = "<group>"; }; 61F83F6E0DBFE07A006FDD30 /* Update Control */ = { isa = PBXGroup; children = ( 61CFB2C20E38496B007A1735 /* Drivers */, 72DBA37B1D60CC34002594A8 /* SPUUpdatePermissionRequest.h */, 72DBA37C1D60CC34002594A8 /* SPUUpdatePermissionRequest.m */, 61B5F8E309C4CE3C00B25A18 /* SPUUpdater.h */, 61B5F8E409C4CE3C00B25A18 /* SPUUpdater.m */, 720E21791D0D00BF003A311C /* SPUUpdaterCycle.h */, 720E217A1D0D00BF003A311C /* SPUUpdaterCycle.m */, 72F9EC471D5EA7D3004AC8B6 /* SPUUpdaterDelegate.h */, 72EE181826DAE6E100C58B19 /* SPUUpdateCheck.h */, 726E078B1CA891E9001A286B /* SPUUpdaterSettings.h */, 7245495B2ECA648000ABA991 /* SPUUpdaterSettings+Debug.h */, 726E078C1CA891E9001A286B /* SPUUpdaterSettings.m */, 72F9EC3D1D5E823F004AC8B6 /* SPUUpdaterTimer.h */, 72F9EC3E1D5E823F004AC8B6 /* SPUUpdaterTimer.m */, 72A450511C69A68900D67EEA /* SUUpdatePermissionResponse.h */, 72A450521C69A68900D67EEA /* SUUpdatePermissionResponse.m */, ); name = "Update Control"; sourceTree = "<group>"; }; 7205C43F1E13049400E370AE /* generate_appcast */ = { isa = PBXGroup; children = ( 7205C4461E1304C300E370AE /* Bridging-Header.h */, 7205C4471E1304CE00E370AE /* Appcast.swift */, 7205C4481E1304CE00E370AE /* ArchiveItem.swift */, 7205C44A1E1304CE00E370AE /* FeedXML.swift */, 7205C4401E13049400E370AE /* main.swift */, 7205C44B1E1304CE00E370AE /* Unarchive.swift */, FA30773E24CBC3E9007BA37D /* URL+Hashing.swift */, ); path = generate_appcast; sourceTree = "<group>"; }; 721C24461CB753E6005440CB /* Installer Progress */ = { isa = PBXGroup; children = ( 721C24581CB7567D005440CB /* InstallerProgress-Info.plist */, 72D9547F1CBACC35006F28BD /* InstallerProgressAppController.h */, 72D954801CBACC35006F28BD /* InstallerProgressAppController.m */, 72D954851CBAD4AE006F28BD /* InstallerProgressDelegate.h */, 72D954821CBAD34F006F28BD /* main.m */, 72D954841CBAD418006F28BD /* ShowInstallerProgress.h */, 72EB87E91CB8798800C37F42 /* ShowInstallerProgress.m */, 722194511D3BF987004C34FF /* SPUInstallerAgentProtocol.h */, 7267E5A31D3D8A8A00D1BF90 /* StatusInfo.h */, 7267E5A41D3D8A8A00D1BF90 /* StatusInfo.m */, 722194521D3BFEB7004C34FF /* SUInstallerAgentInitiationProtocol.h */, ); name = "Installer Progress"; path = InstallerProgress; sourceTree = "<group>"; }; 721D8A891D4D273E0032E472 /* Internal */ = { isa = PBXGroup; children = ( 722589B61E0A24C6005EA0B9 /* Design Practices.md */, 721D8A881D4D272E0032E472 /* Installation.md */, 722589B51E0A24B9005EA0B9 /* Security.md */, ); name = Internal; sourceTree = "<group>"; }; 723AC036259DBF1B00BDB4FA /* Release Notes Views */ = { isa = PBXGroup; children = ( 723ABF1A259D055E00BDB4FA /* SUReleaseNotesView.h */, 723AC00E259DBDAA00BDB4FA /* SUReleaseNotesCommon.h */, 723AC00F259DBDAA00BDB4FA /* SUReleaseNotesCommon.m */, 723ABFC2259D4CB300BDB4FA /* SUWKWebView.h */, 723ABFC3259D4CB300BDB4FA /* SUWKWebView.m */, 723ABF2E259D062F00BDB4FA /* SULegacyWebView.h */, 723ABF2F259D062F00BDB4FA /* SULegacyWebView.m */, 7286EE5D28CEC84900163C1D /* SUTextViewReleaseNotesView.h */, 7286EE5E28CEC84900163C1D /* SUTextViewReleaseNotesView.m */, ); name = "Release Notes Views"; sourceTree = "<group>"; }; 724BB36D1D31D0B7005D534A /* InstallerConnection */ = { isa = PBXGroup; children = ( 724BB3741D31D0B7005D534A /* Info.plist */, 724BB3721D31D0B7005D534A /* main.m */, 724BB3801D31F186005D534A /* SUInstallerCommunicationProtocol.h */, 724BB36F1D31D0B7005D534A /* SUInstallerConnection.h */, 724BB3701D31D0B7005D534A /* SUInstallerConnection.m */, 724BB36E1D31D0B7005D534A /* SUInstallerConnectionProtocol.h */, 724BB3851D32A167005D534A /* SUXPCInstallerConnection.h */, 724BB3861D32A167005D534A /* SUXPCInstallerConnection.m */, ); name = InstallerConnection; path = ../InstallerConnection; sourceTree = "<group>"; }; 724BB3941D333832005D534A /* InstallerStatus */ = { isa = PBXGroup; children = ( 724BB39B1D333832005D534A /* Info.plist */, 724BB3991D333832005D534A /* main.m */, 724BB3961D333832005D534A /* SUInstallerStatus.h */, 724BB3971D333832005D534A /* SUInstallerStatus.m */, 724BB3951D333832005D534A /* SUInstallerStatusProtocol.h */, 7267E5DD1D3D8F5A00D1BF90 /* SUStatusInfoProtocol.h */, 724BB3A61D33461B005D534A /* SUXPCInstallerStatus.h */, 724BB3A71D33461B005D534A /* SUXPCInstallerStatus.m */, ); name = InstallerStatus; path = ../InstallerStatus; sourceTree = "<group>"; }; 725F97811C8A997300265BE4 /* User Driver */ = { isa = PBXGroup; children = ( 7246E0A11C83B685003B4E75 /* SPUStandardUpdaterController.h */, 7246E0A21C83B685003B4E75 /* SPUStandardUpdaterController.m */, 725CB9581C7121830064365A /* SPUStandardUserDriver.h */, 72D60CD828C2BA2100189AB8 /* SPUStandardUserDriver+Private.h */, 725CB9591C7121830064365A /* SPUStandardUserDriver.m */, 726E4A361C89116000C57C6A /* SPUStandardUserDriverDelegate.h */, 722C9538286EA54A0033908A /* SPUGentleUserDriverReminders.h */, 726DF88D1C84277500188804 /* SPUUserUpdateState.h */, 7269E49C2648FC6C0088C213 /* SPUUserUpdateState+Private.h */, 7269E4992648F7C00088C213 /* SPUUserUpdateState.m */, 725CB9561C7120410064365A /* SPUUserDriver.h */, ); name = "User Driver"; sourceTree = "<group>"; }; 72666DC42B0B28D4001511B0 /* common_cli */ = { isa = PBXGroup; children = ( 72666DC52B0B28F4001511B0 /* Secret.swift */, 72C56E0F2EFEF10B0005A484 /* Signing.swift */, ); path = common_cli; sourceTree = "<group>"; }; 726B2B5E1C645FC900388755 /* UI Tests */ = { isa = PBXGroup; children = ( 720B16431C66433D006985FB /* SUTestApplicationTest.swift */, 720B16421C66433D006985FB /* UITests-Info.plist */, ); name = "UI Tests"; path = UITests; sourceTree = "<group>"; }; 726E07AE1CAF08D6001A286B /* InstallerLauncher */ = { isa = PBXGroup; children = ( 726E07B51CAF08D6001A286B /* Info.plist */, 726E07B31CAF08D6001A286B /* main.m */, 726E07B01CAF08D6001A286B /* SUInstallerLauncher.h */, 72EE17FA26D1CBC000C58B19 /* SUInstallerLauncher+Private.h */, 726E07B11CAF08D6001A286B /* SUInstallerLauncher.m */, 726E07AF1CAF08D6001A286B /* SUInstallerLauncherProtocol.h */, 721652671D3C8FED00FD13D8 /* SUInstallerLauncherStatus.h */, ); name = InstallerLauncher; path = ../InstallerLauncher; sourceTree = "<group>"; }; 726E07F01CAF37BD001A286B /* Downloader */ = { isa = PBXGroup; children = ( 723B5D9F1CF7AB0100365F95 /* Info.plist */, 723B5DA01CF7AB0100365F95 /* main.m */, 729BB3D11D503826007C4276 /* Downloader.entitlements */, 723B5DA21CF7AB0100365F95 /* SPUDownloader.h */, 723B5DA31CF7AB0100365F95 /* SPUDownloader.m */, 723B5DA41CF7AB0100365F95 /* SPUDownloaderDelegate.h */, 723B5DA51CF7AB0100365F95 /* SPUDownloaderProtocol.h */, ); name = Downloader; path = ../Downloader; sourceTree = "<group>"; }; 726E4A171C86C88F00C57C6A /* TestAppHelper */ = { isa = PBXGroup; children = ( 726E4A1E1C86C88F00C57C6A /* Info.plist */, 726E4A1C1C86C88F00C57C6A /* main.m */, 726E4A191C86C88F00C57C6A /* TestAppHelper.h */, 726E4A1A1C86C88F00C57C6A /* TestAppHelper.m */, 726E4A181C86C88F00C57C6A /* TestAppHelperProtocol.h */, ); name = TestAppHelper; path = ../TestAppHelper; sourceTree = "<group>"; }; 729746481C91483B00FC134E /* IPC */ = { isa = PBXGroup; children = ( 7267E5B41D3D8AEE00D1BF90 /* SPUInstallationInfo.h */, 7267E5B51D3D8AEE00D1BF90 /* SPUInstallationInfo.m */, 7267E5AF1D3D8AD500D1BF90 /* SPUMessageTypes.h */, 7267E5B01D3D8AD500D1BF90 /* SPUMessageTypes.m */, 726E075A1CA3A6D6001A286B /* SPUSecureCoding.h */, 726E075B1CA3A6D6001A286B /* SPUSecureCoding.m */, ); name = IPC; path = Autoupdate; sourceTree = "<group>"; }; 72B767E81C9D114F00A07552 /* Update Drivers */ = { isa = PBXGroup; children = ( 7229E1B51C97C91100CB50D0 /* SPUUpdateDriver.h */, 72B767E41C9CFD7200A07552 /* SPUAutomaticUpdateDriver.h */, 72B767E51C9CFD7200A07552 /* SPUAutomaticUpdateDriver.m */, 72B767D01C9C7B9300A07552 /* SPUProbingUpdateDriver.h */, 72B767D11C9C7B9300A07552 /* SPUProbingUpdateDriver.m */, 7229E1B71C97CC4D00CB50D0 /* SPUScheduledUpdateDriver.h */, 7229E1B81C97CC4D00CB50D0 /* SPUScheduledUpdateDriver.m */, 72B767DC1C9CDB9700A07552 /* SPUUserInitiatedUpdateDriver.h */, 72B767DD1C9CDB9700A07552 /* SPUUserInitiatedUpdateDriver.m */, ); name = "Update Drivers"; sourceTree = "<group>"; }; 72D9549F1CBB415C006F28BD /* sparkle-cli */ = { isa = PBXGroup; children = ( 72D954AB1CBB415C006F28BD /* Info.plist */, 72D954A41CBB415C006F28BD /* main.m */, 72D954A01CBB415C006F28BD /* SPUCommandLineDriver.h */, 72D954A11CBB415C006F28BD /* SPUCommandLineDriver.m */, 72D954B61CBB467F006F28BD /* SPUCommandLineUserDriver.h */, 72D954B71CBB467F006F28BD /* SPUCommandLineUserDriver.m */, ); path = "sparkle-cli"; sourceTree = "<group>"; }; 72F94F551CC43D99002DEE68 /* XPC Services */ = { isa = PBXGroup; children = ( 726E07F01CAF37BD001A286B /* Downloader */, 724BB36D1D31D0B7005D534A /* InstallerConnection */, 726E07AE1CAF08D6001A286B /* InstallerLauncher */, 724BB3941D333832005D534A /* InstallerStatus */, 72F94F561CC44DE1002DEE68 /* SPUXPCServiceInfo.h */, 72F94F571CC44DE1002DEE68 /* SPUXPCServiceInfo.m */, ); name = "XPC Services"; sourceTree = "<group>"; }; 72F9EBE41D519425004AC8B6 /* Deprecated */ = { isa = PBXGroup; children = ( 72F9EBE01D517E2F004AC8B6 /* SUUpdater.h */, 72F9EBE11D517E2F004AC8B6 /* SUUpdater.m */, 72A6F9891C94E2D6005F404C /* SUUpdaterDelegate.h */, ); name = Deprecated; sourceTree = "<group>"; }; EA1E283322B660ED004AA304 /* ed25519 */ = { isa = PBXGroup; children = ( EA1E283422B660ED004AA304 /* precomp_data.h */, EA1E283522B660ED004AA304 /* seed.c */, EA1E283622B660ED004AA304 /* fe.c */, EA1E283722B660ED004AA304 /* verify.c */, EA1E283822B660ED004AA304 /* ge.c */, EA1E283922B660ED004AA304 /* sc.c */, EA1E283A22B660ED004AA304 /* sign.c */, EA1E283B22B660ED004AA304 /* sha512.h */, EA1E283C22B660ED004AA304 /* key_exchange.c */, EA1E283D22B660ED004AA304 /* add_scalar.c */, EA1E283E22B660ED004AA304 /* ed25519.h */, EA1E283F22B660ED004AA304 /* fe.h */, EA1E284022B660ED004AA304 /* keypair.c */, EA1E284122B660ED004AA304 /* fixedint.h */, EA1E284222B660ED004AA304 /* sha512.c */, EA1E284322B660ED004AA304 /* sc.h */, EA1E284422B660ED004AA304 /* ge.h */, ); name = ed25519; path = "ed25519-sparkle/src"; sourceTree = "<group>"; }; EA1E285F22B66487004AA304 /* generate_keys */ = { isa = PBXGroup; children = ( EA1E286622B664A5004AA304 /* Bridging-Header.h */, EA1E286022B66487004AA304 /* main.swift */, ); path = generate_keys; sourceTree = "<group>"; }; EA1E286B22B665DB004AA304 /* Signatures */ = { isa = PBXGroup; children = ( EA1E286C22B665E8004AA304 /* SUSignatures.h */, EA1E286D22B665E8004AA304 /* SUSignatures.m */, ); name = Signatures; sourceTree = "<group>"; }; EA1E287722B666EB004AA304 /* sign_update */ = { isa = PBXGroup; children = ( EA1E287F22B667D8004AA304 /* Bridging-Header.h */, EA1E287822B666EB004AA304 /* main.swift */, ); path = sign_update; sourceTree = "<group>"; }; EA1E288122B6688A004AA304 /* Command Line Tools */ = { isa = PBXGroup; children = ( 72666DC42B0B28D4001511B0 /* common_cli */, 7205C43F1E13049400E370AE /* generate_appcast */, EA1E285F22B66487004AA304 /* generate_keys */, EA1E287722B666EB004AA304 /* sign_update */, 72D9549F1CBB415C006F28BD /* sparkle-cli */, ); name = "Command Line Tools"; sourceTree = "<group>"; }; FA1941C40D94A6EA00DD942E /* Configurations */ = { isa = PBXGroup; children = ( 14732BB1195FF6B700593899 /* .clang-format */, EA1E281422B64548004AA304 /* bsdiff-Debug.xcconfig */, EA1E281522B64549004AA304 /* bsdiff-Release.xcconfig */, EA1E281622B64549004AA304 /* bsdiff-Shared.xcconfig */, EA1E286822B664FD004AA304 /* CommandLineTool-Debug.xcconfig */, EA1E286722B664FD004AA304 /* CommandLineTool-Release.xcconfig */, EA1E286922B664FD004AA304 /* CommandLineTool-Shared.xcconfig */, FA1941D00D94A70100DD942E /* ConfigCommon.xcconfig */, 149B78631B7D3A0C00D7D62C /* ConfigCommonCoverage.xcconfig */, FA1941CF0D94A70100DD942E /* ConfigCommonDebug.xcconfig */, FA1941CC0D94A70100DD942E /* ConfigCommonRelease.xcconfig */, 728638EE1CAF589C00783084 /* ConfigDownloader.xcconfig */, 72F94ED61CC344A7002DEE68 /* ConfigDownloaderDebug.xcconfig */, FA1941D10D94A70100DD942E /* ConfigFramework.xcconfig */, FA1941CA0D94A70100DD942E /* ConfigFrameworkDebug.xcconfig */, 72A40F082956120D007C7DD5 /* ConfigFrameworkRelease.xcconfig */, 724BB37E1D31D1EA005D534A /* ConfigInstallerConnection.xcconfig */, 724BB37F1D31D1EA005D534A /* ConfigInstallerConnectionDebug.xcconfig */, 726E07C11CAF1E79001A286B /* ConfigInstallerLauncher.xcconfig */, 72F94ED51CC3441A002DEE68 /* ConfigInstallerLauncherDebug.xcconfig */, 721C24571CB754E8005440CB /* ConfigInstallerProgress.xcconfig */, 724BB3A41D3338C8005D534A /* ConfigInstallerStatus.xcconfig */, 724BB3A51D3338C8005D534A /* ConfigInstallerStatusDebug.xcconfig */, FA1941CE0D94A70100DD942E /* ConfigRelaunch.xcconfig */, 72D954B01CBB41E2006F28BD /* ConfigSparkleTool.xcconfig */, 722FB7CA1DD69897001D40CE /* ConfigSwift.xcconfig */, 7205C4511E13053500E370AE /* ConfigSwiftDebug.xcconfig */, 7205C4521E13053500E370AE /* ConfigSwiftRelease.xcconfig */, FA1941CD0D94A70100DD942E /* ConfigTestApp.xcconfig */, 72F94EDA1CC36C37002DEE68 /* ConfigTestAppDebug.xcconfig */, 726E4A281C86CAB500C57C6A /* ConfigTestAppHelper.xcconfig */, 72F94EDB1CC36C6F002DEE68 /* ConfigTestAppHelperDebug.xcconfig */, 729F10FD1C65A9B500DFCCC5 /* ConfigUITest.xcconfig */, 729F10FE1C65A9B500DFCCC5 /* ConfigUITestCoverage.xcconfig */, 7205C46E1E13254500E370AE /* ConfigUITestDebug.xcconfig */, 7205C46F1E13255900E370AE /* ConfigUITestRelease.xcconfig */, FA3AAF3B1050B273004B3130 /* ConfigUnitTest.xcconfig */, 149B78641B7D3A4800D7D62C /* ConfigUnitTestCoverage.xcconfig */, 7205C46C1E13244800E370AE /* ConfigUnitTestDebug.xcconfig */, 7205C46D1E13245600E370AE /* ConfigUnitTestRelease.xcconfig */, EA1E285822B6622E004AA304 /* ed25519-Debug.xcconfig */, EA1E285722B6622E004AA304 /* ed25519-Release.xcconfig */, EA1E285922B6622E004AA304 /* ed25519-Shared.xcconfig */, 14732BC91960F70A00593899 /* make-release-package.sh */, 7235B75E29DFC0120081FE4E /* make-xcframework.sh */, 14652F7919A93E5F00959E44 /* set-git-version-info.sh */, 72045CE426FEE708004F96E5 /* strip-framework.sh */, 720C192827127A3800740C8E /* release-move-tag.sh */, 720B4C2325EBFAFD005A0592 /* link-tools.sh */, 146EC84E19A68CF8004A50C5 /* Sparkle.podspec */, ); path = Configurations; sourceTree = "<group>"; }; /* End PBXGroup section */ /* Begin PBXHeadersBuildPhase section */ 8DC2EF500486A6940098B216 /* Headers */ = { isa = PBXHeadersBuildPhase; buildActionMask = 2147483647; files = ( 61299B3609CB04E000B7442F /* Sparkle.h in Headers */, 72B767E61C9CFD7200A07552 /* SPUAutomaticUpdateDriver.h in Headers */, 72B767D61C9C8B5C00A07552 /* SPUBasicUpdateDriver.h in Headers */, 72B767E21C9CE90A00A07552 /* SPUCoreBasedUpdateDriver.h in Headers */, 72F9EC441D5E9ED8004AC8B6 /* SPUDownloadData.h in Headers */, 72EF30C12675CF38008CE987 /* SPUAppcastItemState.h in Headers */, 72AEB1D429A1A74E0033883E /* SPUStandardVersionDisplay.h in Headers */, 7229E1BD1C98EFF200CB50D0 /* SPUDownloadDriver.h in Headers */, 7267E5FD1D3DD1B700D1BF90 /* SPUResumableUpdate.h in Headers */, 7267E5B61D3D8AEE00D1BF90 /* SPUInstallationInfo.h in Headers */, 7245495C2ECA648000ABA991 /* SPUUpdaterSettings+Debug.h in Headers */, 72EE181926DAE70900C58B19 /* SPUUpdateCheck.h in Headers */, 72C56E192F04343D0005A484 /* SPUAppcastSigningValidationStatus.h in Headers */, 72B767CE1C9B924900A07552 /* SPUInstallerDriver.h in Headers */, 723ABF30259D062F00BDB4FA /* SULegacyWebView.h in Headers */, 721BC20E1D1CDE55002BC71E /* SPULocalCacheDirectory.h in Headers */, 7267E5B11D3D8AD500D1BF90 /* SPUMessageTypes.h in Headers */, 728337A61C9E6FF40085AA99 /* SPUProbeInstallStatus.h in Headers */, 7286EE5F28CEC84900163C1D /* SUTextViewReleaseNotesView.h in Headers */, 723C8A541E2D60DB00C14942 /* SUTouchBarButtonGroup.h in Headers */, 72B767D21C9C7B9300A07552 /* SPUProbingUpdateDriver.h in Headers */, 7229E1B91C97CC4D00CB50D0 /* SPUScheduledUpdateDriver.h in Headers */, 726E075C1CA3A6D6001A286B /* SPUSecureCoding.h in Headers */, 7246E0A31C83B685003B4E75 /* SPUStandardUpdaterController.h in Headers */, 725CB95A1C7121830064365A /* SPUStandardUserDriver.h in Headers */, 726E4A371C89116000C57C6A /* SPUStandardUserDriverDelegate.h in Headers */, 72D60CD928C2BAE900189AB8 /* SPUStandardUserDriver+Private.h in Headers */, 722C9539286EA68C0033908A /* SPUGentleUserDriverReminders.h in Headers */, 729F7EAF273F1840004592DC /* SPUUserAgent+Private.h in Headers */, 72EE17FC26D1CC9D00C58B19 /* SPUInstallationType.h in Headers */, 72EF30BE2675CF38008CE987 /* SPUAppcastItemStateResolver.h in Headers */, 72EF30C7267C716A008CE987 /* SUAppcastItem+Private.h in Headers */, 72EE17FB26D1CC8800C58B19 /* SUInstallerLauncher+Private.h in Headers */, 72C56E0C2EFDFB850005A484 /* SPUExtractSignedFeed.h in Headers */, 61299A5C09CA6D4500B7442F /* SUConstants.h in Headers */, 726DF88E1C84277600188804 /* SPUUserUpdateState.h in Headers */, 72B767DA1C9CD2E400A07552 /* SPUUIBasedUpdateDriver.h in Headers */, 7229E1B61C97C91100CB50D0 /* SPUUpdateDriver.h in Headers */, 72DBA37D1D60CC34002594A8 /* SPUUpdatePermissionRequest.h in Headers */, 61B5F8ED09C4CE3C00B25A18 /* SPUUpdater.h in Headers */, 720E217B1D0D00BF003A311C /* SPUUpdaterCycle.h in Headers */, 72F9EC481D5EA904004AC8B6 /* SPUUpdaterDelegate.h in Headers */, 726E078D1CA891E9001A286B /* SPUUpdaterSettings.h in Headers */, 72F9EC3F1D5E823F004AC8B6 /* SPUUpdaterTimer.h in Headers */, 7269E494264798200088C213 /* SPUSkippedUpdate.h in Headers */, 723ABFC4259D4CB300BDB4FA /* SUWKWebView.h in Headers */, 72AEB1D829A1CB510033883E /* SPUNoUpdateFoundInfo.h in Headers */, 725CB9571C7120410064365A /* SPUUserDriver.h in Headers */, 72B767DE1C9CDB9700A07552 /* SPUUserInitiatedUpdateDriver.h in Headers */, 72F94F581CC44DE1002DEE68 /* SPUXPCServiceInfo.h in Headers */, 61B5FC0D09C4FC8200B25A18 /* SUAppcast.h in Headers */, 72B767CA1C9B707000A07552 /* SUAppcastDriver.h in Headers */, 61B5FC7009C51F4A00B25A18 /* SUAppcastItem.h in Headers */, 725602D51C83551C00DAA70E /* SUApplicationInfo.h in Headers */, 55E6F33319EC9F6C00005E76 /* SUErrors.h in Headers */, 14652F8419A978C200959E44 /* SUExport.h in Headers */, 7267E5E51D3D90AA00D1BF90 /* SUFileManager.h in Headers */, 72B3DEC91E23472200457642 /* SPUDownloadedUpdate.h in Headers */, 61EF67590E25C5B400F754E0 /* SUHost.h in Headers */, 723AC010259DBDAA00BDB4FA /* SUReleaseNotesCommon.h in Headers */, 72162B081C82C9600013C1C5 /* SULocalizations.h in Headers */, 55C14F06136EF6DB00649790 /* SULog.h in Headers */, EA1E286E22B665F0004AA304 /* SUSignatures.h in Headers */, 723ABF1B259D055E00BDB4FA /* SUReleaseNotesView.h in Headers */, 72B3DECD1E23479000457642 /* SPUInformationalUpdate.h in Headers */, 726F2CE51BC9C33D001971A4 /* SUOperatingSystem.h in Headers */, 61A225A40D1C4AC000430CCD /* SUStandardVersionComparator.h in Headers */, 6196CFF909C72148000DC222 /* SUStatusController.h in Headers */, 61A2279C0D1CEE7600430CCD /* SUSystemProfiler.h in Headers */, 61B5FCDF09C52A9F00B25A18 /* SUUpdateAlert.h in Headers */, 72A1C2631CD6849C004CD282 /* SUUpdatePermissionPrompt.h in Headers */, 72A450531C69A68900D67EEA /* SUUpdatePermissionResponse.h in Headers */, 72F9EBE21D517E2F004AC8B6 /* SUUpdater.h in Headers */, 721D588D25BE59F900D23BEA /* SUPhasedUpdateGroupInfo.h in Headers */, 72A6F98A1C94E2D6005F404C /* SUUpdaterDelegate.h in Headers */, 61A2259E0D1C495D00430CCD /* SUVersionComparisonProtocol.h in Headers */, 3772FEA913DE0B6B00F79537 /* SUVersionDisplayProtocol.h in Headers */, 724BB3871D32A167005D534A /* SUXPCInstallerConnection.h in Headers */, 724BB3A81D33461B005D534A /* SUXPCInstallerStatus.h in Headers */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXHeadersBuildPhase section */ /* Begin PBXLegacyTarget section */ 14732BC51960F69300593899 /* Distribution */ = { isa = PBXLegacyTarget; buildArgumentsString = "$(PROJECT_DIR)/Configurations/make-release-package.sh $(ACTION)"; buildConfigurationList = 14732BC61960F69300593899 /* Build configuration list for PBXLegacyTarget "Distribution" */; buildPhases = ( ); buildToolPath = bash; buildWorkingDirectory = ""; dependencies = ( 14732BCB1960F73500593899 /* PBXTargetDependency */, 72F94F6B1CC4A0C8002DEE68 /* PBXTargetDependency */, 725148E9266D918900247C9C /* PBXTargetDependency */, 7218EC352623ED97008FECF3 /* PBXTargetDependency */, 7218EC332623ED94008FECF3 /* PBXTargetDependency */, 1454BA1619637EDB00344E57 /* PBXTargetDependency */, 14732BCF1960F73500593899 /* PBXTargetDependency */, 7205C46B1E13070300E370AE /* PBXTargetDependency */, FA344BA724C869A300B2A401 /* PBXTargetDependency */, FA344BA524C8699E00B2A401 /* PBXTargetDependency */, 895C5DC724D78F700058A82D /* PBXTargetDependency */, ); name = Distribution; passBuildSettingsInEnvironment = 1; productName = Distribution; }; /* End PBXLegacyTarget section */ /* Begin PBXNativeTarget section */ 5D06E8CF0FD68C7C005AE3F6 /* BinaryDelta */ = { isa = PBXNativeTarget; buildConfigurationList = 5D06E8DA0FD68C95005AE3F6 /* Build configuration list for PBXNativeTarget "BinaryDelta" */; buildPhases = ( 5D06E8CD0FD68C7C005AE3F6 /* Sources */, 5D06E8CE0FD68C7C005AE3F6 /* Frameworks */, ); buildRules = ( ); dependencies = ( 376769AF23442F320077B8F7 /* PBXTargetDependency */, ); name = BinaryDelta; packageProductDependencies = ( 725C2EAB2782EF3C007CB7B5 /* ArgumentParser */, ); productName = BinaryDelta; productReference = 5D06E8D00FD68C7C005AE3F6 /* BinaryDelta */; productType = "com.apple.product-type.tool"; }; 612279D80DB5470200AB99EA /* Sparkle Unit Tests */ = { isa = PBXNativeTarget; buildConfigurationList = 612279DD0DB5470300AB99EA /* Build configuration list for PBXNativeTarget "Sparkle Unit Tests" */; buildPhases = ( 612279D50DB5470200AB99EA /* Sources */, 612279D60DB5470200AB99EA /* Frameworks */, 14958C6D19AEBC890061B14F /* Resources */, 142E0E0119A6A13300E4312B /* Copy Files */, ); buildRules = ( ); dependencies = ( 61FA528D0E2D9EB200EF58AD /* PBXTargetDependency */, 376769B123442F490077B8F7 /* PBXTargetDependency */, ); name = "Sparkle Unit Tests"; productName = "Sparkle Unit Tests"; productReference = 612279D90DB5470200AB99EA /* Sparkle Unit Tests.xctest */; productType = "com.apple.product-type.bundle.unit-test"; }; 61B5F90109C4CEE200B25A18 /* Sparkle Test App */ = { isa = PBXNativeTarget; buildConfigurationList = 61B5F90509C4CEE300B25A18 /* Build configuration list for PBXNativeTarget "Sparkle Test App" */; buildPhases = ( 61B5F8FE09C4CEE200B25A18 /* Resources */, 61B5F8FF09C4CEE200B25A18 /* Sources */, 61B5F90009C4CEE200B25A18 /* Frameworks */, 61B5FB4D09C4E9FA00B25A18 /* Copy Frameworks */, 726E4A261C86C88F00C57C6A /* Embed XPC Services */, ); buildRules = ( ); dependencies = ( 72CCCC19287B3D3D00E7156B /* PBXTargetDependency */, 72CCCC17287B3D3400E7156B /* PBXTargetDependency */, 61B5F91C09C4CF7200B25A18 /* PBXTargetDependency */, 726E4A201C86C88F00C57C6A /* PBXTargetDependency */, ); name = "Sparkle Test App"; productName = "Test Application"; productReference = 61B5F90209C4CEE200B25A18 /* Sparkle Test App.app */; productType = "com.apple.product-type.application"; }; 7205C43D1E13049400E370AE /* generate_appcast */ = { isa = PBXNativeTarget; buildConfigurationList = 7205C4451E13049400E370AE /* Build configuration list for PBXNativeTarget "generate_appcast" */; buildPhases = ( 7205C43A1E13049400E370AE /* Sources */, 7205C43B1E13049400E370AE /* Frameworks */, 7205C43C1E13049400E370AE /* CopyFiles */, ); buildRules = ( ); dependencies = ( 72CCCC21287B3DA400E7156B /* PBXTargetDependency */, 72CCCC1F287B3DA100E7156B /* PBXTargetDependency */, ); name = generate_appcast; packageProductDependencies = ( 727DBAE426B5BBFD00111F0C /* ArgumentParser */, ); productName = generate_appcast; productReference = 7205C43E1E13049400E370AE /* generate_appcast */; productType = "com.apple.product-type.tool"; }; 721C24441CB753E6005440CB /* Installer Progress */ = { isa = PBXNativeTarget; buildConfigurationList = 721C24561CB753E7005440CB /* Build configuration list for PBXNativeTarget "Installer Progress" */; buildPhases = ( 721C24411CB753E6005440CB /* Sources */, 721C24421CB753E6005440CB /* Frameworks */, 721C24431CB753E6005440CB /* Resources */, ); buildRules = ( ); dependencies = ( ); name = "Installer Progress"; productName = "Installer Progress"; productReference = 721C24451CB753E6005440CB /* Updater.app */; productType = "com.apple.product-type.application"; }; 724BB36B1D31D0B7005D534A /* SparkleInstallerConnection */ = { isa = PBXNativeTarget; buildConfigurationList = 724BB3781D31D0B7005D534A /* Build configuration list for PBXNativeTarget "SparkleInstallerConnection" */; buildPhases = ( 724BB3681D31D0B7005D534A /* Sources */, 724BB3691D31D0B7005D534A /* Frameworks */, 724BB36A1D31D0B7005D534A /* Resources */, ); buildRules = ( ); dependencies = ( ); name = SparkleInstallerConnection; productName = InstallerConnection; productReference = 724BB36C1D31D0B7005D534A /* InstallerConnection.xpc */; productType = "com.apple.product-type.xpc-service"; }; 724BB3921D333832005D534A /* SparkleInstallerStatus */ = { isa = PBXNativeTarget; buildConfigurationList = 724BB39F1D333832005D534A /* Build configuration list for PBXNativeTarget "SparkleInstallerStatus" */; buildPhases = ( 724BB38F1D333832005D534A /* Sources */, 724BB3901D333832005D534A /* Frameworks */, 724BB3911D333832005D534A /* Resources */, ); buildRules = ( ); dependencies = ( ); name = SparkleInstallerStatus; productName = InstallerStatus; productReference = 724BB3931D333832005D534A /* InstallerStatus.xpc */; productType = "com.apple.product-type.xpc-service"; }; 726B2B5C1C645FC900388755 /* UI Tests */ = { isa = PBXNativeTarget; buildConfigurationList = 726B2B671C645FC900388755 /* Build configuration list for PBXNativeTarget "UI Tests" */; buildPhases = ( 726B2B591C645FC900388755 /* Sources */, 726B2B5A1C645FC900388755 /* Frameworks */, 726B2B5B1C645FC900388755 /* Resources */, ); buildRules = ( ); dependencies = ( 726B2B631C645FC900388755 /* PBXTargetDependency */, ); name = "UI Tests"; productName = "UI Tests"; productReference = 726B2B5D1C645FC900388755 /* UI Tests.xctest */; productType = "com.apple.product-type.bundle.ui-testing"; }; 726E07AC1CAF08D6001A286B /* SparkleInstallerLauncher */ = { isa = PBXNativeTarget; buildConfigurationList = 726E07B91CAF08D6001A286B /* Build configuration list for PBXNativeTarget "SparkleInstallerLauncher" */; buildPhases = ( 726E07A91CAF08D6001A286B /* Sources */, 726E07AA1CAF08D6001A286B /* Frameworks */, ); buildRules = ( ); dependencies = ( ); name = SparkleInstallerLauncher; productName = InstallerLauncher; productReference = 726E07AD1CAF08D6001A286B /* Installer.xpc */; productType = "com.apple.product-type.xpc-service"; }; 726E07EE1CAF37BD001A286B /* SparkleDownloader */ = { isa = PBXNativeTarget; buildConfigurationList = 726E07FB1CAF37BD001A286B /* Build configuration list for PBXNativeTarget "SparkleDownloader" */; buildPhases = ( 726E07EB1CAF37BD001A286B /* Sources */, 726E07EC1CAF37BD001A286B /* Frameworks */, 726E07ED1CAF37BD001A286B /* Resources */, ); buildRules = ( ); dependencies = ( ); name = SparkleDownloader; productName = UpdateDownloader; productReference = 726E07EF1CAF37BD001A286B /* Downloader.xpc */; productType = "com.apple.product-type.xpc-service"; }; 726E4A151C86C88F00C57C6A /* TestAppHelper */ = { isa = PBXNativeTarget; buildConfigurationList = 726E4A221C86C88F00C57C6A /* Build configuration list for PBXNativeTarget "TestAppHelper" */; buildPhases = ( 726E4A121C86C88F00C57C6A /* Sources */, 726E4A131C86C88F00C57C6A /* Frameworks */, 726E4A141C86C88F00C57C6A /* Resources */, ); buildRules = ( ); dependencies = ( 72CCCC15287B3D1900E7156B /* PBXTargetDependency */, ); name = TestAppHelper; productName = TestAppHelper; productReference = 726E4A161C86C88F00C57C6A /* TestAppHelper.xpc */; productType = "com.apple.product-type.xpc-service"; }; 72B398D11D3D879300EE297F /* Autoupdate */ = { isa = PBXNativeTarget; buildConfigurationList = 72B398D61D3D879400EE297F /* Build configuration list for PBXNativeTarget "Autoupdate" */; buildPhases = ( 72B398CE1D3D879300EE297F /* Sources */, 72B398CF1D3D879300EE297F /* Frameworks */, 72B398D01D3D879300EE297F /* CopyFiles */, ); buildRules = ( ); dependencies = ( 37DC0CE62340667000501A67 /* PBXTargetDependency */, 5A06357223FE332300478A72 /* PBXTargetDependency */, ); name = Autoupdate; productName = Autoupdate; productReference = 72B398D21D3D879300EE297F /* Autoupdate */; productType = "com.apple.product-type.tool"; }; 72D9549D1CBB415B006F28BD /* sparkle-cli */ = { isa = PBXNativeTarget; buildConfigurationList = 72D954AF1CBB415C006F28BD /* Build configuration list for PBXNativeTarget "sparkle-cli" */; buildPhases = ( 72D9549A1CBB415B006F28BD /* Sources */, 72D9549B1CBB415B006F28BD /* Frameworks */, 72D9549C1CBB415B006F28BD /* Resources */, 72D954B41CBB4469006F28BD /* Copy Sparkle */, ); buildRules = ( ); dependencies = ( 726F168926747CEB005BEA89 /* PBXTargetDependency */, ); name = "sparkle-cli"; productName = "sparkle-cli"; productReference = 72D9549E1CBB415B006F28BD /* sparkle.app */; productType = "com.apple.product-type.application"; }; 8DC2EF4F0486A6940098B216 /* Sparkle */ = { isa = PBXNativeTarget; buildConfigurationList = 1DEB91AD08733DA50010E9CD /* Build configuration list for PBXNativeTarget "Sparkle" */; buildPhases = ( 8DC2EF500486A6940098B216 /* Headers */, 8DC2EF520486A6940098B216 /* Resources */, 142E0E0319A6A24100E4312B /* Copy Tools */, 72045CDF26FEE51D004F96E5 /* Copy XPC Services */, 72045CE626FEE8CC004F96E5 /* Strip Framework */, 8DC2EF540486A6940098B216 /* Sources */, 8DC2EF560486A6940098B216 /* Frameworks */, 6131B1910DDCDE32005215F0 /* Run Script: Set Git Version Info */, 720B4C2125EBFAA5005A0592 /* Run Script: Link Tools */, ); buildRules = ( ); dependencies = ( 72045CD826FEE471004F96E5 /* PBXTargetDependency */, 72045CDA26FEE471004F96E5 /* PBXTargetDependency */, 72045CDC26FEE471004F96E5 /* PBXTargetDependency */, 72045CDE26FEE471004F96E5 /* PBXTargetDependency */, 72A5D5AC1D6929260009E5AC /* PBXTargetDependency */, 72D954BA1CBB6E27006F28BD /* PBXTargetDependency */, ); name = Sparkle; productInstallPath = "$(HOME)/Library/Frameworks"; productName = Sparkle; productReference = 8DC2EF5B0486A6940098B216 /* Sparkle.framework */; productType = "com.apple.product-type.framework"; }; EA1E280E22B64522004AA304 /* bsdiff */ = { isa = PBXNativeTarget; buildConfigurationList = EA1E281322B64522004AA304 /* Build configuration list for PBXNativeTarget "bsdiff" */; buildPhases = ( EA1E280C22B64522004AA304 /* Sources */, EA1E282522B6467A004AA304 /* Copy Headers */, ); buildRules = ( ); dependencies = ( ); name = bsdiff; productName = bsdiff; productReference = EA1E280F22B64522004AA304 /* libbsdiff.a */; productType = "com.apple.product-type.library.static"; }; EA1E282C22B660BE004AA304 /* ed25519 */ = { isa = PBXNativeTarget; buildConfigurationList = EA1E282E22B660BE004AA304 /* Build configuration list for PBXNativeTarget "ed25519" */; buildPhases = ( EA1E282A22B660BE004AA304 /* Sources */, EA1E282B22B660BE004AA304 /* Frameworks */, EA1E283222B660C6004AA304 /* Copy Headers */, ); buildRules = ( ); dependencies = ( ); name = ed25519; productName = ed25519; productReference = EA1E282D22B660BE004AA304 /* libed25519.a */; productType = "com.apple.product-type.library.static"; }; EA1E285D22B66487004AA304 /* generate_keys */ = { isa = PBXNativeTarget; buildConfigurationList = EA1E286222B66487004AA304 /* Build configuration list for PBXNativeTarget "generate_keys" */; buildPhases = ( EA1E285A22B66487004AA304 /* Sources */, EA1E285B22B66487004AA304 /* Frameworks */, EA1E285C22B66487004AA304 /* CopyFiles */, ); buildRules = ( ); dependencies = ( 72CCCC1B287B3D9000E7156B /* PBXTargetDependency */, ); name = generate_keys; packageProductDependencies = ( 727DBAE626B5C47800111F0C /* ArgumentParser */, ); productName = generate_keys; productReference = EA1E285E22B66487004AA304 /* generate_keys */; productType = "com.apple.product-type.tool"; }; EA1E287522B666EB004AA304 /* sign_update */ = { isa = PBXNativeTarget; buildConfigurationList = EA1E287D22B666EB004AA304 /* Build configuration list for PBXNativeTarget "sign_update" */; buildPhases = ( EA1E287222B666EB004AA304 /* Sources */, EA1E287322B666EB004AA304 /* Frameworks */, EA1E287422B666EB004AA304 /* CopyFiles */, ); buildRules = ( ); dependencies = ( 72CCCC1D287B3D9600E7156B /* PBXTargetDependency */, ); name = sign_update; packageProductDependencies = ( 727DBAE826B5C48A00111F0C /* ArgumentParser */, ); productName = sign_update; productReference = EA1E287622B666EB004AA304 /* sign_update */; productType = "com.apple.product-type.tool"; }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ 0867D690FE84028FC02AAC07 /* Project object */ = { isa = PBXProject; attributes = { CLASSPREFIX = SU; LastSwiftUpdateCheck = 1020; LastUpgradeCheck = 1630; ORGANIZATIONNAME = "Sparkle Project"; TargetAttributes = { 5D06E8CF0FD68C7C005AE3F6 = { LastSwiftMigration = 1320; }; 612279D80DB5470200AB99EA = { LastSwiftMigration = 0800; TestTargetID = 8DC2EF4F0486A6940098B216; }; 61B5F90109C4CEE200B25A18 = { SystemCapabilities = { com.apple.Sandbox = { enabled = 1; }; }; }; 7205C43D1E13049400E370AE = { CreatedOnToolsVersion = 8.2.1; LastSwiftMigration = 1200; ProvisioningStyle = Automatic; }; 721C24441CB753E6005440CB = { CreatedOnToolsVersion = 7.3; }; 724BB36B1D31D0B7005D534A = { CreatedOnToolsVersion = 7.3.1; }; 724BB3921D333832005D534A = { CreatedOnToolsVersion = 7.3.1; }; 726B2B5C1C645FC900388755 = { CreatedOnToolsVersion = 7.2.1; LastSwiftMigration = 0800; TestTargetID = 61B5F90109C4CEE200B25A18; }; 726E07AC1CAF08D6001A286B = { CreatedOnToolsVersion = 7.3; }; 726E07EE1CAF37BD001A286B = { CreatedOnToolsVersion = 7.3; SystemCapabilities = { com.apple.Sandbox = { enabled = 1; }; }; }; 726E4A151C86C88F00C57C6A = { CreatedOnToolsVersion = 7.2.1; SystemCapabilities = { com.apple.Sandbox = { enabled = 0; }; }; }; 72B398D11D3D879300EE297F = { CreatedOnToolsVersion = 7.3.1; }; 72D9549D1CBB415B006F28BD = { CreatedOnToolsVersion = 7.3; }; 895C5DC024D78E210058A82D = { CreatedOnToolsVersion = 12.0; }; EA1E280E22B64522004AA304 = { CreatedOnToolsVersion = 10.2.1; }; EA1E282C22B660BE004AA304 = { CreatedOnToolsVersion = 10.2.1; }; EA1E285D22B66487004AA304 = { CreatedOnToolsVersion = 10.2.1; }; EA1E287522B666EB004AA304 = { CreatedOnToolsVersion = 10.2.1; }; }; }; buildConfigurationList = 1DEB91B108733DA50010E9CD /* Build configuration list for PBXProject "Sparkle" */; compatibilityVersion = "Xcode 6.3"; developmentRegion = en; hasScannedForEncodings = 1; knownRegions = ( zh_TW, en, ca, cs, cy, da, de, es, fi, fr, he, hu, id, is, it, ja, ko, nl, pl, ru, sk, sv, th, tr, zh_CN, pt, ro, sl, uk, ar, nb, el, "zh-Hans", "zh-Hant", "fr-CA", "pt-PT", "pt-BR", Base, hr, fa, zh_HK, nn, vi, ); mainGroup = 0867D691FE84028FC02AAC07 /* Sparkle */; packageReferences = ( 727DBAE326B5BBFD00111F0C /* XCRemoteSwiftPackageReference "swift-argument-parser" */, ); productRefGroup = 034768DFFF38A50411DB9C8B /* Products */; projectDirPath = ""; projectRoot = ""; targets = ( 8DC2EF4F0486A6940098B216 /* Sparkle */, 72B398D11D3D879300EE297F /* Autoupdate */, 726E07AC1CAF08D6001A286B /* SparkleInstallerLauncher */, 724BB36B1D31D0B7005D534A /* SparkleInstallerConnection */, 724BB3921D333832005D534A /* SparkleInstallerStatus */, 726E07EE1CAF37BD001A286B /* SparkleDownloader */, 61B5F90109C4CEE200B25A18 /* Sparkle Test App */, 726E4A151C86C88F00C57C6A /* TestAppHelper */, 612279D80DB5470200AB99EA /* Sparkle Unit Tests */, 5D06E8CF0FD68C7C005AE3F6 /* BinaryDelta */, 72D9549D1CBB415B006F28BD /* sparkle-cli */, 721C24441CB753E6005440CB /* Installer Progress */, 14732BC51960F69300593899 /* Distribution */, 1495005F195FB89400BC5B5B /* All */, 895C5DC024D78E210058A82D /* XCFrameworks */, 726B2B5C1C645FC900388755 /* UI Tests */, 7205C43D1E13049400E370AE /* generate_appcast */, EA1E285D22B66487004AA304 /* generate_keys */, EA1E287522B666EB004AA304 /* sign_update */, EA1E280E22B64522004AA304 /* bsdiff */, EA1E282C22B660BE004AA304 /* ed25519 */, ); }; /* End PBXProject section */ /* Begin PBXResourcesBuildPhase section */ 14958C6D19AEBC890061B14F /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( 725B3A82263FBF0C0041AB8E /* testappcast_minimumAutoupdateVersion.xml in Resources */, 14958C6E19AEBC950061B14F /* signed-test-file.txt in Resources */, 72C56E262F04C28B0005A484 /* testreleasenotes.html in Resources */, 723AD12F29922B5F006BB02F /* test-dangerous-link.xml in Resources */, 72EB736129BEB36100FBCEE7 /* DevSignedAppVersion2.zip in Resources */, 723EDC3F26885A8E000BCBA4 /* testappcast_channels.xml in Resources */, 720DC50427A51A6500DFF3EC /* testappcast_minimumAutoupdateVersionSkipping.xml in Resources */, 72E6D9712C04DE1A005496E4 /* SparkleTestCodeSign_pkg.dmg in Resources */, 72526BE62EF3349D005791ED /* testappcast_minimumUpdateVersion.xml in Resources */, 72AC6B2E1B9B218C00F62325 /* SparkleTestCodeSignApp.dmg in Resources */, C23E885B1BE7B24F0050BB73 /* SparkleTestCodeSignApp.enc.dmg in Resources */, 72AC6B281B9AAD6700F62325 /* SparkleTestCodeSignApp.tar in Resources */, 72AC6B2A1B9AAF3A00F62325 /* SparkleTestCodeSignApp.tar.bz2 in Resources */, 72E6D9732C0526DC005496E4 /* SparkleTestCodeSignApp.enc.nolicense.dmg in Resources */, 72AC6B261B9AAC8800F62325 /* SparkleTestCodeSignApp.tar.gz in Resources */, 72AC6B2C1B9AB0EE00F62325 /* SparkleTestCodeSignApp.tar.xz in Resources */, 729F7ECE27409077004592DC /* SparkleTestCodeSignApp_bad_extraneous.zip in Resources */, 725453552C04DB3700362123 /* SparkleTestCodeSign_apfs_lzma_aux_files_adhoc.dmg in Resources */, 5A5DD41D249F116E0045EB3E /* test-relative-urls.xml in Resources */, F8761EB31ADC50EB000C9034 /* SparkleTestCodeSignApp.zip in Resources */, 5A5DD40424958B000045EB3E /* SUUpdateValidatorTest in Resources */, 14958C6F19AEBC980061B14F /* test-pubkey.pem in Resources */, 5AF6C74F1AEA46D10014A3AB /* test.pkg in Resources */, 72EB735F29BE981300FBCEE7 /* DevSignedApp.zip in Resources */, 72BC6C3D275027BF0083F14B /* SparkleTestCodeSign_apfs.dmg in Resources */, 726FC0382C1E96AA00177986 /* SparkleTestCodeSignApp.enc.aar in Resources */, 726B20612CF4F1D300E6F7DB /* DevSignedAppVersion2.dmg in Resources */, 720DC50627A62CDC00DFF3EC /* testappcast_minimumAutoupdateVersionSkipping2.xml in Resources */, 5AD0FA7F1C73F2E2004BCEFF /* testappcast.xml in Resources */, FA30773D24CBC295007BA37D /* testlocalizedreleasenotesappcast.xml in Resources */, 72CCDEBE27421FD500B53718 /* SparkleTestCodeSignApp_bad_header.zip in Resources */, 726FC0362C1E787A00177986 /* SparkleTestCodeSignApp.aar in Resources */, 722545B626805FF80036465C /* testappcast_info_updates.xml in Resources */, 723414172EBFB482009727E0 /* testappcast_arm64HardwareRequirement.xml in Resources */, 7202DC9A269ABD3500737EC4 /* testappcast_phasedRollout.xml in Resources */, 5F1510A21C96E591006E1629 /* testnamespaces.xml in Resources */, 729F7EAC27366353004592DC /* test-links.xml in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; 61B5F8FE09C4CEE200B25A18 /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( 723ABDDB259A9E8600BDB4FA /* InfoPlist.strings in Resources */, 7230BCDE2E22E2BC00B71297 /* Images.xcassets in Resources */, 72E45CFC1B641961005C701A /* sparkletestcast.xml in Resources */, 725F97A61C8B304D00265BE4 /* SUInstallUpdateViewController.xib in Resources */, 723ABE02259A9E9E00BDB4FA /* MainMenu.xib in Resources */, 72F0EC45278A55CA002A876A /* screenshot.png in Resources */, 72E45CF81B640DAE005C701A /* SUUpdateSettingsWindowController.xib in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; 721C24431CB753E6005440CB /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( 72EB87EC1CB8887E00C37F42 /* SUStatus.xib in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; 724BB36A1D31D0B7005D534A /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( ); runOnlyForDeploymentPostprocessing = 0; }; 724BB3911D333832005D534A /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( ); runOnlyForDeploymentPostprocessing = 0; }; 726B2B5B1C645FC900388755 /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( ); runOnlyForDeploymentPostprocessing = 0; }; 726E07ED1CAF37BD001A286B /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( ); runOnlyForDeploymentPostprocessing = 0; }; 726E4A141C86C88F00C57C6A /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( ); runOnlyForDeploymentPostprocessing = 0; }; 72D9549C1CBB415B006F28BD /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( ); runOnlyForDeploymentPostprocessing = 0; }; 8DC2EF520486A6940098B216 /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( 61AAE8280A321A7F00D8810D /* Sparkle.strings in Resources */, 1EAA4C8B2132C7BF00604473 /* ReleaseNotesColorStyle.css in Resources */, 72FE54422E56A14100227A91 /* SUUpdateAlert.xib in Resources */, 55C14BEF136EF21700649790 /* SUStatus.xib in Resources */, 729C51282E58015600D7365A /* SUUpdatePermissionPrompt.xib in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ 6131B1910DDCDE32005215F0 /* Run Script: Set Git Version Info */ = { isa = PBXShellScriptBuildPhase; alwaysOutOfDate = 1; buildActionMask = 12; files = ( ); inputPaths = ( ); name = "Run Script: Set Git Version Info"; outputPaths = ( ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "\"$PROJECT_DIR/Configurations/set-git-version-info.sh\"\n"; showEnvVarsInLog = 0; }; 72045CE626FEE8CC004F96E5 /* Strip Framework */ = { isa = PBXShellScriptBuildPhase; alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); inputFileListPaths = ( ); inputPaths = ( ); name = "Strip Framework"; outputFileListPaths = ( ); outputPaths = ( ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "\"$PROJECT_DIR/Configurations/strip-framework.sh\"\n"; }; 720B4C2125EBFAA5005A0592 /* Run Script: Link Tools */ = { isa = PBXShellScriptBuildPhase; alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); inputFileListPaths = ( ); inputPaths = ( ); name = "Run Script: Link Tools"; outputFileListPaths = ( ); outputPaths = ( ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "\"$PROJECT_DIR/Configurations/link-tools.sh\"\n"; }; 895C5DC524D78E460058A82D /* ShellScript */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputFileListPaths = ( ); inputPaths = ( ); outputFileListPaths = ( ); outputPaths = ( ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "\"$PROJECT_DIR/Configurations/make-xcframework.sh\"\n"; }; /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ 5D06E8CD0FD68C7C005AE3F6 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( 725EE482277BF44A00D820CE /* SPUXarDeltaArchive.m in Sources */, 728ED34C277DA23400D9238F /* SPUSparkleDeltaArchive.m in Sources */, 725C2EAA2782EC61007CB7B5 /* main.swift in Sources */, 7267E5761D3D895B00D1BF90 /* SUBinaryDeltaApply.m in Sources */, 725EE487277D376000D820CE /* SPUDeltaArchive.m in Sources */, 7267E5781D3D895B00D1BF90 /* SUBinaryDeltaCommon.m in Sources */, 7267E57A1D3D895B00D1BF90 /* SUBinaryDeltaCreate.m in Sources */, 726F2CE81BC9C48F001971A4 /* SUConstants.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; 612279D50DB5470200AB99EA /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( 72C56E202F04B3760005A484 /* Signing.swift in Sources */, 72C56E1F2F04AC890005A484 /* SUFeedSignatureVerifierTest.swift in Sources */, 72D04F3E2B097D4300A6DEAA /* SPUVerifierInformation.m in Sources */, 72266A8C29464D0200645376 /* SUSignatures.m in Sources */, 72266A8B29464CEA00645376 /* SUHost.m in Sources */, 72266A8A2946493C00645376 /* SUCodeSigningVerifier.m in Sources */, 72266A88294635BA00645376 /* SUAppcastDriver.m in Sources */, 72266A872946359600645376 /* SUFileManager.m in Sources */, 725EE488277D398100D820CE /* SPUDeltaArchive.m in Sources */, 725EE483277C767A00D820CE /* SPUXarDeltaArchive.m in Sources */, 7269E4982648D3460088C213 /* SPUSkippedUpdate.m in Sources */, 721D5ABC25C680A300D23BEA /* SUFlatPackageUnarchiver.m in Sources */, 725F97771C8A62F500265BE4 /* SUAdHocCodeSigning.m in Sources */, 5A4094481C74EA5200983BE0 /* SUAppcastTest.swift in Sources */, 7267E5EE1D3D915900D1BF90 /* SUBinaryDeltaApply.m in Sources */, 7267E5EF1D3D915900D1BF90 /* SUBinaryDeltaCommon.m in Sources */, 7267E5F01D3D915900D1BF90 /* SUBinaryDeltaCreate.m in Sources */, 728ED34B277DA23400D9238F /* SPUSparkleDeltaArchive.m in Sources */, 142E0E0919A83AAC00E4312B /* SUBinaryDeltaTest.m in Sources */, 7267E5F11D3D917A00D1BF90 /* SUBinaryDeltaUnarchiver.m in Sources */, F8761EB11ADC5068000C9034 /* SUCodeSigningVerifierTest.m in Sources */, 5A5DD402249586840045EB3E /* SUUpdateValidator.m in Sources */, 7267E5F61D3D919000D1BF90 /* SUDiskImageUnarchiver.m in Sources */, 7267E5F21D3D918000D1BF90 /* SUSignatureVerifier.m in Sources */, 5AF9DC3C1981DBEE001EA135 /* SUSignatureVerifierTest.m in Sources */, 72A4A2401BB6567D00E7820D /* SUFileManagerTest.swift in Sources */, 7267E5F41D3D918B00D1BF90 /* SUGuidedPackageInstaller.m in Sources */, 5A5DD401249585E70045EB3E /* SUUpdateValidatorTest.swift in Sources */, 7267E5EC1D3D912900D1BF90 /* SUInstaller.m in Sources */, 5AF6C7541AEA49840014A3AB /* SUInstallerTest.m in Sources */, 14652F8219A9746000959E44 /* SULog.m in Sources */, 7267E5F81D3D91A800D1BF90 /* SUPipedUnarchiver.m in Sources */, 7267E5F71D3D919600D1BF90 /* SUPlainInstaller.m in Sources */, 72BEBFEF1D7287570019146B /* SUSpotlightImporterTest.swift in Sources */, 7267E5ED1D3D912E00D1BF90 /* SUUnarchiver.m in Sources */, 72316BD41E0DB37E0039EFD9 /* SUUnarchiverNotifier.m in Sources */, 7210C7681B9A9A1500EB90AC /* SUUnarchiverTest.swift in Sources */, 5AE459001C34118500E3BB47 /* SUUpdaterTest.m in Sources */, 5AE459021C34118500E3BB47 /* SUVersionComparisonTest.m in Sources */, 72C56E092EFDFB850005A484 /* SPUExtractSignedFeed.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; 61B5F8FF09C4CEE200B25A18 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( 72F0EC48278A5B95002A876A /* SPUDeltaArchive.m in Sources */, 72F0EC49278A5B95002A876A /* SPUSparkleDeltaArchive.m in Sources */, 72F0EC4A278A5B95002A876A /* SPUXarDeltaArchive.m in Sources */, 72F0EC46278A5B87002A876A /* SUBinaryDeltaCommon.m in Sources */, 72F0EC47278A5B87002A876A /* SUBinaryDeltaCreate.m in Sources */, 61B5F93009C4CFDC00B25A18 /* main.m in Sources */, 726F2CEB1BC9C733001971A4 /* SUConstants.m in Sources */, 725F97A51C8B304D00265BE4 /* SUInstallUpdateViewController.m in Sources */, 725F97841C8AA90000265BE4 /* SUPopUpTitlebarUserDriver.m in Sources */, 72E45CF31B640CDD005C701A /* SUTestApplicationDelegate.m in Sources */, 726E4A2B1C87D56200C57C6A /* SUTestWebServer.m in Sources */, 72E45CF71B640DAE005C701A /* SUUpdateSettingsWindowController.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; 7205C43A1E13049400E370AE /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( 72C56E0E2EFEEF8B0005A484 /* SPUExtractSignedFeed.m in Sources */, 729743AB279D1BD2009910B2 /* SUCodeSigningVerifier.m in Sources */, 725EE489277D39B400D820CE /* SPUDeltaArchive.m in Sources */, 725EE48A277D39B400D820CE /* SPUXarDeltaArchive.m in Sources */, 721D5B1B25C692BB00D23BEA /* SUFlatPackageUnarchiver.m in Sources */, 72464F7C1E2097F600FB341C /* SUOperatingSystem.m in Sources */, 7205C44C1E1304CE00E370AE /* Appcast.swift in Sources */, 7205C44D1E1304CE00E370AE /* ArchiveItem.swift in Sources */, 7205C44F1E1304CE00E370AE /* FeedXML.swift in Sources */, 7205C4411E13049400E370AE /* main.swift in Sources */, 728ED34D277DA23400D9238F /* SPUSparkleDeltaArchive.m in Sources */, 7205C45F1E13066F00E370AE /* SUBinaryDeltaApply.m in Sources */, 7205C4621E1306A600E370AE /* SUBinaryDeltaCommon.m in Sources */, FA30773F24CBC3E9007BA37D /* URL+Hashing.swift in Sources */, 72C56E102EFEF10B0005A484 /* Signing.swift in Sources */, 7205C4611E13069000E370AE /* SUBinaryDeltaCreate.m in Sources */, 7205C45B1E13064C00E370AE /* SUBinaryDeltaUnarchiver.m in Sources */, 7205C45D1E13065F00E370AE /* SUConstants.m in Sources */, 7205C45A1E13063E00E370AE /* SUDiskImageUnarchiver.m in Sources */, 7205C45E1E13066800E370AE /* SUFileManager.m in Sources */, 7205C45C1E13065800E370AE /* SULog.m in Sources */, 7205C4591E13062500E370AE /* SUPipedUnarchiver.m in Sources */, 7205C4561E13060D00E370AE /* SUStandardVersionComparator.m in Sources */, 7205C4571E13061F00E370AE /* SUUnarchiver.m in Sources */, 7205C4581E13061F00E370AE /* SUUnarchiverNotifier.m in Sources */, 7205C4501E1304CE00E370AE /* Unarchive.swift in Sources */, 72666DC62B0B28F4001511B0 /* Secret.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; 721C24411CB753E6005440CB /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( 72EF30C22675CF67008CE987 /* SPUAppcastItemState.m in Sources */, 72EF30C32675CF67008CE987 /* SPUAppcastItemStateResolver.m in Sources */, 720AC2DC2618E8A500E25A3E /* SPUSecureCoding.m in Sources */, 720AC2C92618E89500E25A3E /* SUAppcastItem.m in Sources */, 720AC2A42618E85700E25A3E /* SPUInstallationInfo.m in Sources */, 72D954811CBACC35006F28BD /* InstallerProgressAppController.m in Sources */, 72D954831CBAD34F006F28BD /* main.m in Sources */, 72EB87EA1CB8798800C37F42 /* ShowInstallerProgress.m in Sources */, 727F340E2605321D00020E85 /* SULog+NSError.m in Sources */, 7267E5DC1D3D8F1E00D1BF90 /* SPUMessageTypes.m in Sources */, 723C8A561E2D60DB00C14942 /* SUTouchBarButtonGroup.m in Sources */, 5AA89BA623FE276A0094DAB8 /* SUSignatures.m in Sources */, 7267E5FA1D3DAC3600D1BF90 /* StatusInfo.m in Sources */, 720595EF1D700568000572E8 /* SUApplicationInfo.m in Sources */, 722FB7E6260EE53F00EB571C /* SUNormalization.m in Sources */, 721C245E1CB757DE005440CB /* SUConstants.m in Sources */, 7267E5EB1D3D90C200D1BF90 /* SUFileManager.m in Sources */, 721C24611CB75C5D005440CB /* SUHost.m in Sources */, 721C24621CB75C68005440CB /* SULog.m in Sources */, 729051E41DC82AC0003DEA7F /* SUOperatingSystem.m in Sources */, 721C245C1CB7576E005440CB /* SUStatusController.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; 724BB3681D31D0B7005D534A /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( 724BB3731D31D0B7005D534A /* main.m in Sources */, 724BB3711D31D0B7005D534A /* SUInstallerConnection.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; 724BB38F1D333832005D534A /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( 724BB39A1D333832005D534A /* main.m in Sources */, 724BB3981D333832005D534A /* SUInstallerStatus.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; 726B2B591C645FC900388755 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( 720B16451C66433D006985FB /* SUTestApplicationTest.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; 726E07A91CAF08D6001A286B /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( 7290BF302E5A5B0F00D75022 /* SUCodeSigningVerifier.m in Sources */, 72464F7E1E21ED8C00FB341C /* SUHost.m in Sources */, 726E07B41CAF08D6001A286B /* main.m in Sources */, 721D8A7B1D4963190032E472 /* SPULocalCacheDirectory.m in Sources */, 7267E5E11D3D901600D1BF90 /* SPUMessageTypes.m in Sources */, 5AA89BA523FE27660094DAB8 /* SUSignatures.m in Sources */, 726E07BF1CAF0C6C001A286B /* SUConstants.m in Sources */, 7267E5E81D3D90AA00D1BF90 /* SUFileManager.m in Sources */, 726E07B21CAF08D6001A286B /* SUInstallerLauncher.m in Sources */, 726E07C01CAF15B7001A286B /* SULog.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; 726E07EB1CAF37BD001A286B /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( 7290BF2F2E5A5AFA00D75022 /* SUCodeSigningVerifier.m in Sources */, 723B5DA71CF7AB0100365F95 /* main.m in Sources */, 72E539121D68C3FA0092CE5E /* SPUDownloadData.m in Sources */, 723B5DA91CF7AB0100365F95 /* SPUDownloader.m in Sources */, 721BC2101D1CDE55002BC71E /* SPULocalCacheDirectory.m in Sources */, 728638ED1CAF50CE00783084 /* SUConstants.m in Sources */, 721D8A871D4C5BF10032E472 /* SULog.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; 726E4A121C86C88F00C57C6A /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( 726E4A1D1C86C88F00C57C6A /* main.m in Sources */, 725F97781C8A65AC00265BE4 /* SUAdHocCodeSigning.m in Sources */, 726E4A1B1C86C88F00C57C6A /* TestAppHelper.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; 72B398CE1D3D879300EE297F /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( 725EE480277BF13B00D820CE /* SPUXarDeltaArchive.m in Sources */, 728ED34A277DA23400D9238F /* SPUSparkleDeltaArchive.m in Sources */, 72EF30C42675CFA1008CE987 /* SPUAppcastItemState.m in Sources */, 72EF30C52675CFA1008CE987 /* SPUAppcastItemStateResolver.m in Sources */, 72464F701E1F31E000FB341C /* SUOperatingSystem.m in Sources */, 7267E5A81D3D8A9900D1BF90 /* AgentConnection.m in Sources */, 7267E5A21D3D8A7E00D1BF90 /* AppInstaller.m in Sources */, 7267E59F1D3D8A6F00D1BF90 /* main.m in Sources */, 7267E5B81D3D8AEE00D1BF90 /* SPUInstallationInfo.m in Sources */, 7267E5AE1D3D8AB700D1BF90 /* SPUInstallationInputData.m in Sources */, 721D8A861D4ADFEB0032E472 /* SPULocalCacheDirectory.m in Sources */, 7267E5B31D3D8AD500D1BF90 /* SPUMessageTypes.m in Sources */, 7267E5CD1D3D8C7200D1BF90 /* SPUSecureCoding.m in Sources */, 7267E5CB1D3D8C6400D1BF90 /* SUAppcastItem.m in Sources */, 7267E5751D3D895B00D1BF90 /* SUBinaryDeltaApply.m in Sources */, 725EE486277D375F00D820CE /* SPUDeltaArchive.m in Sources */, 7267E5771D3D895B00D1BF90 /* SUBinaryDeltaCommon.m in Sources */, 7267E5791D3D895B00D1BF90 /* SUBinaryDeltaCreate.m in Sources */, 7267E57F1D3D896700D1BF90 /* SUBinaryDeltaUnarchiver.m in Sources */, 7267E59C1D3D8A5A00D1BF90 /* SUCodeSigningVerifier.m in Sources */, 7267E5CC1D3D8C6B00D1BF90 /* SUConstants.m in Sources */, 72D04F3D2B094C8400A6DEAA /* SPUVerifierInformation.m in Sources */, 7267E5861D3D89B300D1BF90 /* SUDiskImageUnarchiver.m in Sources */, 7267E59D1D3D8A5A00D1BF90 /* SUSignatureVerifier.m in Sources */, 7267E5E71D3D90AA00D1BF90 /* SUFileManager.m in Sources */, 7267E5C21D3D8B2700D1BF90 /* SUGuidedPackageInstaller.m in Sources */, 727F340D2605321D00020E85 /* SULog+NSError.m in Sources */, 7267E5D71D3D8D3F00D1BF90 /* SUHost.m in Sources */, 7267E5C31D3D8B2700D1BF90 /* SUInstaller.m in Sources */, 5A6DD17123FE1FFC000AEF33 /* SUSignatures.m in Sources */, 721D5A8525C65D3F00D23BEA /* SUFlatPackageUnarchiver.m in Sources */, 72C56E0A2EFDFB850005A484 /* SPUExtractSignedFeed.m in Sources */, 7267E5CE1D3D8C7500D1BF90 /* SULog.m in Sources */, 7267E5871D3D89B300D1BF90 /* SUPipedUnarchiver.m in Sources */, 7267E5C51D3D8B2700D1BF90 /* SUPlainInstaller.m in Sources */, 7267E5D81D3D8D4400D1BF90 /* SUStandardVersionComparator.m in Sources */, 7267E5881D3D89B300D1BF90 /* SUUnarchiver.m in Sources */, 72316BD31E0DA8430039EFD9 /* SUUnarchiverNotifier.m in Sources */, 729924941DF4A45000DBCDF5 /* SUUpdateValidator.m in Sources */, 722FB7E5260EE53F00EB571C /* SUNormalization.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; 72D9549A1CBB415B006F28BD /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( 72D954A51CBB415C006F28BD /* main.m in Sources */, 72D954A21CBB415C006F28BD /* SPUCommandLineDriver.m in Sources */, 72D954B81CBB467F006F28BD /* SPUCommandLineUserDriver.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; 8DC2EF540486A6940098B216 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( 72C56E0B2EFDFB850005A484 /* SPUExtractSignedFeed.m in Sources */, 72C56E052EFB45BD0005A484 /* SPUVerifierInformation.m in Sources */, 72C56E042EFB45B10005A484 /* SUSignatureVerifier.m in Sources */, 723ABF31259D062F00BDB4FA /* SULegacyWebView.m in Sources */, 72B767E71C9CFD7200A07552 /* SPUAutomaticUpdateDriver.m in Sources */, 72B767D71C9C8B5C00A07552 /* SPUBasicUpdateDriver.m in Sources */, 72B767E31C9CE90A00A07552 /* SPUCoreBasedUpdateDriver.m in Sources */, 72F9EC451D5E9ED8004AC8B6 /* SPUDownloadData.m in Sources */, 723ABFC5259D4CB300BDB4FA /* SUWKWebView.m in Sources */, 7229E1BE1C98EFF200CB50D0 /* SPUDownloadDriver.m in Sources */, 723B5DAA1CF7AB6A00365F95 /* SPUDownloader.m in Sources */, 7267E5B71D3D8AEE00D1BF90 /* SPUInstallationInfo.m in Sources */, 7267E5DF1D3D8FFA00D1BF90 /* SPUInstallationInputData.m in Sources */, 72B767CF1C9B924900A07552 /* SPUInstallerDriver.m in Sources */, 72EF30BF2675CF38008CE987 /* SPUAppcastItemState.m in Sources */, 721BC20F1D1CDE55002BC71E /* SPULocalCacheDirectory.m in Sources */, 7267E5B21D3D8AD500D1BF90 /* SPUMessageTypes.m in Sources */, 72B3DECF1E23479000457642 /* SPUInformationalUpdate.m in Sources */, 728337A71C9E6FF40085AA99 /* SPUProbeInstallStatus.m in Sources */, 72B767D31C9C7B9300A07552 /* SPUProbingUpdateDriver.m in Sources */, 7229E1BA1C97CC4D00CB50D0 /* SPUScheduledUpdateDriver.m in Sources */, 726E075D1CA3A6D6001A286B /* SPUSecureCoding.m in Sources */, 7246E0A41C83B685003B4E75 /* SPUStandardUpdaterController.m in Sources */, 725CB95B1C7121830064365A /* SPUStandardUserDriver.m in Sources */, 723C8A551E2D60DB00C14942 /* SUTouchBarButtonGroup.m in Sources */, 72B767DB1C9CD2E400A07552 /* SPUUIBasedUpdateDriver.m in Sources */, 72DBA37E1D60CC34002594A8 /* SPUUpdatePermissionRequest.m in Sources */, 72EF30C02675CF38008CE987 /* SPUAppcastItemStateResolver.m in Sources */, 61B5F8EE09C4CE3C00B25A18 /* SPUUpdater.m in Sources */, 720E217C1D0D00BF003A311C /* SPUUpdaterCycle.m in Sources */, 72AEB1D529A1A74E0033883E /* SPUStandardVersionDisplay.m in Sources */, 726E078E1CA891E9001A286B /* SPUUpdaterSettings.m in Sources */, 721D588F25BE59F900D23BEA /* SUPhasedUpdateGroupInfo.m in Sources */, 72F9EC401D5E823F004AC8B6 /* SPUUpdaterTimer.m in Sources */, 72B767DF1C9CDB9700A07552 /* SPUUserInitiatedUpdateDriver.m in Sources */, 72F94F591CC44DE1002DEE68 /* SPUXPCServiceInfo.m in Sources */, 61B5FBB709C4FAFF00B25A18 /* SUAppcast.m in Sources */, 72B767CB1C9B707000A07552 /* SUAppcastDriver.m in Sources */, 61B5FC6F09C51F4900B25A18 /* SUAppcastItem.m in Sources */, 7269E496264798200088C213 /* SPUSkippedUpdate.m in Sources */, 7269E49A2648F7C00088C213 /* SPUUserUpdateState.m in Sources */, 725602D61C83551C00DAA70E /* SUApplicationInfo.m in Sources */, 72DBA37F1D62C23E002594A8 /* SUCodeSigningVerifier.m in Sources */, 61299A6009CA6EB100B7442F /* SUConstants.m in Sources */, 7267E5E61D3D90AA00D1BF90 /* SUFileManager.m in Sources */, 61EF67560E25B58D00F754E0 /* SUHost.m in Sources */, 724BB3891D32B915005D534A /* SUInstallerConnection.m in Sources */, 72F94F5A1CC450DE002DEE68 /* SUInstallerLauncher.m in Sources */, 724BB3AA1D3347C2005D534A /* SUInstallerStatus.m in Sources */, 55C14F07136EF6DB00649790 /* SULog.m in Sources */, 726F2CE61BC9C33D001971A4 /* SUOperatingSystem.m in Sources */, 61A225A50D1C4AC000430CCD /* SUStandardVersionComparator.m in Sources */, 727F340B2605321D00020E85 /* SULog+NSError.m in Sources */, EA1E287022B66621004AA304 /* SUSignatures.m in Sources */, 7286EE6028CEC84900163C1D /* SUTextViewReleaseNotesView.m in Sources */, 6196CFFA09C72149000DC222 /* SUStatusController.m in Sources */, 72B3DECA1E23472200457642 /* SPUDownloadedUpdate.m in Sources */, 72AEB1D929A1CB510033883E /* SPUNoUpdateFoundInfo.m in Sources */, 61A2279D0D1CEE7600430CCD /* SUSystemProfiler.m in Sources */, 61B5FCDE09C52A9F00B25A18 /* SUUpdateAlert.m in Sources */, 612DCBB00D488BC60015DBEA /* SUUpdatePermissionPrompt.m in Sources */, 72A450541C69A68900D67EEA /* SUUpdatePermissionResponse.m in Sources */, 729F7EB0273F1840004592DC /* SPUUserAgent+Private.m in Sources */, 72F9EBE31D517E2F004AC8B6 /* SUUpdater.m in Sources */, 724BB3881D32A167005D534A /* SUXPCInstallerConnection.m in Sources */, 724BB3A91D33461B005D534A /* SUXPCInstallerStatus.m in Sources */, 723AC011259DBDAA00BDB4FA /* SUReleaseNotesCommon.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; EA1E280C22B64522004AA304 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( EA1E282122B64677004AA304 /* bscommon.c in Sources */, EA1E282222B64677004AA304 /* bsdiff.c in Sources */, EA1E282322B64677004AA304 /* bspatch.c in Sources */, EA1E282422B64677004AA304 /* sais.c in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; EA1E282A22B660BE004AA304 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( EA1E284622B660ED004AA304 /* fe.c in Sources */, EA1E284822B660ED004AA304 /* ge.c in Sources */, EA1E284722B660ED004AA304 /* verify.c in Sources */, EA1E284C22B660ED004AA304 /* add_scalar.c in Sources */, EA1E284D22B660ED004AA304 /* keypair.c in Sources */, EA1E284522B660ED004AA304 /* seed.c in Sources */, EA1E284B22B660ED004AA304 /* key_exchange.c in Sources */, EA1E284A22B660ED004AA304 /* sign.c in Sources */, EA1E284922B660ED004AA304 /* sc.c in Sources */, EA1E284E22B660ED004AA304 /* sha512.c in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; EA1E285A22B66487004AA304 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( 72C56E122EFEF10B0005A484 /* Signing.swift in Sources */, EA1E286122B66487004AA304 /* main.swift in Sources */, 72666DC72B0B28F4001511B0 /* Secret.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; EA1E287222B666EB004AA304 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( 72C56E0D2EFE002D0005A484 /* SPUExtractSignedFeed.m in Sources */, EA1E287922B666EB004AA304 /* main.swift in Sources */, 72666DC82B0B28F4001511B0 /* Secret.swift in Sources */, 72C56E112EFEF10B0005A484 /* Signing.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXSourcesBuildPhase section */ /* Begin PBXTargetDependency section */ 1454BA1619637EDB00344E57 /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = 61B5F90109C4CEE200B25A18 /* Sparkle Test App */; targetProxy = 1454BA1519637EDB00344E57 /* PBXContainerItemProxy */; }; 14732BCB1960F73500593899 /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = 8DC2EF4F0486A6940098B216 /* Sparkle */; targetProxy = 14732BCA1960F73500593899 /* PBXContainerItemProxy */; }; 14732BCF1960F73500593899 /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = 5D06E8CF0FD68C7C005AE3F6 /* BinaryDelta */; targetProxy = 14732BCE1960F73500593899 /* PBXContainerItemProxy */; }; 14950064195FB8A600BC5B5B /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = 8DC2EF4F0486A6940098B216 /* Sparkle */; targetProxy = 14950063195FB8A600BC5B5B /* PBXContainerItemProxy */; }; 14950066195FB8A600BC5B5B /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = 61B5F90109C4CEE200B25A18 /* Sparkle Test App */; targetProxy = 14950065195FB8A600BC5B5B /* PBXContainerItemProxy */; }; 14950068195FB8A600BC5B5B /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = 612279D80DB5470200AB99EA /* Sparkle Unit Tests */; targetProxy = 14950067195FB8A600BC5B5B /* PBXContainerItemProxy */; }; 1495006A195FB8A600BC5B5B /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = 5D06E8CF0FD68C7C005AE3F6 /* BinaryDelta */; targetProxy = 14950069195FB8A600BC5B5B /* PBXContainerItemProxy */; }; 376769AF23442F320077B8F7 /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = EA1E280E22B64522004AA304 /* bsdiff */; targetProxy = 376769AE23442F320077B8F7 /* PBXContainerItemProxy */; }; 376769B123442F490077B8F7 /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = EA1E280E22B64522004AA304 /* bsdiff */; targetProxy = 376769B023442F490077B8F7 /* PBXContainerItemProxy */; }; 37DC0CE62340667000501A67 /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = EA1E280E22B64522004AA304 /* bsdiff */; targetProxy = 37DC0CE52340667000501A67 /* PBXContainerItemProxy */; }; 5A06357223FE332300478A72 /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = EA1E282C22B660BE004AA304 /* ed25519 */; targetProxy = 5A06357123FE332300478A72 /* PBXContainerItemProxy */; }; 61B5F91C09C4CF7200B25A18 /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = 8DC2EF4F0486A6940098B216 /* Sparkle */; targetProxy = 61B5F91B09C4CF7200B25A18 /* PBXContainerItemProxy */; }; 61FA528D0E2D9EB200EF58AD /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = 8DC2EF4F0486A6940098B216 /* Sparkle */; targetProxy = 61FA528C0E2D9EB200EF58AD /* PBXContainerItemProxy */; }; 72045CD826FEE471004F96E5 /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = 726E07EE1CAF37BD001A286B /* SparkleDownloader */; targetProxy = 72045CD726FEE471004F96E5 /* PBXContainerItemProxy */; }; 72045CDA26FEE471004F96E5 /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = 724BB36B1D31D0B7005D534A /* SparkleInstallerConnection */; targetProxy = 72045CD926FEE471004F96E5 /* PBXContainerItemProxy */; }; 72045CDC26FEE471004F96E5 /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = 726E07AC1CAF08D6001A286B /* SparkleInstallerLauncher */; targetProxy = 72045CDB26FEE471004F96E5 /* PBXContainerItemProxy */; }; 72045CDE26FEE471004F96E5 /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = 724BB3921D333832005D534A /* SparkleInstallerStatus */; targetProxy = 72045CDD26FEE471004F96E5 /* PBXContainerItemProxy */; }; 7205C4691E1306FB00E370AE /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = 7205C43D1E13049400E370AE /* generate_appcast */; targetProxy = 7205C4681E1306FB00E370AE /* PBXContainerItemProxy */; }; 7205C46B1E13070300E370AE /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = 7205C43D1E13049400E370AE /* generate_appcast */; targetProxy = 7205C46A1E13070300E370AE /* PBXContainerItemProxy */; }; 7218EC332623ED94008FECF3 /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = 724BB36B1D31D0B7005D534A /* SparkleInstallerConnection */; targetProxy = 7218EC322623ED94008FECF3 /* PBXContainerItemProxy */; }; 7218EC352623ED97008FECF3 /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = 724BB3921D333832005D534A /* SparkleInstallerStatus */; targetProxy = 7218EC342623ED97008FECF3 /* PBXContainerItemProxy */; }; 725148E9266D918900247C9C /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = 726E07EE1CAF37BD001A286B /* SparkleDownloader */; targetProxy = 725148E8266D918900247C9C /* PBXContainerItemProxy */; }; 726B2B631C645FC900388755 /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = 61B5F90109C4CEE200B25A18 /* Sparkle Test App */; targetProxy = 726B2B621C645FC900388755 /* PBXContainerItemProxy */; }; 726E4A201C86C88F00C57C6A /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = 726E4A151C86C88F00C57C6A /* TestAppHelper */; targetProxy = 726E4A1F1C86C88F00C57C6A /* PBXContainerItemProxy */; }; 726F168926747CEB005BEA89 /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = 8DC2EF4F0486A6940098B216 /* Sparkle */; targetProxy = 726F168826747CEB005BEA89 /* PBXContainerItemProxy */; }; 72A5D5AC1D6929260009E5AC /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = 72B398D11D3D879300EE297F /* Autoupdate */; targetProxy = 72A5D5AB1D6929260009E5AC /* PBXContainerItemProxy */; }; 72CCCC15287B3D1900E7156B /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = 8DC2EF4F0486A6940098B216 /* Sparkle */; targetProxy = 72CCCC14287B3D1900E7156B /* PBXContainerItemProxy */; }; 72CCCC17287B3D3400E7156B /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = EA1E280E22B64522004AA304 /* bsdiff */; targetProxy = 72CCCC16287B3D3400E7156B /* PBXContainerItemProxy */; }; 72CCCC19287B3D3D00E7156B /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = EA1E282C22B660BE004AA304 /* ed25519 */; targetProxy = 72CCCC18287B3D3D00E7156B /* PBXContainerItemProxy */; }; 72CCCC1B287B3D9000E7156B /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = EA1E282C22B660BE004AA304 /* ed25519 */; targetProxy = 72CCCC1A287B3D9000E7156B /* PBXContainerItemProxy */; }; 72CCCC1D287B3D9600E7156B /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = EA1E282C22B660BE004AA304 /* ed25519 */; targetProxy = 72CCCC1C287B3D9600E7156B /* PBXContainerItemProxy */; }; 72CCCC1F287B3DA100E7156B /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = EA1E282C22B660BE004AA304 /* ed25519 */; targetProxy = 72CCCC1E287B3DA100E7156B /* PBXContainerItemProxy */; }; 72CCCC21287B3DA400E7156B /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = EA1E280E22B64522004AA304 /* bsdiff */; targetProxy = 72CCCC20287B3DA400E7156B /* PBXContainerItemProxy */; }; 72D954BA1CBB6E27006F28BD /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = 721C24441CB753E6005440CB /* Installer Progress */; targetProxy = 72D954B91CBB6E27006F28BD /* PBXContainerItemProxy */; }; 72EF30B926747E39008CE987 /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = 8DC2EF4F0486A6940098B216 /* Sparkle */; targetProxy = 72EF30B826747E39008CE987 /* PBXContainerItemProxy */; }; 72F94F6B1CC4A0C8002DEE68 /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = 726E07AC1CAF08D6001A286B /* SparkleInstallerLauncher */; targetProxy = 72F94F6A1CC4A0C8002DEE68 /* PBXContainerItemProxy */; }; 895C5DC724D78F700058A82D /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = 895C5DC024D78E210058A82D /* XCFrameworks */; targetProxy = 895C5DC624D78F700058A82D /* PBXContainerItemProxy */; }; FA344BA524C8699E00B2A401 /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = EA1E287522B666EB004AA304 /* sign_update */; targetProxy = FA344BA424C8699E00B2A401 /* PBXContainerItemProxy */; }; FA344BA724C869A300B2A401 /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = EA1E285D22B66487004AA304 /* generate_keys */; targetProxy = FA344BA624C869A300B2A401 /* PBXContainerItemProxy */; }; /* End PBXTargetDependency section */ /* Begin PBXVariantGroup section */ 61AAE8220A321A7F00D8810D /* Sparkle.strings */ = { isa = PBXVariantGroup; children = ( 147D6DA71B66EC1C006607AB /* ca */, 615409C4103BBC4000125AF1 /* cs */, 61131A050F846CE600E97AF6 /* da */, 619B17200E1E9D0800E72754 /* de */, 555CF29A196C52330000B31E /* el */, 61AAE84F0A321AF700D8810D /* es */, 147D6DA91B66EC22006607AB /* fi */, 61AAE8590A321B0400D8810D /* fr */, 147D6DAA1B66EC25006607AB /* he */, 613151B20FB4946A000DCD59 /* is */, 61F614540E24A12D009F47E7 /* it */, 611A904610240DF700CC659E /* ja */, FE5536F517A2C6A7007CB333 /* ko */, 4607BEA21948443800EF8DA4 /* nb */, 61AAE8710A321F7700D8810D /* nl */, 611A904210240DD300CC659E /* pl */, 61E31A80103299500051D188 /* pt-BR */, 6186554310D7484E00B1E074 /* pt-PT */, 004A8652192A492B00C9730D /* ro */, 6195D4920E404AD700D41A50 /* ru */, FE5536F617A2C6AB007CB333 /* sk */, 61BA66CC14BDFA0400D02D86 /* sl */, 618915730E35937600B5E981 /* sv */, 61F3AC1215C22D4A00260CA2 /* th */, 5AEF45D9189D1CC90030D7DC /* tr */, 0263187214FEBB31005EBF43 /* uk */, 61131A090F846D0A00E97AF6 /* zh_CN */, 61131A0A0F846D1100E97AF6 /* zh_TW */, 723ABCD7259A9A9D00BDB4FA /* Base */, 72E1DACB25B3E8DF0001BA6D /* hr */, 726FD2CC25F4BE5F00123BC6 /* fa */, F67C9B7B281410B600740813 /* hu */, F6E11510281410D10003736C /* ar */, 37A5F28528E9219000891504 /* zh_HK */, 728FBA1C2BB5013300651EDF /* nn */, E0949FD02EFC4CFD0039C748 /* vi */, ); name = Sparkle.strings; sourceTree = "<group>"; }; 723ABDD9259A9E8600BDB4FA /* InfoPlist.strings */ = { isa = PBXVariantGroup; children = ( 723ABDDA259A9E8600BDB4FA /* Base */, ); name = InfoPlist.strings; sourceTree = "<group>"; }; 723ABE00259A9E9E00BDB4FA /* MainMenu.xib */ = { isa = PBXVariantGroup; children = ( 723ABE01259A9E9E00BDB4FA /* Base */, 723ABE15259A9EB000BDB4FA /* en */, 723ABE16259A9EBE00BDB4FA /* ar */, 723ABE17259A9ECA00BDB4FA /* ca */, 723ABE18259A9ECF00BDB4FA /* zh_CN */, 723ABE19259A9ED200BDB4FA /* zh_TW */, 723ABE1A259A9ED600BDB4FA /* cs */, 723ABE1B259A9ED800BDB4FA /* da */, 723ABE1C259A9EDB00BDB4FA /* nl */, 723ABE1D259A9EDE00BDB4FA /* fi */, 723ABE1E259A9EE100BDB4FA /* fr */, 723ABE21259A9EEB00BDB4FA /* de */, 723ABE22259A9EEE00BDB4FA /* el */, 723ABE23259A9EF200BDB4FA /* he */, 723ABE24259A9EF600BDB4FA /* hu */, 723ABE25259A9EF800BDB4FA /* is */, 723ABE28259A9F0100BDB4FA /* it */, 723ABE29259A9F0300BDB4FA /* ja */, 723ABE2A259A9F0600BDB4FA /* ko */, 723ABE2B259A9F0800BDB4FA /* nb */, 723ABE2C259A9F0B00BDB4FA /* pl */, 723ABE2D259A9F0F00BDB4FA /* pt-BR */, 723ABE2E259A9F1100BDB4FA /* pt-PT */, 723ABE2F259A9F1400BDB4FA /* ro */, 723ABE30259A9F1700BDB4FA /* ru */, 723ABE31259A9F1A00BDB4FA /* sk */, 723ABE34259A9F2100BDB4FA /* sl */, 723ABE35259A9F2400BDB4FA /* es */, 723ABE36259A9F2600BDB4FA /* sv */, 723ABE37259A9F2900BDB4FA /* th */, 723ABE38259A9F2B00BDB4FA /* tr */, 723ABE39259A9F2E00BDB4FA /* uk */, 72E1DAB725B3E8AE0001BA6D /* hr */, 726FD2CB25F4BE5F00123BC6 /* fa */, 37A5F28828E9219000891504 /* zh_HK */, 728FBA1B2BB5013300651EDF /* nn */, E0949FCF2EFC4CFC0039C748 /* vi */, ); name = MainMenu.xib; sourceTree = "<group>"; }; /* End PBXVariantGroup section */ /* Begin XCBuildConfiguration section */ 14732BC71960F69300593899 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { }; name = Debug; }; 14732BC81960F69300593899 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { }; name = Release; }; 14950061195FB89500BC5B5B /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { }; name = Debug; }; 14950062195FB89500BC5B5B /* Release */ = { isa = XCBuildConfiguration; buildSettings = { }; name = Release; }; 149B785A1B7D398100D7D62C /* Coverage */ = { isa = XCBuildConfiguration; baseConfigurationReference = 149B78631B7D3A0C00D7D62C /* ConfigCommonCoverage.xcconfig */; buildSettings = { }; name = Coverage; }; 149B785B1B7D398100D7D62C /* Coverage */ = { isa = XCBuildConfiguration; baseConfigurationReference = FA1941CA0D94A70100DD942E /* ConfigFrameworkDebug.xcconfig */; buildSettings = { }; name = Coverage; }; 149B785C1B7D398100D7D62C /* Coverage */ = { isa = XCBuildConfiguration; baseConfigurationReference = 72F94EDA1CC36C37002DEE68 /* ConfigTestAppDebug.xcconfig */; buildSettings = { }; name = Coverage; }; 149B785D1B7D398100D7D62C /* Coverage */ = { isa = XCBuildConfiguration; baseConfigurationReference = 149B78641B7D3A4800D7D62C /* ConfigUnitTestCoverage.xcconfig */; buildSettings = { }; name = Coverage; }; 149B785E1B7D398100D7D62C /* Coverage */ = { isa = XCBuildConfiguration; baseConfigurationReference = EA1E286722B664FD004AA304 /* CommandLineTool-Release.xcconfig */; buildSettings = { }; name = Coverage; }; 149B78601B7D398100D7D62C /* Coverage */ = { isa = XCBuildConfiguration; buildSettings = { }; name = Coverage; }; 149B78621B7D398100D7D62C /* Coverage */ = { isa = XCBuildConfiguration; buildSettings = { }; name = Coverage; }; 1DEB91AE08733DA50010E9CD /* Debug */ = { isa = XCBuildConfiguration; baseConfigurationReference = FA1941CA0D94A70100DD942E /* ConfigFrameworkDebug.xcconfig */; buildSettings = { }; name = Debug; }; 1DEB91AF08733DA50010E9CD /* Release */ = { isa = XCBuildConfiguration; baseConfigurationReference = 72A40F082956120D007C7DD5 /* ConfigFrameworkRelease.xcconfig */; buildSettings = { }; name = Release; }; 1DEB91B208733DA50010E9CD /* Debug */ = { isa = XCBuildConfiguration; baseConfigurationReference = FA1941CF0D94A70100DD942E /* ConfigCommonDebug.xcconfig */; buildSettings = { }; name = Debug; }; 1DEB91B308733DA50010E9CD /* Release */ = { isa = XCBuildConfiguration; baseConfigurationReference = FA1941CC0D94A70100DD942E /* ConfigCommonRelease.xcconfig */; buildSettings = { }; name = Release; }; 5D06E8D20FD68C7D005AE3F6 /* Debug */ = { isa = XCBuildConfiguration; baseConfigurationReference = EA1E286822B664FD004AA304 /* CommandLineTool-Debug.xcconfig */; buildSettings = { }; name = Debug; }; 5D06E8D30FD68C7D005AE3F6 /* Release */ = { isa = XCBuildConfiguration; baseConfigurationReference = EA1E286722B664FD004AA304 /* CommandLineTool-Release.xcconfig */; buildSettings = { }; name = Release; }; 612279DB0DB5470300AB99EA /* Debug */ = { isa = XCBuildConfiguration; baseConfigurationReference = 7205C46C1E13244800E370AE /* ConfigUnitTestDebug.xcconfig */; buildSettings = { }; name = Debug; }; 612279DC0DB5470300AB99EA /* Release */ = { isa = XCBuildConfiguration; baseConfigurationReference = 7205C46D1E13245600E370AE /* ConfigUnitTestRelease.xcconfig */; buildSettings = { }; name = Release; }; 61B5F90609C4CEE300B25A18 /* Debug */ = { isa = XCBuildConfiguration; baseConfigurationReference = 72F94EDA1CC36C37002DEE68 /* ConfigTestAppDebug.xcconfig */; buildSettings = { }; name = Debug; }; 61B5F90709C4CEE300B25A18 /* Release */ = { isa = XCBuildConfiguration; baseConfigurationReference = FA1941CD0D94A70100DD942E /* ConfigTestApp.xcconfig */; buildSettings = { }; name = Release; }; 7205C4421E13049400E370AE /* Debug */ = { isa = XCBuildConfiguration; baseConfigurationReference = EA1E286822B664FD004AA304 /* CommandLineTool-Debug.xcconfig */; buildSettings = { }; name = Debug; }; 7205C4431E13049400E370AE /* Coverage */ = { isa = XCBuildConfiguration; baseConfigurationReference = EA1E286722B664FD004AA304 /* CommandLineTool-Release.xcconfig */; buildSettings = { }; name = Coverage; }; 7205C4441E13049400E370AE /* Release */ = { isa = XCBuildConfiguration; baseConfigurationReference = EA1E286722B664FD004AA304 /* CommandLineTool-Release.xcconfig */; buildSettings = { }; name = Release; }; 721C24531CB753E7005440CB /* Debug */ = { isa = XCBuildConfiguration; baseConfigurationReference = 721C24571CB754E8005440CB /* ConfigInstallerProgress.xcconfig */; buildSettings = { }; name = Debug; }; 721C24541CB753E7005440CB /* Coverage */ = { isa = XCBuildConfiguration; baseConfigurationReference = 721C24571CB754E8005440CB /* ConfigInstallerProgress.xcconfig */; buildSettings = { }; name = Coverage; }; 721C24551CB753E7005440CB /* Release */ = { isa = XCBuildConfiguration; baseConfigurationReference = 721C24571CB754E8005440CB /* ConfigInstallerProgress.xcconfig */; buildSettings = { }; name = Release; }; 724BB3791D31D0B7005D534A /* Debug */ = { isa = XCBuildConfiguration; baseConfigurationReference = 724BB37F1D31D1EA005D534A /* ConfigInstallerConnectionDebug.xcconfig */; buildSettings = { }; name = Debug; }; 724BB37A1D31D0B7005D534A /* Coverage */ = { isa = XCBuildConfiguration; baseConfigurationReference = 724BB37E1D31D1EA005D534A /* ConfigInstallerConnection.xcconfig */; buildSettings = { }; name = Coverage; }; 724BB37B1D31D0B7005D534A /* Release */ = { isa = XCBuildConfiguration; baseConfigurationReference = 724BB37E1D31D1EA005D534A /* ConfigInstallerConnection.xcconfig */; buildSettings = { }; name = Release; }; 724BB3A01D333832005D534A /* Debug */ = { isa = XCBuildConfiguration; baseConfigurationReference = 724BB3A51D3338C8005D534A /* ConfigInstallerStatusDebug.xcconfig */; buildSettings = { }; name = Debug; }; 724BB3A11D333832005D534A /* Coverage */ = { isa = XCBuildConfiguration; baseConfigurationReference = 724BB3A41D3338C8005D534A /* ConfigInstallerStatus.xcconfig */; buildSettings = { }; name = Coverage; }; 724BB3A21D333832005D534A /* Release */ = { isa = XCBuildConfiguration; baseConfigurationReference = 724BB3A41D3338C8005D534A /* ConfigInstallerStatus.xcconfig */; buildSettings = { }; name = Release; }; 726B2B641C645FC900388755 /* Debug */ = { isa = XCBuildConfiguration; baseConfigurationReference = 7205C46E1E13254500E370AE /* ConfigUITestDebug.xcconfig */; buildSettings = { }; name = Debug; }; 726B2B651C645FC900388755 /* Coverage */ = { isa = XCBuildConfiguration; baseConfigurationReference = 729F10FE1C65A9B500DFCCC5 /* ConfigUITestCoverage.xcconfig */; buildSettings = { }; name = Coverage; }; 726B2B661C645FC900388755 /* Release */ = { isa = XCBuildConfiguration; baseConfigurationReference = 7205C46F1E13255900E370AE /* ConfigUITestRelease.xcconfig */; buildSettings = { }; name = Release; }; 726E07BA1CAF08D6001A286B /* Debug */ = { isa = XCBuildConfiguration; baseConfigurationReference = 72F94ED51CC3441A002DEE68 /* ConfigInstallerLauncherDebug.xcconfig */; buildSettings = { }; name = Debug; }; 726E07BB1CAF08D6001A286B /* Coverage */ = { isa = XCBuildConfiguration; baseConfigurationReference = 72F94ED51CC3441A002DEE68 /* ConfigInstallerLauncherDebug.xcconfig */; buildSettings = { }; name = Coverage; }; 726E07BC1CAF08D6001A286B /* Release */ = { isa = XCBuildConfiguration; baseConfigurationReference = 726E07C11CAF1E79001A286B /* ConfigInstallerLauncher.xcconfig */; buildSettings = { }; name = Release; }; 726E07FC1CAF37BD001A286B /* Debug */ = { isa = XCBuildConfiguration; baseConfigurationReference = 72F94ED61CC344A7002DEE68 /* ConfigDownloaderDebug.xcconfig */; buildSettings = { CLANG_ANALYZER_NONNULL = YES; }; name = Debug; }; 726E07FD1CAF37BD001A286B /* Coverage */ = { isa = XCBuildConfiguration; baseConfigurationReference = 72F94ED61CC344A7002DEE68 /* ConfigDownloaderDebug.xcconfig */; buildSettings = { CLANG_ANALYZER_NONNULL = YES; }; name = Coverage; }; 726E07FE1CAF37BD001A286B /* Release */ = { isa = XCBuildConfiguration; baseConfigurationReference = 728638EE1CAF589C00783084 /* ConfigDownloader.xcconfig */; buildSettings = { CLANG_ANALYZER_NONNULL = YES; }; name = Release; }; 726E4A231C86C88F00C57C6A /* Debug */ = { isa = XCBuildConfiguration; baseConfigurationReference = 72F94EDB1CC36C6F002DEE68 /* ConfigTestAppHelperDebug.xcconfig */; buildSettings = { }; name = Debug; }; 726E4A241C86C88F00C57C6A /* Coverage */ = { isa = XCBuildConfiguration; baseConfigurationReference = 72F94EDB1CC36C6F002DEE68 /* ConfigTestAppHelperDebug.xcconfig */; buildSettings = { }; name = Coverage; }; 726E4A251C86C88F00C57C6A /* Release */ = { isa = XCBuildConfiguration; baseConfigurationReference = 726E4A281C86CAB500C57C6A /* ConfigTestAppHelper.xcconfig */; buildSettings = { }; name = Release; }; 72B398D71D3D879400EE297F /* Debug */ = { isa = XCBuildConfiguration; baseConfigurationReference = FA1941CE0D94A70100DD942E /* ConfigRelaunch.xcconfig */; buildSettings = { }; name = Debug; }; 72B398D81D3D879400EE297F /* Coverage */ = { isa = XCBuildConfiguration; baseConfigurationReference = FA1941CE0D94A70100DD942E /* ConfigRelaunch.xcconfig */; buildSettings = { }; name = Coverage; }; 72B398D91D3D879400EE297F /* Release */ = { isa = XCBuildConfiguration; baseConfigurationReference = FA1941CE0D94A70100DD942E /* ConfigRelaunch.xcconfig */; buildSettings = { }; name = Release; }; 72D954AC1CBB415C006F28BD /* Debug */ = { isa = XCBuildConfiguration; baseConfigurationReference = 72D954B01CBB41E2006F28BD /* ConfigSparkleTool.xcconfig */; buildSettings = { CLANG_ANALYZER_NONNULL = YES; }; name = Debug; }; 72D954AD1CBB415C006F28BD /* Coverage */ = { isa = XCBuildConfiguration; baseConfigurationReference = 72D954B01CBB41E2006F28BD /* ConfigSparkleTool.xcconfig */; buildSettings = { CLANG_ANALYZER_NONNULL = YES; }; name = Coverage; }; 72D954AE1CBB415C006F28BD /* Release */ = { isa = XCBuildConfiguration; baseConfigurationReference = 72D954B01CBB41E2006F28BD /* ConfigSparkleTool.xcconfig */; buildSettings = { CLANG_ANALYZER_NONNULL = YES; }; name = Release; }; 895C5DC124D78E210058A82D /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { }; name = Debug; }; 895C5DC224D78E210058A82D /* Coverage */ = { isa = XCBuildConfiguration; buildSettings = { }; name = Coverage; }; 895C5DC324D78E210058A82D /* Release */ = { isa = XCBuildConfiguration; buildSettings = { }; name = Release; }; EA1E281022B64522004AA304 /* Debug */ = { isa = XCBuildConfiguration; baseConfigurationReference = EA1E281422B64548004AA304 /* bsdiff-Debug.xcconfig */; buildSettings = { }; name = Debug; }; EA1E281122B64522004AA304 /* Coverage */ = { isa = XCBuildConfiguration; baseConfigurationReference = EA1E281522B64549004AA304 /* bsdiff-Release.xcconfig */; buildSettings = { }; name = Coverage; }; EA1E281222B64522004AA304 /* Release */ = { isa = XCBuildConfiguration; baseConfigurationReference = EA1E281522B64549004AA304 /* bsdiff-Release.xcconfig */; buildSettings = { }; name = Release; }; EA1E282F22B660BE004AA304 /* Debug */ = { isa = XCBuildConfiguration; baseConfigurationReference = EA1E285822B6622E004AA304 /* ed25519-Debug.xcconfig */; buildSettings = { }; name = Debug; }; EA1E283022B660BE004AA304 /* Coverage */ = { isa = XCBuildConfiguration; baseConfigurationReference = EA1E285722B6622E004AA304 /* ed25519-Release.xcconfig */; buildSettings = { }; name = Coverage; }; EA1E283122B660BE004AA304 /* Release */ = { isa = XCBuildConfiguration; baseConfigurationReference = EA1E285722B6622E004AA304 /* ed25519-Release.xcconfig */; buildSettings = { }; name = Release; }; EA1E286322B66487004AA304 /* Debug */ = { isa = XCBuildConfiguration; baseConfigurationReference = EA1E286822B664FD004AA304 /* CommandLineTool-Debug.xcconfig */; buildSettings = { }; name = Debug; }; EA1E286422B66487004AA304 /* Coverage */ = { isa = XCBuildConfiguration; baseConfigurationReference = EA1E286722B664FD004AA304 /* CommandLineTool-Release.xcconfig */; buildSettings = { }; name = Coverage; }; EA1E286522B66487004AA304 /* Release */ = { isa = XCBuildConfiguration; baseConfigurationReference = EA1E286722B664FD004AA304 /* CommandLineTool-Release.xcconfig */; buildSettings = { }; name = Release; }; EA1E287A22B666EB004AA304 /* Debug */ = { isa = XCBuildConfiguration; baseConfigurationReference = EA1E286822B664FD004AA304 /* CommandLineTool-Debug.xcconfig */; buildSettings = { }; name = Debug; }; EA1E287B22B666EB004AA304 /* Coverage */ = { isa = XCBuildConfiguration; baseConfigurationReference = EA1E286722B664FD004AA304 /* CommandLineTool-Release.xcconfig */; buildSettings = { }; name = Coverage; }; EA1E287C22B666EB004AA304 /* Release */ = { isa = XCBuildConfiguration; baseConfigurationReference = EA1E286722B664FD004AA304 /* CommandLineTool-Release.xcconfig */; buildSettings = { }; name = Release; }; /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ 14732BC61960F69300593899 /* Build configuration list for PBXLegacyTarget "Distribution" */ = { isa = XCConfigurationList; buildConfigurations = ( 14732BC71960F69300593899 /* Debug */, 149B78601B7D398100D7D62C /* Coverage */, 14732BC81960F69300593899 /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; 14950060195FB89500BC5B5B /* Build configuration list for PBXAggregateTarget "All" */ = { isa = XCConfigurationList; buildConfigurations = ( 14950061195FB89500BC5B5B /* Debug */, 149B78621B7D398100D7D62C /* Coverage */, 14950062195FB89500BC5B5B /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; 1DEB91AD08733DA50010E9CD /* Build configuration list for PBXNativeTarget "Sparkle" */ = { isa = XCConfigurationList; buildConfigurations = ( 1DEB91AE08733DA50010E9CD /* Debug */, 149B785B1B7D398100D7D62C /* Coverage */, 1DEB91AF08733DA50010E9CD /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; 1DEB91B108733DA50010E9CD /* Build configuration list for PBXProject "Sparkle" */ = { isa = XCConfigurationList; buildConfigurations = ( 1DEB91B208733DA50010E9CD /* Debug */, 149B785A1B7D398100D7D62C /* Coverage */, 1DEB91B308733DA50010E9CD /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; 5D06E8DA0FD68C95005AE3F6 /* Build configuration list for PBXNativeTarget "BinaryDelta" */ = { isa = XCConfigurationList; buildConfigurations = ( 5D06E8D20FD68C7D005AE3F6 /* Debug */, 149B785E1B7D398100D7D62C /* Coverage */, 5D06E8D30FD68C7D005AE3F6 /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; 612279DD0DB5470300AB99EA /* Build configuration list for PBXNativeTarget "Sparkle Unit Tests" */ = { isa = XCConfigurationList; buildConfigurations = ( 612279DB0DB5470300AB99EA /* Debug */, 149B785D1B7D398100D7D62C /* Coverage */, 612279DC0DB5470300AB99EA /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; 61B5F90509C4CEE300B25A18 /* Build configuration list for PBXNativeTarget "Sparkle Test App" */ = { isa = XCConfigurationList; buildConfigurations = ( 61B5F90609C4CEE300B25A18 /* Debug */, 149B785C1B7D398100D7D62C /* Coverage */, 61B5F90709C4CEE300B25A18 /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; 7205C4451E13049400E370AE /* Build configuration list for PBXNativeTarget "generate_appcast" */ = { isa = XCConfigurationList; buildConfigurations = ( 7205C4421E13049400E370AE /* Debug */, 7205C4431E13049400E370AE /* Coverage */, 7205C4441E13049400E370AE /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; 721C24561CB753E7005440CB /* Build configuration list for PBXNativeTarget "Installer Progress" */ = { isa = XCConfigurationList; buildConfigurations = ( 721C24531CB753E7005440CB /* Debug */, 721C24541CB753E7005440CB /* Coverage */, 721C24551CB753E7005440CB /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; 724BB3781D31D0B7005D534A /* Build configuration list for PBXNativeTarget "SparkleInstallerConnection" */ = { isa = XCConfigurationList; buildConfigurations = ( 724BB3791D31D0B7005D534A /* Debug */, 724BB37A1D31D0B7005D534A /* Coverage */, 724BB37B1D31D0B7005D534A /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; 724BB39F1D333832005D534A /* Build configuration list for PBXNativeTarget "SparkleInstallerStatus" */ = { isa = XCConfigurationList; buildConfigurations = ( 724BB3A01D333832005D534A /* Debug */, 724BB3A11D333832005D534A /* Coverage */, 724BB3A21D333832005D534A /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; 726B2B671C645FC900388755 /* Build configuration list for PBXNativeTarget "UI Tests" */ = { isa = XCConfigurationList; buildConfigurations = ( 726B2B641C645FC900388755 /* Debug */, 726B2B651C645FC900388755 /* Coverage */, 726B2B661C645FC900388755 /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; 726E07B91CAF08D6001A286B /* Build configuration list for PBXNativeTarget "SparkleInstallerLauncher" */ = { isa = XCConfigurationList; buildConfigurations = ( 726E07BA1CAF08D6001A286B /* Debug */, 726E07BB1CAF08D6001A286B /* Coverage */, 726E07BC1CAF08D6001A286B /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; 726E07FB1CAF37BD001A286B /* Build configuration list for PBXNativeTarget "SparkleDownloader" */ = { isa = XCConfigurationList; buildConfigurations = ( 726E07FC1CAF37BD001A286B /* Debug */, 726E07FD1CAF37BD001A286B /* Coverage */, 726E07FE1CAF37BD001A286B /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; 726E4A221C86C88F00C57C6A /* Build configuration list for PBXNativeTarget "TestAppHelper" */ = { isa = XCConfigurationList; buildConfigurations = ( 726E4A231C86C88F00C57C6A /* Debug */, 726E4A241C86C88F00C57C6A /* Coverage */, 726E4A251C86C88F00C57C6A /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; 72B398D61D3D879400EE297F /* Build configuration list for PBXNativeTarget "Autoupdate" */ = { isa = XCConfigurationList; buildConfigurations = ( 72B398D71D3D879400EE297F /* Debug */, 72B398D81D3D879400EE297F /* Coverage */, 72B398D91D3D879400EE297F /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; 72D954AF1CBB415C006F28BD /* Build configuration list for PBXNativeTarget "sparkle-cli" */ = { isa = XCConfigurationList; buildConfigurations = ( 72D954AC1CBB415C006F28BD /* Debug */, 72D954AD1CBB415C006F28BD /* Coverage */, 72D954AE1CBB415C006F28BD /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; 895C5DC424D78E210058A82D /* Build configuration list for PBXAggregateTarget "XCFrameworks" */ = { isa = XCConfigurationList; buildConfigurations = ( 895C5DC124D78E210058A82D /* Debug */, 895C5DC224D78E210058A82D /* Coverage */, 895C5DC324D78E210058A82D /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; EA1E281322B64522004AA304 /* Build configuration list for PBXNativeTarget "bsdiff" */ = { isa = XCConfigurationList; buildConfigurations = ( EA1E281022B64522004AA304 /* Debug */, EA1E281122B64522004AA304 /* Coverage */, EA1E281222B64522004AA304 /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; EA1E282E22B660BE004AA304 /* Build configuration list for PBXNativeTarget "ed25519" */ = { isa = XCConfigurationList; buildConfigurations = ( EA1E282F22B660BE004AA304 /* Debug */, EA1E283022B660BE004AA304 /* Coverage */, EA1E283122B660BE004AA304 /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; EA1E286222B66487004AA304 /* Build configuration list for PBXNativeTarget "generate_keys" */ = { isa = XCConfigurationList; buildConfigurations = ( EA1E286322B66487004AA304 /* Debug */, EA1E286422B66487004AA304 /* Coverage */, EA1E286522B66487004AA304 /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; EA1E287D22B666EB004AA304 /* Build configuration list for PBXNativeTarget "sign_update" */ = { isa = XCConfigurationList; buildConfigurations = ( EA1E287A22B666EB004AA304 /* Debug */, EA1E287B22B666EB004AA304 /* Coverage */, EA1E287C22B666EB004AA304 /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; /* End XCConfigurationList section */ /* Begin XCRemoteSwiftPackageReference section */ 727DBAE326B5BBFD00111F0C /* XCRemoteSwiftPackageReference "swift-argument-parser" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/apple/swift-argument-parser.git"; requirement = { kind = upToNextMajorVersion; minimumVersion = 0.4.3; }; }; /* End XCRemoteSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ 725C2EAB2782EF3C007CB7B5 /* ArgumentParser */ = { isa = XCSwiftPackageProductDependency; package = 727DBAE326B5BBFD00111F0C /* XCRemoteSwiftPackageReference "swift-argument-parser" */; productName = ArgumentParser; }; 727DBAE426B5BBFD00111F0C /* ArgumentParser */ = { isa = XCSwiftPackageProductDependency; package = 727DBAE326B5BBFD00111F0C /* XCRemoteSwiftPackageReference "swift-argument-parser" */; productName = ArgumentParser; }; 727DBAE626B5C47800111F0C /* ArgumentParser */ = { isa = XCSwiftPackageProductDependency; package = 727DBAE326B5BBFD00111F0C /* XCRemoteSwiftPackageReference "swift-argument-parser" */; productName = ArgumentParser; }; 727DBAE826B5C48A00111F0C /* ArgumentParser */ = { isa = XCSwiftPackageProductDependency; package = 727DBAE326B5BBFD00111F0C /* XCRemoteSwiftPackageReference "swift-argument-parser" */; productName = ArgumentParser; }; /* End XCSwiftPackageProductDependency section */ }; rootObject = 0867D690FE84028FC02AAC07 /* Project object */; } ================================================ FILE: Sparkle.xcodeproj/project.xcworkspace/contents.xcworkspacedata ================================================ <?xml version="1.0" encoding="UTF-8"?> <Workspace version = "1.0"> <FileRef location = "self:"> </FileRef> </Workspace> ================================================ FILE: Sparkle.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist ================================================ <?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <plist version="1.0"> <dict> <key>IDEDidComputeMac32BitWarning</key> <true/> </dict> </plist> ================================================ FILE: Sparkle.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved ================================================ { "pins" : [ { "identity" : "swift-argument-parser", "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-argument-parser.git", "state" : { "revision" : "83b23d940471b313427da226196661856f6ba3e0", "version" : "0.4.4" } } ], "version" : 2 } ================================================ FILE: Sparkle.xcodeproj/xcshareddata/xcschemes/BinaryDelta.xcscheme ================================================ <?xml version="1.0" encoding="UTF-8"?> <Scheme LastUpgradeVersion = "1630" version = "1.3"> <BuildAction parallelizeBuildables = "YES" buildImplicitDependencies = "NO"> <BuildActionEntries> <BuildActionEntry buildForTesting = "YES" buildForRunning = "YES" buildForProfiling = "YES" buildForArchiving = "YES" buildForAnalyzing = "YES"> <BuildableReference BuildableIdentifier = "primary" BlueprintIdentifier = "5D06E8CF0FD68C7C005AE3F6" BuildableName = "BinaryDelta" BlueprintName = "BinaryDelta" ReferencedContainer = "container:Sparkle.xcodeproj"> </BuildableReference> </BuildActionEntry> </BuildActionEntries> </BuildAction> <TestAction buildConfiguration = "Debug" selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" shouldUseLaunchSchemeArgsEnv = "YES"> <Testables> </Testables> </TestAction> <LaunchAction buildConfiguration = "Debug" selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" launchStyle = "0" useCustomWorkingDirectory = "NO" ignoresPersistentStateOnLaunch = "NO" debugDocumentVersioning = "YES" debugServiceExtension = "internal" allowLocationSimulation = "YES"> <BuildableProductRunnable runnableDebuggingMode = "0"> <BuildableReference BuildableIdentifier = "primary" BlueprintIdentifier = "5D06E8CF0FD68C7C005AE3F6" BuildableName = "BinaryDelta" BlueprintName = "BinaryDelta" ReferencedContainer = "container:Sparkle.xcodeproj"> </BuildableReference> </BuildableProductRunnable> </LaunchAction> <ProfileAction buildConfiguration = "Release" shouldUseLaunchSchemeArgsEnv = "YES" savedToolIdentifier = "" useCustomWorkingDirectory = "NO" debugDocumentVersioning = "YES"> <BuildableProductRunnable runnableDebuggingMode = "0"> <BuildableReference BuildableIdentifier = "primary" BlueprintIdentifier = "5D06E8CF0FD68C7C005AE3F6" BuildableName = "BinaryDelta" BlueprintName = "BinaryDelta" ReferencedContainer = "container:Sparkle.xcodeproj"> </BuildableReference> </BuildableProductRunnable> </ProfileAction> <AnalyzeAction buildConfiguration = "Debug"> </AnalyzeAction> <ArchiveAction buildConfiguration = "Release" revealArchiveInOrganizer = "YES"> </ArchiveAction> </Scheme> ================================================ FILE: Sparkle.xcodeproj/xcshareddata/xcschemes/Distribution.xcscheme ================================================ <?xml version="1.0" encoding="UTF-8"?> <Scheme LastUpgradeVersion = "1630" version = "1.3"> <BuildAction parallelizeBuildables = "YES" buildImplicitDependencies = "YES"> <BuildActionEntries> <BuildActionEntry buildForTesting = "YES" buildForRunning = "YES" buildForProfiling = "YES" buildForArchiving = "YES" buildForAnalyzing = "YES"> <BuildableReference BuildableIdentifier = "primary" BlueprintIdentifier = "14732BC51960F69300593899" BuildableName = "Distribution" BlueprintName = "Distribution" ReferencedContainer = "container:Sparkle.xcodeproj"> </BuildableReference> </BuildActionEntry> </BuildActionEntries> </BuildAction> <TestAction buildConfiguration = "Debug" selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" shouldUseLaunchSchemeArgsEnv = "YES"> <Testables> <TestableReference skipped = "NO"> <BuildableReference BuildableIdentifier = "primary" BlueprintIdentifier = "612279D80DB5470200AB99EA" BuildableName = "Sparkle Unit Tests.xctest" BlueprintName = "Sparkle Unit Tests" ReferencedContainer = "container:Sparkle.xcodeproj"> </BuildableReference> </TestableReference> </Testables> </TestAction> <LaunchAction buildConfiguration = "Release" selectedDebuggerIdentifier = "" selectedLauncherIdentifier = "Xcode.IDEFoundation.Launcher.PosixSpawn" launchStyle = "0" useCustomWorkingDirectory = "NO" ignoresPersistentStateOnLaunch = "NO" debugDocumentVersioning = "YES" debugServiceExtension = "internal" allowLocationSimulation = "YES"> <MacroExpansion> <BuildableReference BuildableIdentifier = "primary" BlueprintIdentifier = "14732BC51960F69300593899" BuildableName = "Distribution" BlueprintName = "Distribution" ReferencedContainer = "container:Sparkle.xcodeproj"> </BuildableReference> </MacroExpansion> </LaunchAction> <ProfileAction buildConfiguration = "Release" shouldUseLaunchSchemeArgsEnv = "YES" savedToolIdentifier = "" useCustomWorkingDirectory = "NO" debugDocumentVersioning = "YES"> <MacroExpansion> <BuildableReference BuildableIdentifier = "primary" BlueprintIdentifier = "14732BC51960F69300593899" BuildableName = "Distribution" BlueprintName = "Distribution" ReferencedContainer = "container:Sparkle.xcodeproj"> </BuildableReference> </MacroExpansion> </ProfileAction> <AnalyzeAction buildConfiguration = "Debug"> </AnalyzeAction> <ArchiveAction buildConfiguration = "Release" revealArchiveInOrganizer = "YES"> </ArchiveAction> </Scheme> ================================================ FILE: Sparkle.xcodeproj/xcshareddata/xcschemes/Sparkle Test App.xcscheme ================================================ <?xml version="1.0" encoding="UTF-8"?> <Scheme LastUpgradeVersion = "1630" version = "1.3"> <BuildAction parallelizeBuildables = "YES" buildImplicitDependencies = "NO"> <BuildActionEntries> <BuildActionEntry buildForTesting = "YES" buildForRunning = "YES" buildForProfiling = "YES" buildForArchiving = "YES" buildForAnalyzing = "YES"> <BuildableReference BuildableIdentifier = "primary" BlueprintIdentifier = "61B5F90109C4CEE200B25A18" BuildableName = "Sparkle Test App.app" BlueprintName = "Sparkle Test App" ReferencedContainer = "container:Sparkle.xcodeproj"> </BuildableReference> </BuildActionEntry> </BuildActionEntries> </BuildAction> <TestAction buildConfiguration = "Debug" selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" shouldUseLaunchSchemeArgsEnv = "YES"> <Testables> </Testables> </TestAction> <LaunchAction buildConfiguration = "Debug" selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" launchStyle = "0" useCustomWorkingDirectory = "NO" ignoresPersistentStateOnLaunch = "NO" debugDocumentVersioning = "NO" debugServiceExtension = "internal" enableGPUFrameCaptureMode = "3" enableGPUValidationMode = "1" allowLocationSimulation = "NO"> <BuildableProductRunnable runnableDebuggingMode = "0"> <BuildableReference BuildableIdentifier = "primary" BlueprintIdentifier = "61B5F90109C4CEE200B25A18" BuildableName = "Sparkle Test App.app" BlueprintName = "Sparkle Test App" ReferencedContainer = "container:Sparkle.xcodeproj"> </BuildableReference> </BuildableProductRunnable> <EnvironmentVariables> <EnvironmentVariable key = "TEST_MODE" value = "DELTA_AND_MARKDOWN" isEnabled = "NO"> </EnvironmentVariable> </EnvironmentVariables> </LaunchAction> <ProfileAction buildConfiguration = "Release" shouldUseLaunchSchemeArgsEnv = "YES" savedToolIdentifier = "" useCustomWorkingDirectory = "NO" debugDocumentVersioning = "YES"> <BuildableProductRunnable runnableDebuggingMode = "0"> <BuildableReference BuildableIdentifier = "primary" BlueprintIdentifier = "61B5F90109C4CEE200B25A18" BuildableName = "Sparkle Test App.app" BlueprintName = "Sparkle Test App" ReferencedContainer = "container:Sparkle.xcodeproj"> </BuildableReference> </BuildableProductRunnable> </ProfileAction> <AnalyzeAction buildConfiguration = "Debug"> </AnalyzeAction> <ArchiveAction buildConfiguration = "Release" revealArchiveInOrganizer = "YES"> </ArchiveAction> </Scheme> ================================================ FILE: Sparkle.xcodeproj/xcshareddata/xcschemes/Sparkle.xcscheme ================================================ <?xml version="1.0" encoding="UTF-8"?> <Scheme LastUpgradeVersion = "1630" version = "2.0"> <BuildAction parallelizeBuildables = "YES" buildImplicitDependencies = "NO"> <BuildActionEntries> <BuildActionEntry buildForTesting = "YES" buildForRunning = "YES" buildForProfiling = "YES" buildForArchiving = "YES" buildForAnalyzing = "YES"> <BuildableReference BuildableIdentifier = "primary" BlueprintIdentifier = "8DC2EF4F0486A6940098B216" BuildableName = "Sparkle.framework" BlueprintName = "Sparkle" ReferencedContainer = "container:Sparkle.xcodeproj"> </BuildableReference> </BuildActionEntry> </BuildActionEntries> </BuildAction> <TestAction buildConfiguration = "Debug" selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" shouldUseLaunchSchemeArgsEnv = "YES"> <Testables> <TestableReference skipped = "NO"> <BuildableReference BuildableIdentifier = "primary" BlueprintIdentifier = "612279D80DB5470200AB99EA" BuildableName = "Sparkle Unit Tests.xctest" BlueprintName = "Sparkle Unit Tests" ReferencedContainer = "container:Sparkle.xcodeproj"> </BuildableReference> </TestableReference> </Testables> </TestAction> <LaunchAction buildConfiguration = "Debug" selectedDebuggerIdentifier = "" selectedLauncherIdentifier = "Xcode.IDEFoundation.Launcher.PosixSpawn" launchStyle = "0" useCustomWorkingDirectory = "NO" ignoresPersistentStateOnLaunch = "NO" debugDocumentVersioning = "YES" debugXPCServices = "NO" debugServiceExtension = "internal" allowLocationSimulation = "YES"> <MacroExpansion> <BuildableReference BuildableIdentifier = "primary" BlueprintIdentifier = "8DC2EF4F0486A6940098B216" BuildableName = "Sparkle.framework" BlueprintName = "Sparkle" ReferencedContainer = "container:Sparkle.xcodeproj"> </BuildableReference> </MacroExpansion> </LaunchAction> <ProfileAction buildConfiguration = "Release" shouldUseLaunchSchemeArgsEnv = "YES" savedToolIdentifier = "" useCustomWorkingDirectory = "NO" debugDocumentVersioning = "YES"> <MacroExpansion> <BuildableReference BuildableIdentifier = "primary" BlueprintIdentifier = "8DC2EF4F0486A6940098B216" BuildableName = "Sparkle.framework" BlueprintName = "Sparkle" ReferencedContainer = "container:Sparkle.xcodeproj"> </BuildableReference> </MacroExpansion> </ProfileAction> <AnalyzeAction buildConfiguration = "Debug"> </AnalyzeAction> <ArchiveAction buildConfiguration = "Release" revealArchiveInOrganizer = "YES"> </ArchiveAction> </Scheme> ================================================ FILE: Sparkle.xcodeproj/xcshareddata/xcschemes/UITests.xcscheme ================================================ <?xml version="1.0" encoding="UTF-8"?> <Scheme LastUpgradeVersion = "1630" version = "1.3"> <BuildAction parallelizeBuildables = "YES" buildImplicitDependencies = "YES"> </BuildAction> <TestAction buildConfiguration = "Debug" selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" shouldUseLaunchSchemeArgsEnv = "YES"> <Testables> <TestableReference skipped = "NO"> <BuildableReference BuildableIdentifier = "primary" BlueprintIdentifier = "726B2B5C1C645FC900388755" BuildableName = "UI Tests.xctest" BlueprintName = "UI Tests" ReferencedContainer = "container:Sparkle.xcodeproj"> </BuildableReference> </TestableReference> </Testables> </TestAction> <LaunchAction buildConfiguration = "Debug" selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" launchStyle = "0" useCustomWorkingDirectory = "NO" ignoresPersistentStateOnLaunch = "NO" debugDocumentVersioning = "YES" debugServiceExtension = "internal" allowLocationSimulation = "YES"> </LaunchAction> <ProfileAction buildConfiguration = "Release" shouldUseLaunchSchemeArgsEnv = "YES" savedToolIdentifier = "" useCustomWorkingDirectory = "NO" debugDocumentVersioning = "YES"> </ProfileAction> <AnalyzeAction buildConfiguration = "Debug"> </AnalyzeAction> <ArchiveAction buildConfiguration = "Release" revealArchiveInOrganizer = "YES"> </ArchiveAction> </Scheme> ================================================ FILE: Sparkle.xcodeproj/xcshareddata/xcschemes/generate_appcast.xcscheme ================================================ <?xml version="1.0" encoding="UTF-8"?> <Scheme LastUpgradeVersion = "1630" version = "1.3"> <BuildAction parallelizeBuildables = "YES" buildImplicitDependencies = "YES"> <BuildActionEntries> <BuildActionEntry buildForTesting = "YES" buildForRunning = "YES" buildForProfiling = "YES" buildForArchiving = "YES" buildForAnalyzing = "YES"> <BuildableReference BuildableIdentifier = "primary" BlueprintIdentifier = "7205C43D1E13049400E370AE" BuildableName = "generate_appcast" BlueprintName = "generate_appcast" ReferencedContainer = "container:Sparkle.xcodeproj"> </BuildableReference> </BuildActionEntry> </BuildActionEntries> </BuildAction> <TestAction buildConfiguration = "Debug" selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" shouldUseLaunchSchemeArgsEnv = "YES"> <Testables> </Testables> </TestAction> <LaunchAction buildConfiguration = "Debug" selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" launchStyle = "0" useCustomWorkingDirectory = "NO" ignoresPersistentStateOnLaunch = "NO" debugDocumentVersioning = "YES" debugServiceExtension = "internal" allowLocationSimulation = "YES"> <BuildableProductRunnable runnableDebuggingMode = "0"> <BuildableReference BuildableIdentifier = "primary" BlueprintIdentifier = "7205C43D1E13049400E370AE" BuildableName = "generate_appcast" BlueprintName = "generate_appcast" ReferencedContainer = "container:Sparkle.xcodeproj"> </BuildableReference> </BuildableProductRunnable> </LaunchAction> <ProfileAction buildConfiguration = "Release" shouldUseLaunchSchemeArgsEnv = "YES" savedToolIdentifier = "" useCustomWorkingDirectory = "NO" debugDocumentVersioning = "YES"> <BuildableProductRunnable runnableDebuggingMode = "0"> <BuildableReference BuildableIdentifier = "primary" BlueprintIdentifier = "7205C43D1E13049400E370AE" BuildableName = "generate_appcast" BlueprintName = "generate_appcast" ReferencedContainer = "container:Sparkle.xcodeproj"> </BuildableReference> </BuildableProductRunnable> </ProfileAction> <AnalyzeAction buildConfiguration = "Debug"> </AnalyzeAction> <ArchiveAction buildConfiguration = "Release" revealArchiveInOrganizer = "YES"> </ArchiveAction> </Scheme> ================================================ FILE: Sparkle.xcodeproj/xcshareddata/xcschemes/generate_keys.xcscheme ================================================ <?xml version="1.0" encoding="UTF-8"?> <Scheme LastUpgradeVersion = "1630" version = "1.3"> <BuildAction parallelizeBuildables = "YES" buildImplicitDependencies = "YES"> <BuildActionEntries> <BuildActionEntry buildForTesting = "YES" buildForRunning = "YES" buildForProfiling = "YES" buildForArchiving = "YES" buildForAnalyzing = "YES"> <BuildableReference BuildableIdentifier = "primary" BlueprintIdentifier = "EA1E285D22B66487004AA304" BuildableName = "generate_keys" BlueprintName = "generate_keys" ReferencedContainer = "container:Sparkle.xcodeproj"> </BuildableReference> </BuildActionEntry> </BuildActionEntries> </BuildAction> <TestAction buildConfiguration = "Debug" selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" shouldUseLaunchSchemeArgsEnv = "YES"> <Testables> </Testables> </TestAction> <LaunchAction buildConfiguration = "Debug" selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" launchStyle = "0" useCustomWorkingDirectory = "NO" ignoresPersistentStateOnLaunch = "NO" debugDocumentVersioning = "YES" debugServiceExtension = "internal" allowLocationSimulation = "YES"> <BuildableProductRunnable runnableDebuggingMode = "0"> <BuildableReference BuildableIdentifier = "primary" BlueprintIdentifier = "EA1E285D22B66487004AA304" BuildableName = "generate_keys" BlueprintName = "generate_keys" ReferencedContainer = "container:Sparkle.xcodeproj"> </BuildableReference> </BuildableProductRunnable> </LaunchAction> <ProfileAction buildConfiguration = "Release" shouldUseLaunchSchemeArgsEnv = "YES" savedToolIdentifier = "" useCustomWorkingDirectory = "NO" debugDocumentVersioning = "YES"> <BuildableProductRunnable runnableDebuggingMode = "0"> <BuildableReference BuildableIdentifier = "primary" BlueprintIdentifier = "EA1E285D22B66487004AA304" BuildableName = "generate_keys" BlueprintName = "generate_keys" ReferencedContainer = "container:Sparkle.xcodeproj"> </BuildableReference> </BuildableProductRunnable> </ProfileAction> <AnalyzeAction buildConfiguration = "Debug"> </AnalyzeAction> <ArchiveAction buildConfiguration = "Release" revealArchiveInOrganizer = "YES"> </ArchiveAction> </Scheme> ================================================ FILE: Sparkle.xcodeproj/xcshareddata/xcschemes/sign_update.xcscheme ================================================ <?xml version="1.0" encoding="UTF-8"?> <Scheme LastUpgradeVersion = "1630" version = "1.3"> <BuildAction parallelizeBuildables = "YES" buildImplicitDependencies = "YES"> <BuildActionEntries> <BuildActionEntry buildForTesting = "YES" buildForRunning = "YES" buildForProfiling = "YES" buildForArchiving = "YES" buildForAnalyzing = "YES"> <BuildableReference BuildableIdentifier = "primary" BlueprintIdentifier = "EA1E287522B666EB004AA304" BuildableName = "sign_update" BlueprintName = "sign_update" ReferencedContainer = "container:Sparkle.xcodeproj"> </BuildableReference> </BuildActionEntry> </BuildActionEntries> </BuildAction> <TestAction buildConfiguration = "Debug" selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" shouldUseLaunchSchemeArgsEnv = "YES"> <Testables> </Testables> </TestAction> <LaunchAction buildConfiguration = "Debug" selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" launchStyle = "0" useCustomWorkingDirectory = "NO" ignoresPersistentStateOnLaunch = "NO" debugDocumentVersioning = "YES" debugServiceExtension = "internal" allowLocationSimulation = "YES"> <BuildableProductRunnable runnableDebuggingMode = "0"> <BuildableReference BuildableIdentifier = "primary" BlueprintIdentifier = "EA1E287522B666EB004AA304" BuildableName = "sign_update" BlueprintName = "sign_update" ReferencedContainer = "container:Sparkle.xcodeproj"> </BuildableReference> </BuildableProductRunnable> </LaunchAction> <ProfileAction buildConfiguration = "Release" shouldUseLaunchSchemeArgsEnv = "YES" savedToolIdentifier = "" useCustomWorkingDirectory = "NO" debugDocumentVersioning = "YES"> <BuildableProductRunnable runnableDebuggingMode = "0"> <BuildableReference BuildableIdentifier = "primary" BlueprintIdentifier = "EA1E287522B666EB004AA304" BuildableName = "sign_update" BlueprintName = "sign_update" ReferencedContainer = "container:Sparkle.xcodeproj"> </BuildableReference> </BuildableProductRunnable> </ProfileAction> <AnalyzeAction buildConfiguration = "Debug"> </AnalyzeAction> <ArchiveAction buildConfiguration = "Release" revealArchiveInOrganizer = "YES"> </ArchiveAction> </Scheme> ================================================ FILE: Sparkle.xcodeproj/xcshareddata/xcschemes/sparkle-cli.xcscheme ================================================ <?xml version="1.0" encoding="UTF-8"?> <Scheme LastUpgradeVersion = "1630" version = "1.3"> <BuildAction parallelizeBuildables = "YES" buildImplicitDependencies = "NO"> <BuildActionEntries> <BuildActionEntry buildForTesting = "YES" buildForRunning = "YES" buildForProfiling = "YES" buildForArchiving = "YES" buildForAnalyzing = "YES"> <BuildableReference BuildableIdentifier = "primary" BlueprintIdentifier = "72D9549D1CBB415B006F28BD" BuildableName = "sparkle.app" BlueprintName = "sparkle-cli" ReferencedContainer = "container:Sparkle.xcodeproj"> </BuildableReference> </BuildActionEntry> </BuildActionEntries> </BuildAction> <TestAction buildConfiguration = "Debug" selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" shouldUseLaunchSchemeArgsEnv = "YES"> <Testables> </Testables> </TestAction> <LaunchAction buildConfiguration = "Debug" selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" launchStyle = "0" useCustomWorkingDirectory = "NO" ignoresPersistentStateOnLaunch = "NO" debugDocumentVersioning = "NO" debugServiceExtension = "internal" allowLocationSimulation = "YES"> <BuildableProductRunnable runnableDebuggingMode = "0"> <BuildableReference BuildableIdentifier = "primary" BlueprintIdentifier = "72D9549D1CBB415B006F28BD" BuildableName = "sparkle.app" BlueprintName = "sparkle-cli" ReferencedContainer = "container:Sparkle.xcodeproj"> </BuildableReference> </BuildableProductRunnable> </LaunchAction> <ProfileAction buildConfiguration = "Release" shouldUseLaunchSchemeArgsEnv = "YES" savedToolIdentifier = "" useCustomWorkingDirectory = "NO" debugDocumentVersioning = "YES"> <BuildableProductRunnable runnableDebuggingMode = "0"> <BuildableReference BuildableIdentifier = "primary" BlueprintIdentifier = "72D9549D1CBB415B006F28BD" BuildableName = "sparkle.app" BlueprintName = "sparkle-cli" ReferencedContainer = "container:Sparkle.xcodeproj"> </BuildableReference> </BuildableProductRunnable> </ProfileAction> <AnalyzeAction buildConfiguration = "Debug"> </AnalyzeAction> <ArchiveAction buildConfiguration = "Release" revealArchiveInOrganizer = "YES"> </ArchiveAction> </Scheme> ================================================ FILE: TestAppHelper/Info.plist ================================================ <?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <plist version="1.0"> <dict> <key>CFBundleDevelopmentRegion</key> <string>en</string> <key>CFBundleDisplayName</key> <string>TestAppHelper</string> <key>CFBundleExecutable</key> <string>$(EXECUTABLE_NAME)</string> <key>CFBundleIdentifier</key> <string>$(PRODUCT_BUNDLE_IDENTIFIER)</string> <key>CFBundleInfoDictionaryVersion</key> <string>6.0</string> <key>CFBundleName</key> <string>$(PRODUCT_NAME)</string> <key>CFBundlePackageType</key> <string>XPC!</string> <key>CFBundleShortVersionString</key> <string>1.0</string> <key>CFBundleSignature</key> <string>????</string> <key>CFBundleVersion</key> <string>1</string> <key>NSHumanReadableCopyright</key> <string>Copyright © 2016 Sparkle Project. All rights reserved.</string> <key>XPCService</key> <dict> <key>ServiceType</key> <string>Application</string> <key>JoinExistingSession</key> <true/> </dict> <key>NSAppTransportSecurity</key> <dict> <key>NSAllowsArbitraryLoads</key> <true/> </dict> </dict> </plist> ================================================ FILE: TestAppHelper/TestAppHelper.h ================================================ // // TestAppHelper.h // TestAppHelper // // Created by Mayur Pawashe on 3/2/16. // Copyright © 2016 Sparkle Project. All rights reserved. // #import <Foundation/Foundation.h> #import "TestAppHelperProtocol.h" @protocol SPUUserDriver; // This object implements the protocol which we have defined. It provides the actual behavior for the service. It is 'exported' by the service to make it available to the process hosting the service over an NSXPCConnection. @interface TestAppHelper : NSObject <TestAppHelperProtocol> @end ================================================ FILE: TestAppHelper/TestAppHelper.m ================================================ // // TestAppHelper.m // TestAppHelper // // Created by Mayur Pawashe on 3/2/16. // Copyright © 2016 Sparkle Project. All rights reserved. // #import "TestAppHelper.h" #import "SUAdHocCodeSigning.h" @implementation TestAppHelper - (void)codeSignApplicationAtPath:(NSString *)applicationPath reply:(void (^)(BOOL))reply { reply([SUAdHocCodeSigning codeSignApplicationAtPath:applicationPath]); } @end ================================================ FILE: TestAppHelper/TestAppHelperProtocol.h ================================================ // // TestAppHelperProtocol.h // TestAppHelper // // Created by Mayur Pawashe on 3/2/16. // Copyright © 2016 Sparkle Project. All rights reserved. // #import <Foundation/Foundation.h> // The protocol that this service will vend as its API. This header file will also need to be visible to the process hosting the service. @protocol TestAppHelperProtocol - (void)codeSignApplicationAtPath:(NSString *)applicationPath reply:(void (^)(BOOL))reply; @end ================================================ FILE: TestAppHelper/main.m ================================================ // // main.m // TestAppHelper // // Created by Mayur Pawashe on 3/2/16. // Copyright © 2016 Sparkle Project. All rights reserved. // #import <Foundation/Foundation.h> #import "TestAppHelper.h" #import <Sparkle/SPUUserDriver.h> @interface ServiceDelegate : NSObject <NSXPCListenerDelegate> @end @implementation ServiceDelegate - (BOOL)listener:(NSXPCListener *)__unused listener shouldAcceptNewConnection:(NSXPCConnection *)newConnection { // This method is where the NSXPCListener configures, accepts, and resumes a new incoming NSXPCConnection. // Configure the connection. // First, set the interface that the exported object implements. newConnection.exportedInterface = [NSXPCInterface interfaceWithProtocol:@protocol(TestAppHelperProtocol)]; // Next, set the object that the connection exports. All messages sent on the connection to this service will be sent to the exported object to handle. The connection retains the exported object. newConnection.exportedObject = [[TestAppHelper alloc] init]; // Resuming the connection allows the system to deliver more incoming messages. [newConnection resume]; // Returning YES from this method tells the system that you have accepted this connection. If you want to reject the connection for some reason, call -invalidate on the connection and return NO. return YES; } @end int main(__unused int argc, __unused const char *argv[]) { // Create the delegate for the service. ServiceDelegate *delegate = [ServiceDelegate new]; // Set up the one NSXPCListener for this service. It will handle all incoming connections. NSXPCListener *listener = [NSXPCListener serviceListener]; listener.delegate = delegate; // Resuming the serviceListener starts this service. This method does not return. [listener resume]; return 0; } ================================================ FILE: TestApplication/AppIcon.icon/icon.json ================================================ { "fill" : { "automatic-gradient" : "srgb:1.00000,1.00000,1.00000,1.00000" }, "groups" : [ { "layers" : [ { "fill-specializations" : [ { "value" : { "solid" : "extended-srgb:0.00000,0.47843,1.00000,1.00000" } }, { "appearance" : "dark", "value" : { "solid" : "extended-srgb:0.00000,0.47843,1.00000,1.00000" } } ], "glass" : true, "hidden" : false, "image-name" : "sparkles.png", "name" : "sparkles", "position" : { "scale" : 1.15, "translation-in-points" : [ 0, 0 ] } }, { "glass" : true, "hidden" : false, "image-name" : "circular_arrow.png", "name" : "circular_arrow", "position" : { "scale" : 1.25, "translation-in-points" : [ 0, 0 ] } } ], "shadow" : { "kind" : "neutral", "opacity" : 0.5 }, "translucency" : { "enabled" : true, "value" : 0.5 } } ], "supported-platforms" : { "circles" : [ "watchOS" ], "squares" : "shared" } } ================================================ FILE: TestApplication/Base.lproj/InfoPlist.strings ================================================ /* Localized versions of Info.plist keys */ NSHumanReadableCopyright = "© Andy Matuschak, 2006"; ================================================ FILE: TestApplication/Base.lproj/MainMenu.xib ================================================ <?xml version="1.0" encoding="UTF-8"?> <document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="24506" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES"> <dependencies> <deployment identifier="macosx"/> <plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="24506"/> </dependencies> <objects> <customObject id="-2" userLabel="File's Owner" customClass="NSApplication"> <connections> <outlet property="delegate" destination="iTs-bN-9SB" id="w0I-XO-wWG"/> </connections> </customObject> <customObject id="-1" userLabel="First Responder" customClass="FirstResponder"/> <customObject id="-3" userLabel="Application" customClass="NSObject"/> <menu title="MainMenu" systemMenu="main" id="29" userLabel="MainMenu"> <items> <menuItem title="Sparkle Test App" id="56"> <menu key="submenu" title="Sparkle Test App" systemMenu="apple" id="57"> <items> <menuItem title="About Sparkle Test App" id="58"> <modifierMask key="keyEquivalentModifierMask"/> <connections> <action selector="orderFrontStandardAboutPanel:" target="-2" id="142"/> </connections> </menuItem> <menuItem title="Check for Updates…" keyEquivalent="u" id="207"> <connections> <action selector="checkForUpdates:" target="-1" id="yqv-bK-pBA"/> </connections> </menuItem> <menuItem isSeparatorItem="YES" id="196"> <modifierMask key="keyEquivalentModifierMask" command="YES"/> </menuItem> <menuItem title="Services" id="131"> <menu key="submenu" title="Services" systemMenu="services" id="130"/> </menuItem> <menuItem isSeparatorItem="YES" id="144"> <modifierMask key="keyEquivalentModifierMask" command="YES"/> </menuItem> <menuItem title="Hide Sparkle Test App" keyEquivalent="h" id="134"> <connections> <action selector="hide:" target="-2" id="152"/> </connections> </menuItem> <menuItem title="Hide Others" keyEquivalent="h" id="145"> <modifierMask key="keyEquivalentModifierMask" option="YES" command="YES"/> <connections> <action selector="hideOtherApplications:" target="-2" id="146"/> </connections> </menuItem> <menuItem title="Show All" id="150"> <connections> <action selector="unhideAllApplications:" target="-2" id="153"/> </connections> </menuItem> <menuItem isSeparatorItem="YES" id="149"> <modifierMask key="keyEquivalentModifierMask" command="YES"/> </menuItem> <menuItem title="Quit Sparkle Test App" keyEquivalent="q" id="136"> <connections> <action selector="terminate:" target="-2" id="139"/> </connections> </menuItem> </items> </menu> </menuItem> <menuItem title="File" id="83"> <menu key="submenu" title="File" id="81"> <items> <menuItem title="New" keyEquivalent="n" id="82"/> <menuItem title="Open..." keyEquivalent="o" id="72"/> <menuItem title="Open Recent" id="124"> <menu key="submenu" title="Open Recent" systemMenu="recentDocuments" id="125"> <items> <menuItem title="Clear Menu" id="126"> <connections> <action selector="clearRecentDocuments:" target="-1" id="127"/> </connections> </menuItem> </items> </menu> </menuItem> <menuItem isSeparatorItem="YES" id="79"> <modifierMask key="keyEquivalentModifierMask" command="YES"/> </menuItem> <menuItem title="Close" keyEquivalent="w" id="73"> <connections> <action selector="performClose:" target="-1" id="193"/> </connections> </menuItem> <menuItem title="Save" keyEquivalent="s" id="75"/> <menuItem title="Save As…" keyEquivalent="S" id="80"/> <menuItem title="Revert" id="112"> <modifierMask key="keyEquivalentModifierMask"/> </menuItem> <menuItem isSeparatorItem="YES" id="74"> <modifierMask key="keyEquivalentModifierMask" command="YES"/> </menuItem> <menuItem title="Page Setup…" keyEquivalent="p" id="77"> <modifierMask key="keyEquivalentModifierMask" control="YES" option="YES" command="YES"/> <connections> <action selector="runPageLayout:" target="-1" id="87"/> </connections> </menuItem> <menuItem title="Print…" keyEquivalent="p" id="78"> <modifierMask key="keyEquivalentModifierMask" shift="YES" control="YES" option="YES" command="YES"/> <connections> <action selector="print:" target="-1" id="86"/> </connections> </menuItem> </items> </menu> </menuItem> <menuItem title="Edit" id="163"> <menu key="submenu" title="Edit" id="169"> <items> <menuItem title="Undo" keyEquivalent="z" id="158"> <connections> <action selector="undo:" target="-1" id="180"/> </connections> </menuItem> <menuItem title="Redo" keyEquivalent="Z" id="173"> <connections> <action selector="redo:" target="-1" id="178"/> </connections> </menuItem> <menuItem isSeparatorItem="YES" id="156"> <modifierMask key="keyEquivalentModifierMask" command="YES"/> </menuItem> <menuItem title="Cut" keyEquivalent="x" id="160"> <connections> <action selector="cut:" target="-1" id="175"/> </connections> </menuItem> <menuItem title="Copy" keyEquivalent="c" id="157"> <connections> <action selector="copy:" target="-1" id="181"/> </connections> </menuItem> <menuItem title="Paste" keyEquivalent="v" id="171"> <connections> <action selector="paste:" target="-1" id="176"/> </connections> </menuItem> <menuItem title="Paste and Match Style" keyEquivalent="V" id="204"> <modifierMask key="keyEquivalentModifierMask" option="YES" command="YES"/> <connections> <action selector="pasteAsPlainText:" target="-1" id="205"/> </connections> </menuItem> <menuItem title="Delete" id="164"> <connections> <action selector="delete:" target="-1" id="195"/> </connections> </menuItem> <menuItem title="Select All" keyEquivalent="a" id="172"> <connections> <action selector="selectAll:" target="-1" id="179"/> </connections> </menuItem> <menuItem isSeparatorItem="YES" id="174"> <modifierMask key="keyEquivalentModifierMask" command="YES"/> </menuItem> <menuItem title="Find" id="168"> <menu key="submenu" title="Find" id="159"> <items> <menuItem title="Find…" tag="1" keyEquivalent="f" id="154"> <connections> <action selector="performFindPanelAction:" target="-1" id="199"/> </connections> </menuItem> <menuItem title="Find Next" tag="2" keyEquivalent="g" id="167"> <connections> <action selector="performFindPanelAction:" target="-1" id="200"/> </connections> </menuItem> <menuItem title="Find Previous" tag="3" keyEquivalent="G" id="162"> <connections> <action selector="performFindPanelAction:" target="-1" id="201"/> </connections> </menuItem> <menuItem title="Use Selection for Find" tag="7" keyEquivalent="e" id="161"> <connections> <action selector="performFindPanelAction:" target="-1" id="202"/> </connections> </menuItem> <menuItem title="Jump to Selection" keyEquivalent="j" id="155"> <connections> <action selector="centerSelectionInVisibleArea:" target="-1" id="203"/> </connections> </menuItem> </items> </menu> </menuItem> <menuItem title="Spelling" id="184"> <menu key="submenu" title="Spelling" id="185"> <items> <menuItem title="Spelling…" keyEquivalent=":" id="187"> <connections> <action selector="showGuessPanel:" target="-1" id="188"/> </connections> </menuItem> <menuItem title="Check Spelling" keyEquivalent=";" id="189"> <connections> <action selector="checkSpelling:" target="-1" id="190"/> </connections> </menuItem> <menuItem title="Check Spelling as You Type" id="191"> <connections> <action selector="toggleContinuousSpellChecking:" target="-1" id="192"/> </connections> </menuItem> </items> </menu> </menuItem> </items> </menu> </menuItem> <menuItem title="Window" id="19"> <menu key="submenu" title="Window" systemMenu="window" id="24"> <items> <menuItem title="Minimize" keyEquivalent="m" id="23"> <connections> <action selector="performMiniaturize:" target="-1" id="37"/> </connections> </menuItem> <menuItem title="Zoom" id="197"> <connections> <action selector="performZoom:" target="-1" id="198"/> </connections> </menuItem> <menuItem isSeparatorItem="YES" id="92"> <modifierMask key="keyEquivalentModifierMask" command="YES"/> </menuItem> <menuItem title="Bring All to Front" keyEquivalent="p" id="5"> <connections> <action selector="arrangeInFront:" target="-1" id="39"/> </connections> </menuItem> </items> </menu> </menuItem> <menuItem title="Help" id="103"> <menu key="submenu" title="Help" id="106"> <items> <menuItem title="Sparkle Test App Help" keyEquivalent="?" id="111"> <connections> <action selector="showHelp:" target="-1" id="122"/> </connections> </menuItem> </items> </menu> </menuItem> </items> <point key="canvasLocation" x="140" y="154"/> </menu> <customObject id="iTs-bN-9SB" customClass="SUTestApplicationDelegate"/> </objects> </document> ================================================ FILE: TestApplication/SUAdHocCodeSigning.h ================================================ // // SUAdHocCodeSigning.h // Sparkle // // Created by Mayur Pawashe on 3/4/16. // Copyright © 2016 Sparkle Project. All rights reserved. // #import <Foundation/Foundation.h> SPU_OBJC_DIRECT_MEMBERS @interface SUAdHocCodeSigning : NSObject + (BOOL)codeSignApplicationAtPath:(NSString *)applicationPath; @end ================================================ FILE: TestApplication/SUAdHocCodeSigning.m ================================================ // // SUAdHocCodeSigning.m // Sparkle // // Created by Mayur Pawashe on 3/4/16. // Copyright © 2016 Sparkle Project. All rights reserved. // #import "SUAdHocCodeSigning.h" @implementation SUAdHocCodeSigning + (BOOL)codeSignApplicationAtPath:(NSString *)applicationPath { BOOL success = NO; @try { // ad-hoc signing with the dash NSArray *arguments = @[ @"--force", @"--deep", @"--sign", @"-", applicationPath ]; NSTask *task = [NSTask launchedTaskWithLaunchPath:@"/usr/bin/codesign" arguments:arguments]; [task waitUntilExit]; success = (task.terminationStatus == 0); } @catch (NSException *exception) { NSLog(@"Failed to code sign application at %@", applicationPath); NSLog(@"Exception: %@", exception); } return success; } @end ================================================ FILE: TestApplication/SUInstallUpdateViewController.h ================================================ // // SUInstallUpdateViewController.h // Sparkle // // Created by Mayur Pawashe on 3/5/16. // Copyright © 2016 Sparkle Project. All rights reserved. // #import <Cocoa/Cocoa.h> #import <Sparkle/Sparkle.h> SPU_OBJC_DIRECT_MEMBERS @interface SUInstallUpdateViewController : NSViewController - (instancetype)initWithAppcastItem:(SUAppcastItem *)appcastItem reply:(void (^)(SPUUserUpdateChoice))reply; - (void)showReleaseNotesWithDownloadData:(SPUDownloadData *)downloadData; @end ================================================ FILE: TestApplication/SUInstallUpdateViewController.m ================================================ // // SUInstallUpdateViewController.m // Sparkle // // Created by Mayur Pawashe on 3/5/16. // Copyright © 2016 Sparkle Project. All rights reserved. // #import "SUInstallUpdateViewController.h" @implementation SUInstallUpdateViewController { void (^_reply)(SPUUserUpdateChoice); SUAppcastItem *_appcastItem; NSAttributedString *_preloadedReleaseNotes; IBOutlet NSTextView *_textView; IBOutlet NSButton *_skipUpdatesButton; } - (instancetype)initWithAppcastItem:(SUAppcastItem *)appcastItem reply:(void (^)(SPUUserUpdateChoice))reply { self = [super initWithNibName:@"SUInstallUpdateViewController" bundle:nil]; if (self != nil) { _appcastItem = appcastItem; _reply = [reply copy]; } else { assert(false); } return self; } - (void)viewDidLoad { [super viewDidLoad]; [[_textView enclosingScrollView] setDrawsBackground:NO]; [_textView setDrawsBackground:NO]; if (_preloadedReleaseNotes != nil) { [self displayReleaseNotes:_preloadedReleaseNotes]; _preloadedReleaseNotes = nil; } else if (_appcastItem.releaseNotesURL == nil) { NSString *descriptionString = _appcastItem.itemDescription; if (descriptionString != nil) { NSString *descriptionFormat = _appcastItem.itemDescriptionFormat; if ([descriptionFormat isEqualToString:@"plain-text"] || [descriptionFormat isEqualToString:@"markdown"]) { NSAttributedString *attributedString = [[NSAttributedString alloc] initWithString:descriptionString]; [self displayReleaseNotes:attributedString]; } else { NSData *htmlData = [descriptionString dataUsingEncoding:NSUTF8StringEncoding]; NSAttributedString *attributedString = [[NSAttributedString alloc] initWithHTML:htmlData documentAttributes:NULL]; [self displayReleaseNotes:attributedString]; } } } } - (void)displayReleaseNotes:(NSAttributedString *)releaseNotes SPU_OBJC_DIRECT { if (_textView == nil) { _preloadedReleaseNotes = releaseNotes; } else { [_textView.textStorage setAttributedString:releaseNotes]; } } - (void)displayHTMLReleaseNotes:(NSData *)releaseNotes SPU_OBJC_DIRECT { NSAttributedString *attributedString = [[NSAttributedString alloc] initWithHTML:releaseNotes documentAttributes:NULL]; [self displayReleaseNotes:attributedString]; } - (void)displayPlainTextReleaseNotes:(NSData *)releaseNotes encoding:(NSStringEncoding)encoding SPU_OBJC_DIRECT { NSString *string = [[NSString alloc] initWithData:releaseNotes encoding:encoding]; NSAttributedString *attributedString = [[NSAttributedString alloc] initWithString:string attributes:nil]; [self displayReleaseNotes:attributedString]; } - (void)showReleaseNotesWithDownloadData:(SPUDownloadData *)downloadData { // Partially copied from SPUCommandLineUserDriver // Not all user drivers need this kind of implementation (eg: see SPUStandardUserDriver) // Also I'm not extremely confident about the correctness of this code so I don't want to export it publicly if (downloadData.MIMEType != nil && [downloadData.MIMEType isEqualToString:@"text/plain"]) { NSStringEncoding encoding; if (downloadData.textEncodingName == nil) { encoding = NSUTF8StringEncoding; } else { CFStringEncoding cfEncoding = CFStringConvertIANACharSetNameToEncoding((CFStringRef)downloadData.textEncodingName); if (cfEncoding != kCFStringEncodingInvalidId) { encoding = CFStringConvertEncodingToNSStringEncoding(cfEncoding); } else { encoding = NSUTF8StringEncoding; } } [self displayPlainTextReleaseNotes:downloadData.data encoding:encoding]; } else { [self displayHTMLReleaseNotes:downloadData.data]; } } - (IBAction)installUpdate:(id)__unused sender { if (_reply != nil) { _reply(SPUUserUpdateChoiceInstall); _reply = nil; } } - (IBAction)installUpdateLater:(id)__unused sender { if (_reply != nil) { _reply(SPUUserUpdateChoiceDismiss); _reply = nil; } } - (IBAction)skipUpdate:(id)__unused sender { if (_reply != nil) { _reply(SPUUserUpdateChoiceSkip); _reply = nil; } } @end ================================================ FILE: TestApplication/SUInstallUpdateViewController.xib ================================================ <?xml version="1.0" encoding="UTF-8"?> <document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="24506" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES"> <dependencies> <deployment identifier="macosx"/> <plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="24506"/> <capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/> </dependencies> <objects> <customObject id="-2" userLabel="File's Owner" customClass="SUInstallUpdateViewController"> <connections> <outlet property="_skipUpdatesButton" destination="GnO-99-FrA" id="WO4-cT-KGm"/> <outlet property="_textView" destination="hvq-rZ-gUG" id="x7h-XU-cqB"/> <outlet property="view" destination="Hz6-mo-xeY" id="0bl-1N-x8E"/> </connections> </customObject> <customObject id="-1" userLabel="First Responder" customClass="FirstResponder"/> <customObject id="-3" userLabel="Application" customClass="NSObject"/> <customView id="Hz6-mo-xeY"> <rect key="frame" x="0.0" y="0.0" width="406" height="182"/> <autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/> <subviews> <button verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="JaG-xU-cSv"> <rect key="frame" x="146" y="6" width="114" height="32"/> <autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/> <buttonCell key="cell" type="push" title="Install Later" bezelStyle="rounded" alignment="center" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="vGh-C9-cmI"> <behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/> <font key="font" metaFont="system"/> <string key="keyEquivalent" base64-UTF8="YES"> Gw </string> </buttonCell> <connections> <action selector="installUpdateLater:" target="-2" id="49e-4G-Ofu"/> </connections> </button> <button verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="GnO-99-FrA"> <rect key="frame" x="20" y="6" width="114" height="32"/> <autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/> <buttonCell key="cell" type="push" title="Skip Update" bezelStyle="rounded" alignment="center" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="Q9S-re-mYe"> <behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/> <font key="font" metaFont="system"/> </buttonCell> <connections> <action selector="skipUpdate:" target="-2" id="VsN-ie-9xR"/> </connections> </button> <scrollView fixedFrame="YES" horizontalLineScroll="10" horizontalPageScroll="10" verticalLineScroll="10" verticalPageScroll="10" hasHorizontalScroller="NO" usesPredominantAxisScrolling="NO" translatesAutoresizingMaskIntoConstraints="NO" id="3Cj-JW-f43"> <rect key="frame" x="0.0" y="46" width="406" height="136"/> <autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/> <clipView key="contentView" drawsBackground="NO" id="CIT-u4-hRg"> <rect key="frame" x="1" y="1" width="404" height="134"/> <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/> <subviews> <textView editable="NO" importsGraphics="NO" verticallyResizable="YES" findStyle="panel" allowsNonContiguousLayout="YES" id="hvq-rZ-gUG"> <rect key="frame" x="0.0" y="0.0" width="404" height="134"/> <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/> <color key="textColor" name="textColor" catalog="System" colorSpace="catalog"/> <color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/> <size key="minSize" width="404" height="134"/> <size key="maxSize" width="463" height="10000000"/> <color key="insertionPointColor" name="controlTextColor" catalog="System" colorSpace="catalog"/> </textView> </subviews> </clipView> <scroller key="horizontalScroller" hidden="YES" wantsLayer="YES" verticalHuggingPriority="750" doubleValue="1" horizontal="YES" id="t2X-nw-Cy5"> <rect key="frame" x="-100" y="-100" width="87" height="18"/> <autoresizingMask key="autoresizingMask"/> </scroller> <scroller key="verticalScroller" wantsLayer="YES" verticalHuggingPriority="750" horizontal="NO" id="dth-jh-Csr"> <rect key="frame" x="388" y="1" width="17" height="134"/> <autoresizingMask key="autoresizingMask"/> </scroller> </scrollView> <button verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="cYY-dW-0o8"> <rect key="frame" x="272" y="6" width="114" height="32"/> <autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/> <buttonCell key="cell" type="push" title="Install Update" bezelStyle="rounded" alignment="center" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="AoI-0t-LLU"> <behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/> <font key="font" metaFont="system"/> <string key="keyEquivalent" base64-UTF8="YES"> DQ </string> </buttonCell> <connections> <action selector="installUpdate:" target="-2" id="G8I-20-OHo"/> </connections> </button> </subviews> <point key="canvasLocation" x="192" y="251"/> </customView> </objects> </document> ================================================ FILE: TestApplication/SUPopUpTitlebarUserDriver.h ================================================ // // SUPopUpTitlebarUserDriver.h // Sparkle // // Created by Mayur Pawashe on 3/5/16. // Copyright © 2016 Sparkle Project. All rights reserved. // #import <Foundation/Foundation.h> #import <Sparkle/Sparkle.h> @class NSWindow; SPU_OBJC_DIRECT_MEMBERS @interface SUPopUpTitlebarUserDriver : NSObject <SPUUserDriver> - (instancetype)initWithWindow:(NSWindow *)window; @end ================================================ FILE: TestApplication/SUPopUpTitlebarUserDriver.m ================================================ // // SUPopUpTitlebarUserDriver.m // Sparkle // // Created by Mayur Pawashe on 3/5/16. // Copyright © 2016 Sparkle Project. All rights reserved. // #import "SUPopUpTitlebarUserDriver.h" #import "SUInstallUpdateViewController.h" #import <AppKit/AppKit.h> @implementation SUPopUpTitlebarUserDriver { void (^_updateButtonAction)(NSButton *); NSWindow *_window; SUInstallUpdateViewController *_installUpdateViewController; NSTitlebarAccessoryViewController *_accessoryViewController; NSButton *_updateButton; uint64_t _expectedContentLength; uint64_t _contentLengthDownloaded; BOOL _addedAccessory; } - (instancetype)initWithWindow:(NSWindow *)window { self = [super init]; if (self != nil) { _window = window; } return self; } - (void)addUpdateButtonWithTitle:(NSString *)title SPU_OBJC_DIRECT { [self addUpdateButtonWithTitle:title action:nil]; } - (void)addUpdateButtonWithTitle:(NSString *)title action:(void (^)(NSButton *button))action SPU_OBJC_DIRECT { if (_updateButton == nil) { NSButton *updateButton = [[NSButton alloc] initWithFrame:NSMakeRect(0, 0, 160, 100)]; updateButton.title = title; updateButton.bezelStyle = NSBezelStyleRecessed; _updateButton = updateButton; } else { _updateButton.title = title; } if (action != nil) { _updateButton.target = self; _updateButton.action = @selector(updateButtonAction:); _updateButtonAction = action; _updateButton.enabled = YES; } else { _updateButton.enabled = NO; _updateButton.target = nil; _updateButtonAction = nil; } if (_accessoryViewController == nil) { _accessoryViewController = [[NSTitlebarAccessoryViewController alloc] init]; _accessoryViewController.layoutAttribute = NSLayoutAttributeRight; _accessoryViewController.view = _updateButton; } if (!_addedAccessory) { [_window addTitlebarAccessoryViewController:_accessoryViewController]; _addedAccessory = YES; } } - (void)updateButtonAction:(NSButton *)sender { if (_updateButtonAction != nil) { _updateButtonAction(sender); } } - (void)removeUpdateButton SPU_OBJC_DIRECT { [_accessoryViewController removeFromParentViewController]; _addedAccessory = NO; _updateButtonAction = nil; } #pragma mark Update Permission - (void)showUpdatePermissionRequest:(SPUUpdatePermissionRequest *)__unused request reply:(void (^)(SUUpdatePermissionResponse *))reply { // Just make a decision.. SUUpdatePermissionResponse *response = [[SUUpdatePermissionResponse alloc] initWithAutomaticUpdateChecks:YES sendSystemProfile:NO]; reply(response); } #pragma mark Update Found - (void)showUpdateWithAppcastItem:(SUAppcastItem *)appcastItem reply:(void (^)(SPUUserUpdateChoice))reply { NSPopover *popover = [[NSPopover alloc] init]; popover.behavior = NSPopoverBehaviorTransient; __weak __typeof__(self) weakSelf = self; __block NSButton *actionButton = nil; SUInstallUpdateViewController *viewController = [[SUInstallUpdateViewController alloc] initWithAppcastItem:appcastItem reply:^(SPUUserUpdateChoice choice) { reply(choice); [popover close]; actionButton.enabled = NO; __typeof__(self) strongSelf = weakSelf; if (strongSelf != nil) { strongSelf->_installUpdateViewController = nil; } }]; _installUpdateViewController = viewController; [self addUpdateButtonWithTitle:@"Update Available" action:^(NSButton *button) { actionButton = button; popover.contentViewController = viewController; [popover showRelativeToRect:button.bounds ofView:button preferredEdge:NSMaxYEdge]; }]; } - (void)showUpdateFoundWithAppcastItem:(SUAppcastItem *)appcastItem state:(SPUUserUpdateState *)state reply:(void (^)(SPUUserUpdateChoice))reply { if (appcastItem.informationOnlyUpdate) { // Todo: show user interface for this NSLog(@"Found info URL: %@", appcastItem.infoURL); // Remove UI from user initiated check [self removeUpdateButton]; reply(SPUUserUpdateChoiceDismiss); } else { [self showUpdateWithAppcastItem:appcastItem reply:reply]; } } - (void)showUpdateReleaseNotesWithDownloadData:(SPUDownloadData *)downloadData { [_installUpdateViewController showReleaseNotesWithDownloadData:downloadData]; } - (void)showUpdateReleaseNotesFailedToDownloadWithError:(NSError *)__unused error { } - (void)showUpdateInFocus { [_window makeKeyAndOrderFront:nil]; if (_updateButton.enabled) { // Not the proper way to do things but ignoring warnings in Test App. #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wobjc-messaging-id" #pragma clang diagnostic ignored "-Warc-performSelector-leaks" [_updateButton.target performSelector:_updateButton.action withObject:_updateButton]; #pragma clang diagnostic pop } } #pragma mark Install & Relaunch Update - (void)showReadyToInstallAndRelaunch:(void (^)(SPUUserUpdateChoice))installUpdateHandler { [self addUpdateButtonWithTitle:@"Install & Relaunch" action:^(NSButton *__unused button) { installUpdateHandler(SPUUserUpdateChoiceInstall); }]; } #pragma mark Check for Updates - (void)showUserInitiatedUpdateCheckWithCancellation:(void (^)(void))__unused cancellation { [self addUpdateButtonWithTitle:@"Checking for Updates…"]; } #pragma mark Update Errors - (void)acceptAcknowledgementAfterDelay:(void (^)(void))acknowledgement { dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ // Installation will be dismissed shortly after this acknowledgement(); }); } - (void)showUpdaterError:(NSError *)error acknowledgement:(void (^)(void))acknowledgement { NSLog(@"Error: %@", error); [self addUpdateButtonWithTitle:@"Update Errored!" action:nil]; [self acceptAcknowledgementAfterDelay:acknowledgement]; } - (void)showUpdateNotFoundWithError:(NSError *)error acknowledgement:(void (^)(void))acknowledgement { [self addUpdateButtonWithTitle:@"No Update Available" action:nil]; [self acceptAcknowledgementAfterDelay:acknowledgement]; } #pragma mark Download & Install Updates - (void)showDownloadInitiatedWithCancellation:(void (^)(void))__unused cancellation { } - (void)showDownloadDidReceiveExpectedContentLength:(uint64_t)expectedContentLength { [self addUpdateButtonWithTitle:@"Downloading…"]; _contentLengthDownloaded = 0; _expectedContentLength = expectedContentLength; } - (void)showDownloadDidReceiveDataOfLength:(uint64_t)length { _contentLengthDownloaded += length; // In case our expected content length was incorrect if (_contentLengthDownloaded > _expectedContentLength) { _expectedContentLength = _contentLengthDownloaded; } if (_expectedContentLength > 0) { double progress = (double)_contentLengthDownloaded / (double)_expectedContentLength; [self addUpdateButtonWithTitle:[NSString stringWithFormat:@"Downloading (%0.0f%%)", progress * 100] action:nil]; } } - (void)showDownloadDidStartExtractingUpdate { [self addUpdateButtonWithTitle:@"Extracting…"]; } - (void)showExtractionReceivedProgress:(double)progress { [self addUpdateButtonWithTitle:[NSString stringWithFormat:@"Extracting (%d%%)…", (int)(progress * 100)]]; } - (void)showInstallingUpdateWithApplicationTerminated:(BOOL)applicationTerminated retryTerminatingApplication:(void (^)(void))__unused retryTerminatingApplication { if (applicationTerminated) { [self addUpdateButtonWithTitle:@"Installing…"]; } else { // In case our termination request fails or is delayed [self removeUpdateButton]; } } - (void)showUpdateInstalledAndRelaunched:(BOOL)__unused relaunched acknowledgement:(void (^)(void))acknowledgement { [self addUpdateButtonWithTitle:@"Installation Finished!"]; [self acceptAcknowledgementAfterDelay:acknowledgement]; } #pragma mark Aborting Everything - (void)dismissUpdateInstallation { [self removeUpdateButton]; } @end ================================================ FILE: TestApplication/SUTestApplicationDelegate.h ================================================ // // SUTestApplicationDelegate.h // Sparkle // // Created by Mayur Pawashe on 7/25/15. // Copyright (c) 2015 Sparkle Project. All rights reserved. // #import <Cocoa/Cocoa.h> @interface SUTestApplicationDelegate : NSObject <NSApplicationDelegate> @end ================================================ FILE: TestApplication/SUTestApplicationDelegate.m ================================================ // // SUTestApplicationDelegate.m // Sparkle // // Created by Mayur Pawashe on 7/25/15. // Copyright (c) 2015 Sparkle Project. All rights reserved. // #import "SUTestApplicationDelegate.h" #import "SUUpdateSettingsWindowController.h" #import "SUTestWebServer.h" #import "TestAppHelperProtocol.h" #import "ed25519.h" #import <Sparkle/Sparkle.h> #import "SUPopUpTitlebarUserDriver.h" #import "SUBinaryDeltaCreate.h" @interface SUTestApplicationDelegate () <NSMenuItemValidation, SPUUpdaterDelegate> @end @implementation SUTestApplicationDelegate { SPUUpdater *_updater; SUUpdateSettingsWindowController *_updateSettingsWindowController; SUTestWebServer *_webServer; NSString *_testMode; } - (void)applicationDidFinishLaunching:(NSNotification * __unused)notification { NSBundle *mainBundle = [NSBundle mainBundle]; NSString *testModeEnv = [[[NSProcessInfo processInfo] environment] objectForKey:@"TEST_MODE"]; NSString *testMode; if (testModeEnv == nil) { testMode = @"REGULAR"; } else { testMode = testModeEnv; } _testMode = testMode; // Check if we are already up to date NSString *mainBundleVersion = (NSString *)[mainBundle objectForInfoDictionaryKey:(__bridge NSString *)kCFBundleVersionKey]; if (([mainBundleVersion hasPrefix:@"2."] && [testMode isEqualToString:@"REGULAR"]) || (([mainBundleVersion isEqualToString:@"2.1"] || [mainBundleVersion isEqualToString:@"2.2"]) && [testMode isEqualToString:@"DELTA_AND_MARKDOWN"]) || ([mainBundleVersion isEqualToString:@"2.2"] && [testMode isEqualToString:@"AUTOMATIC"])) { NSAlert *alreadyUpdatedAlert = [[NSAlert alloc] init]; alreadyUpdatedAlert.messageText = @"Update succeeded!"; alreadyUpdatedAlert.informativeText = @"This is the updated version of Sparkle Test App.\n\nDelete and rebuild the app to test updates again."; [alreadyUpdatedAlert runModal]; [[NSApplication sharedApplication] terminate:nil]; } #if SPARKLE_BUILD_UI_BITS // Detect as early as possible if the shift key is held down BOOL shiftKeyHeldDown = ([NSEvent modifierFlags] & NSEventModifierFlagShift) != 0; #endif NSFileManager *fileManager = [NSFileManager defaultManager]; // Locate user's cache directory NSError *cacheError = nil; NSURL *cacheDirectoryURL = [[NSFileManager defaultManager] URLForDirectory:NSCachesDirectory inDomain:NSUserDomainMask appropriateForURL:nil create:YES error:&cacheError]; if (cacheDirectoryURL == nil) { NSLog(@"Failed to locate cache directory with error: %@", cacheError); abort(); } NSString *bundleIdentifier = mainBundle.bundleIdentifier; assert(bundleIdentifier != nil); // Create a directory that'll be used for our web server listing NSURL *serverDirectoryURL = [[cacheDirectoryURL URLByAppendingPathComponent:bundleIdentifier] URLByAppendingPathComponent:@"ServerData"]; if ([serverDirectoryURL checkResourceIsReachableAndReturnError:nil]) { NSError *removeServerDirectoryError = nil; if (![fileManager removeItemAtURL:serverDirectoryURL error:&removeServerDirectoryError]) { abort(); } } NSError *createDirectoryError = nil; if (![[NSFileManager defaultManager] createDirectoryAtURL:serverDirectoryURL withIntermediateDirectories:YES attributes:nil error:&createDirectoryError]) { NSLog(@"Failed creating directory at %@ with error %@", serverDirectoryURL.path, createDirectoryError); abort(); } NSURL *bundleURL = mainBundle.bundleURL; assert(bundleURL != nil); // Copy main bundle into server directory NSString *bundleURLLastComponent = bundleURL.lastPathComponent; assert(bundleURLLastComponent != nil); NSURL *destinationBundleURL = [serverDirectoryURL URLByAppendingPathComponent:bundleURLLastComponent]; NSError *copyBundleError = nil; if (![fileManager copyItemAtURL:bundleURL toURL:destinationBundleURL error:©BundleError]) { NSLog(@"Failed to copy main bundle into server directory with error %@", copyBundleError); abort(); } // Update bundle's version keys to latest version NSURL *infoURL = [[destinationBundleURL URLByAppendingPathComponent:@"Contents"] URLByAppendingPathComponent:@"Info.plist"]; BOOL infoFileExists = [infoURL checkResourceIsReachableAndReturnError:nil]; assert(infoFileExists); NSString *finalUpdatedVersion; if ([testMode isEqualToString:@"REGULAR"]) { finalUpdatedVersion = @"2.0"; } else if ([testMode isEqualToString:@"DELTA_AND_MARKDOWN"]) { finalUpdatedVersion = @"2.1"; } else if ([testMode isEqualToString:@"AUTOMATIC"]) { finalUpdatedVersion = @"2.2"; } else { assert(false); } NSMutableDictionary *infoDictionary = [[NSMutableDictionary alloc] initWithContentsOfURL:infoURL]; [infoDictionary setObject:finalUpdatedVersion forKey:(__bridge NSString *)kCFBundleVersionKey]; [infoDictionary setObject:finalUpdatedVersion forKey:@"CFBundleShortVersionString"]; BOOL wroteInfoFile = [infoDictionary writeToURL:infoURL atomically:NO]; assert(wroteInfoFile); // Overwrite and add new data { NSURL *screenshotURL = [[[[destinationBundleURL URLByAppendingPathComponent:@"Contents"] URLByAppendingPathComponent:@"Resources"] URLByAppendingPathComponent:@"screenshot"] URLByAppendingPathExtension:@"png"]; assert([screenshotURL checkResourceIsReachableAndReturnError:NULL]); NSMutableData *screenshotData = [NSMutableData dataWithContentsOfURL:screenshotURL]; uint32_t garbage = 1337; [screenshotData appendBytes:&garbage length:sizeof(garbage)]; BOOL wroteData = [screenshotData writeToURL:screenshotURL atomically:NO]; assert(wroteData); NSURL *newDataURL = [[screenshotURL URLByDeletingLastPathComponent] URLByAppendingPathComponent:@"new_file"]; assert(newDataURL != nil); BOOL wroteNewData = [[NSData dataWithBytes:&garbage length:sizeof(garbage)] writeToURL:newDataURL atomically:NO]; assert(wroteNewData); } [self signApplicationIfRequiredAtPath:destinationBundleURL.path completion:^{ dispatch_async(dispatch_get_main_queue(), ^{ // Change current working directory so web server knows where to list files NSString *serverDirectoryPath = serverDirectoryURL.path; assert(serverDirectoryPath != nil); NSString * const appcastName = @"sparkletestcast"; NSString * const appcastExtension = @"xml"; // Copy our appcast over to the server directory NSURL *appcastDestinationURL = [[serverDirectoryURL URLByAppendingPathComponent:appcastName] URLByAppendingPathExtension:appcastExtension]; NSError *copyAppcastError = nil; NSURL *appcastURL = [mainBundle URLForResource:appcastName withExtension:appcastExtension]; assert(appcastURL != nil); if (![fileManager copyItemAtURL:appcastURL toURL:appcastDestinationURL error:©AppcastError]) { NSLog(@"Failed to copy appcast into cache directory with error %@", copyAppcastError); abort(); } // Update the appcast with the file size and signature of the update archive // We could be using some sort of XML parser instead of doing string substitutions, but for now, this is easier NSError *appcastError = nil; NSMutableString *appcastContents = [[NSMutableString alloc] initWithContentsOfURL:appcastDestinationURL encoding:NSUTF8StringEncoding error:&appcastError]; if (appcastContents == nil) { NSLog(@"Failed to load appcast contents with error %@", appcastError); abort(); } // Don't ever do this at home, kids (seriously) // (that is, including the private key inside of your application) const unsigned char self_sign_demo_only_insecure_hack[64] = {200, 238, 135, 84, 10, 189, 3, 193, 61, 208, 203, 30, 133, 47, 12, 22, 19, 52, 252, 99, 110, 205, 209, 94, 215, 144, 201, 70, 27, 162, 163, 108, 0, 164, 68, 184, 226, 93, 121, 199, 172, 17, 26, 64, 89, 68, 232, 41, 2, 26, 245, 175, 158, 165, 42, 55, 5, 97, 8, 243, 251, 164, 93, 9}; // in normal app this goes to Info.plist const unsigned char public_key[32] = {121, 17, 79, 45, 155, 141, 51, 169, 188, 110, 91, 102, 182, 147, 215, 225, 252, 202, 110, 231, 200, 215, 62, 171, 40, 145, 237, 128, 130, 44, 150, 89}; unsigned char signature[64]; if ([testMode isEqualToString:@"DELTA_AND_MARKDOWN"]) { NSError *deltaCreationError = nil; NSURL *patchURL = [serverDirectoryURL URLByAppendingPathComponent:@"patch.delta"]; if (!createBinaryDelta(bundleURL.path, destinationBundleURL.path, patchURL.path, SUBinaryDeltaMajorVersionDefault, SPUDeltaCompressionModeDefault, 0, NO, &deltaCreationError)) { NSLog(@"Failed to create binary delta patch: %@", deltaCreationError); abort(); } NSData *archive = [NSData dataWithContentsOfURL:patchURL]; assert(archive != nil); ed25519_sign(signature, (const unsigned char *)archive.bytes, archive.length, public_key, self_sign_demo_only_insecure_hack); NSString *signatureString = [[NSData dataWithBytes:signature length:64] base64EncodedStringWithOptions:(NSDataBase64EncodingOptions)0]; // Obtain the file attributes to get the file size of our update later NSError *fileAttributesError = nil; NSString *archiveURLPath = patchURL.path; assert(archiveURLPath != nil); NSDictionary *archiveFileAttributes = [[NSFileManager defaultManager] attributesOfItemAtPath:archiveURLPath error:&fileAttributesError]; if (archiveFileAttributes == nil) { NSLog(@"Failed to retrieve file attributes from delta archive with error %@", fileAttributesError); abort(); } NSUInteger numberOfLengthReplacements = [appcastContents replaceOccurrencesOfString:@"$INSERT_DELTA_ARCHIVE_LENGTH" withString:[NSString stringWithFormat:@"%llu", archiveFileAttributes.fileSize] options:NSLiteralSearch range:NSMakeRange(0, appcastContents.length)]; assert(numberOfLengthReplacements == 1); NSUInteger numberOfSignatureReplacements = [appcastContents replaceOccurrencesOfString:@"$INSERT_DELTA_EDDSA_SIGNATURE" withString:signatureString options:NSLiteralSearch range:NSMakeRange(0, appcastContents.length)]; assert(numberOfSignatureReplacements == 1); NSUInteger numberOfFromVersionReplacements = [appcastContents replaceOccurrencesOfString:@"$INSERT_DELTA_FROM_VERSION" withString:mainBundleVersion options:NSLiteralSearch range:NSMakeRange(0, appcastContents.length)]; assert(numberOfFromVersionReplacements == 1); NSRange feedSignaturesPrefixRange = [appcastContents rangeOfString:@"<!-- sparkle-signatures:\n" options:(NSStringCompareOptions)(NSLiteralSearch | NSBackwardsSearch)]; assert(feedSignaturesPrefixRange.location != NSNotFound); ed25519_sign(signature, (const unsigned char *)appcastContents.UTF8String, feedSignaturesPrefixRange.location, public_key, self_sign_demo_only_insecure_hack); NSString *feedSignatureString = [[NSData dataWithBytes:signature length:64] base64EncodedStringWithOptions:(NSDataBase64EncodingOptions)0]; NSUInteger numberOfFeedSignatureReplacements = [appcastContents replaceOccurrencesOfString:@"$INSERT_EDDSA_FEED_SIGNATURE" withString:feedSignatureString options:NSLiteralSearch range:NSMakeRange(0, appcastContents.length)]; assert(numberOfFeedSignatureReplacements == 1); NSUInteger numberOfFeedLengthReplacements = [appcastContents replaceOccurrencesOfString:@"$INSERT_FEED_LENGTH" withString:@(feedSignaturesPrefixRange.location).stringValue options:NSLiteralSearch range:NSMakeRange(0, appcastContents.length)]; assert(numberOfFeedLengthReplacements == 1); NSError *writeAppcastError = nil; if (![appcastContents writeToURL:appcastDestinationURL atomically:NO encoding:NSUTF8StringEncoding error:&writeAppcastError]) { NSLog(@"Failed to write updated appcast with error %@", writeAppcastError); abort(); } } else { // Create the archive for our update NSString *zipName = @"Sparkle_Test_App.zip"; NSTask *dittoTask = [[NSTask alloc] init]; dittoTask.launchPath = @"/usr/bin/ditto"; dittoTask.arguments = @[@"-c", @"-k", @"--sequesterRsrc", @"--keepParent", (NSString *)destinationBundleURL.lastPathComponent, zipName]; dittoTask.currentDirectoryPath = serverDirectoryPath; [dittoTask launch]; [dittoTask waitUntilExit]; assert(dittoTask.terminationStatus == 0); NSURL *archiveURL = [serverDirectoryURL URLByAppendingPathComponent:zipName]; NSData *archive = [NSData dataWithContentsOfURL:archiveURL]; assert(archive != nil); ed25519_sign(signature, (const unsigned char *)archive.bytes, archive.length, public_key, self_sign_demo_only_insecure_hack); NSString *signatureString = [[NSData dataWithBytes:signature length:64] base64EncodedStringWithOptions:(NSDataBase64EncodingOptions)0]; // Obtain the file attributes to get the file size of our update later NSError *fileAttributesError = nil; NSString *archiveURLPath = archiveURL.path; assert(archiveURLPath != nil); NSDictionary *archiveFileAttributes = [[NSFileManager defaultManager] attributesOfItemAtPath:archiveURLPath error:&fileAttributesError]; if (archiveFileAttributes == nil) { NSLog(@"Failed to retrieve file attributes from archive with error %@", fileAttributesError); abort(); } NSUInteger numberOfLengthReplacements = [appcastContents replaceOccurrencesOfString:@"$INSERT_ARCHIVE_LENGTH" withString:[NSString stringWithFormat:@"%llu", archiveFileAttributes.fileSize] options:NSLiteralSearch range:NSMakeRange(0, appcastContents.length)]; assert(numberOfLengthReplacements == 2); NSUInteger numberOfSignatureReplacements = [appcastContents replaceOccurrencesOfString:@"$INSERT_EDDSA_SIGNATURE" withString:signatureString options:NSLiteralSearch range:NSMakeRange(0, appcastContents.length)]; assert(numberOfSignatureReplacements == 2); NSRange feedSignaturesPrefixRange = [appcastContents rangeOfString:@"<!-- sparkle-signatures:\n" options:(NSStringCompareOptions)(NSLiteralSearch | NSBackwardsSearch)]; assert(feedSignaturesPrefixRange.location != NSNotFound); ed25519_sign(signature, (const unsigned char *)appcastContents.UTF8String, feedSignaturesPrefixRange.location, public_key, self_sign_demo_only_insecure_hack); NSString *feedSignatureString = [[NSData dataWithBytes:signature length:64] base64EncodedStringWithOptions:(NSDataBase64EncodingOptions)0]; NSUInteger numberOfFeedSignatureReplacements = [appcastContents replaceOccurrencesOfString:@"$INSERT_EDDSA_FEED_SIGNATURE" withString:feedSignatureString options:NSLiteralSearch range:NSMakeRange(0, appcastContents.length)]; assert(numberOfFeedSignatureReplacements == 1); NSUInteger numberOfFeedLengthReplacements = [appcastContents replaceOccurrencesOfString:@"$INSERT_FEED_LENGTH" withString:@(feedSignaturesPrefixRange.location).stringValue options:NSLiteralSearch range:NSMakeRange(0, appcastContents.length)]; assert(numberOfFeedLengthReplacements == 1); NSError *writeAppcastError = nil; if (![appcastContents writeToURL:appcastDestinationURL atomically:NO encoding:NSUTF8StringEncoding error:&writeAppcastError]) { NSLog(@"Failed to write updated appcast with error %@", writeAppcastError); abort(); } } [fileManager removeItemAtURL:destinationBundleURL error:NULL]; // Finally start the server SUTestWebServer *webServer = [[SUTestWebServer alloc] initWithPort:1337 workingDirectory:serverDirectoryPath]; if (!webServer) { NSLog(@"Failed to create the web server"); abort(); } self->_webServer = webServer; // Set up updater and the updater settings window { self->_updateSettingsWindowController = [[SUUpdateSettingsWindowController alloc] init]; NSWindow *settingsWindow = self->_updateSettingsWindowController.window; NSBundle *hostBundle = [NSBundle mainBundle]; NSBundle *applicationBundle = hostBundle; id<SPUUserDriver> userDriver; #if SPARKLE_BUILD_UI_BITS if (shiftKeyHeldDown) { userDriver = [[SUPopUpTitlebarUserDriver alloc] initWithWindow:settingsWindow]; } else { userDriver = [[SPUStandardUserDriver alloc] initWithHostBundle:hostBundle delegate:nil]; } #else userDriver = [[SUPopUpTitlebarUserDriver alloc] initWithWindow:settingsWindow]; #endif SPUUpdater *updater = [[SPUUpdater alloc] initWithHostBundle:hostBundle applicationBundle:applicationBundle userDriver:userDriver delegate:self]; self->_updater = updater; self->_updateSettingsWindowController.updater = updater; NSError *updaterError = nil; if (![updater startUpdater:&updaterError]) { NSLog(@"Failed to start updater with error: %@", updaterError); NSAlert *alert = [[NSAlert alloc] init]; alert.messageText = @"Updater Error"; alert.informativeText = @"The Updater failed to start. For detailed error information, check the Console.app log."; [alert addButtonWithTitle:@"OK"]; [alert runModal]; } [self->_updateSettingsWindowController showWindow:nil]; } }); }]; } - (NSSet<NSString *> *)allowedChannelsForUpdater:(SPUUpdater *)updater { if ([_testMode isEqualToString:@"DELTA_AND_MARKDOWN"]) { return [NSSet setWithObject:@"delta"]; } else if ([_testMode isEqualToString:@"AUTOMATIC"]) { return [NSSet setWithObject:@"automatic"]; } else { return [NSSet set]; } } - (void)signApplicationIfRequiredAtPath:(NSString *)applicationPath completion:(void (^)(void))completionBlock SPU_OBJC_DIRECT { // This is unfortunately necessary for testing sandboxing NSXPCConnection *codeSignConnection = [[NSXPCConnection alloc] initWithServiceName:@"org.sparkle-project.TestAppHelper"]; codeSignConnection.remoteObjectInterface = [NSXPCInterface interfaceWithProtocol:@protocol(TestAppHelperProtocol)]; [codeSignConnection resume]; [(id<TestAppHelperProtocol>)codeSignConnection.remoteObjectProxy codeSignApplicationAtPath:applicationPath reply:^(BOOL success) { assert(success); [codeSignConnection invalidate]; completionBlock(); }]; } - (void)applicationWillTerminate:(NSNotification * __unused)notification { [_webServer close]; } - (IBAction)checkForUpdates:(id __unused)sender { [_updater checkForUpdates]; } - (BOOL)validateMenuItem:(NSMenuItem *)menuItem { if (menuItem.action == @selector(checkForUpdates:)) { return _updater.canCheckForUpdates; } return YES; } @end ================================================ FILE: TestApplication/SUTestWebServer.h ================================================ // // SUTestWebServer.h // Sparkle // // Created by Kevin Wojniak on 10/8/15. // Copyright © 2015 Sparkle Project. All rights reserved. // #import <Foundation/Foundation.h> SPU_OBJC_DIRECT_MEMBERS @interface SUTestWebServer : NSObject - (instancetype)initWithPort:(int)port workingDirectory:(NSString*)workingDirectory; - (void)close; @end ================================================ FILE: TestApplication/SUTestWebServer.m ================================================ // // SUTestWebServer.m // Sparkle // // Created by Kevin Wojniak on 10/8/15. // Copyright © 2015 Sparkle Project. All rights reserved. // #import "SUTestWebServer.h" #import <sys/socket.h> #import <netinet/in.h> @class SUTestWebServerConnection; @protocol SUTestWebServerConnectionDelegate <NSObject> @required - (void)connectionDidClose:(SUTestWebServerConnection*)sender; @end @interface SUTestWebServerConnection : NSObject <NSStreamDelegate> @end @implementation SUTestWebServerConnection { NSString* _workingDirectory; NSInputStream *_inputStream; NSOutputStream *_outputStream; NSData *_dataToWrite; NSInteger _numBytesToWrite; __weak id<SUTestWebServerConnectionDelegate> _delegate; } - (instancetype)initWithNativeHandle:(CFSocketNativeHandle)handle workingDirectory:(NSString*)workingDirectory delegate:(id<SUTestWebServerConnectionDelegate>)delegate SPU_OBJC_DIRECT { self = [super init]; assert(self != nil); _workingDirectory = workingDirectory; _delegate = delegate; CFReadStreamRef readStream = NULL; CFWriteStreamRef writeStream = NULL; CFStreamCreatePairWithSocket(NULL, handle, &readStream, &writeStream); assert(readStream != NULL); assert(writeStream != NULL); _inputStream = (__bridge NSInputStream*)readStream; assert(_inputStream != nil); _inputStream.delegate = self; _outputStream = (__bridge NSOutputStream*)writeStream; assert(_outputStream != nil); _outputStream.delegate = self; [_inputStream scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode]; [_outputStream scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode]; [_inputStream open]; [_outputStream open]; return self; } - (void)close SPU_OBJC_DIRECT { NSInputStream *inputStream = _inputStream; NSOutputStream *outputStream = _outputStream; if (inputStream == nil) { assert(outputStream == nil); return; } [inputStream close]; [outputStream close]; [inputStream removeFromRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode]; [outputStream removeFromRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode]; _inputStream = nil; _outputStream = nil; [_delegate connectionDidClose:self]; } - (void)stream:(NSStream *)aStream handleEvent:(NSStreamEvent)eventCode { if (eventCode == NSStreamEventEndEncountered) { [self close]; return; } if (aStream == _inputStream && eventCode == NSStreamEventHasBytesAvailable) { uint8_t buffer[8096]; const NSInteger numBytes = [_inputStream read:buffer maxLength:sizeof(buffer)]; if (numBytes > 0) { NSString *request = [[NSString alloc] initWithBytes:buffer length:(NSUInteger)numBytes encoding:NSUTF8StringEncoding]; NSArray *lines = [request componentsSeparatedByString:@"\r\n"]; NSString *requestLine = lines.count >= 3 ? [lines objectAtIndex:0] : nil; NSArray *parts = requestLine ? [requestLine componentsSeparatedByString:@" "] : nil; // Only process GET requests for existing files if ([(NSString *)[parts objectAtIndex:0] isEqualToString:@"GET"]) { // Use NSURL to strip out query parameters NSString *path = [NSURL URLWithString:[parts objectAtIndex:1] relativeToURL:nil].path; NSString *filePath = [_workingDirectory stringByAppendingString:path]; BOOL isDir = NO; if (![[NSFileManager defaultManager] fileExistsAtPath:filePath isDirectory:&isDir] || isDir) { NSLog(@"%@ - 404", requestLine); [self write404]; } else { NSLog(@"%@ - 200", requestLine); [self write:[NSData dataWithContentsOfFile:filePath] status:YES]; } } else { NSLog(@"%@ - 404", requestLine); [self write404]; } } } else if (aStream == _outputStream && eventCode == NSStreamEventHasSpaceAvailable && _dataToWrite != nil) { [self checkIfCanWriteNow]; } } - (void)write404 SPU_OBJC_DIRECT { NSString *body = @"<html><head><title>404 Not Found

Not Found

"; [self write:[body dataUsingEncoding:NSUTF8StringEncoding] status:NO]; } - (void)write:(NSData*)body status:(BOOL)status SPU_OBJC_DIRECT { NSString *state = status ? @"200 OK" : @"404 Not Found"; NSString *header = [NSString stringWithFormat:@"HTTP/1.0 %@\r\nContent-Length: %lu\r\n\r\n", state, body.length]; NSMutableData *response = [[header dataUsingEncoding:NSUTF8StringEncoding] mutableCopy]; [response appendData:body]; [self queueWrite:response]; } - (void)queueWrite:(NSData*)data SPU_OBJC_DIRECT { assert(_dataToWrite == nil); assert(data != nil); assert(data.length > 0); _dataToWrite = data; _numBytesToWrite = (NSInteger)data.length; [self checkIfCanWriteNow]; } - (void)checkIfCanWriteNow SPU_OBJC_DIRECT { assert(_dataToWrite != nil); if (_numBytesToWrite == 0) { // nothing more to write, we're done. _dataToWrite = nil; _numBytesToWrite = -1; } else if (_outputStream.hasSpaceAvailable) { [self writeNow]; } // otherwise wait for space available event } - (void)writeNow SPU_OBJC_DIRECT { assert(_outputStream != nil); assert(_outputStream.hasSpaceAvailable); assert(_dataToWrite != nil); NSData *dataToWrite = _dataToWrite; const uint8_t *bytesOffset = (const uint8_t*)dataToWrite.bytes + ((NSInteger)dataToWrite.length - _numBytesToWrite); const NSInteger bytesWritten = [_outputStream write:bytesOffset maxLength:(NSUInteger)_numBytesToWrite]; if (bytesWritten > 0) { _numBytesToWrite = _numBytesToWrite - bytesWritten; assert(_numBytesToWrite >= 0); // wait for next space available event to write more } else { NSLog(@"Error: bytes written = %ld (%@)", bytesWritten, [NSString stringWithUTF8String:strerror(errno)]); } } @end SPU_OBJC_DIRECT_MEMBERS @interface SUTestWebServer () { CFSocketRef _socket; } @property (nonatomic) NSMutableArray *connections; @property (nonatomic) NSString *workingDirectory; - (void)accept:(CFSocketNativeHandle)address; @end static void connectCallback(CFSocketRef __unused s, CFSocketCallBackType type, CFDataRef __unused address, const void *data, void *info) { if (type == kCFSocketAcceptCallBack) { assert(data != NULL); assert(info != NULL); SUTestWebServer *server = (__bridge SUTestWebServer*)info; assert(server != nil); [server accept:*(const CFSocketNativeHandle*)data]; } } @implementation SUTestWebServer @synthesize connections = _connections; @synthesize workingDirectory = _workingDirectory; - (instancetype)initWithPort:(int)port workingDirectory:(NSString*)workingDirectory { self = [super init]; assert(self != nil); CFSocketContext ctx; memset(&ctx, 0, sizeof(ctx)); ctx.info = (__bridge void*)self; _socket = CFSocketCreate(NULL, 0, 0, 0, kCFSocketAcceptCallBack, connectCallback, &ctx); assert(_socket != NULL); struct sockaddr_in address; memset(&address, 0, sizeof(address)); address.sin_len = sizeof(address); address.sin_family = AF_INET; address.sin_port = htons(port); address.sin_addr.s_addr = INADDR_ANY; // will fail if port is in use. CFSocketError socketErr = CFSocketSetAddress(_socket, (CFDataRef)[NSData dataWithBytes:&address length:sizeof(address)]); if (socketErr != kCFSocketSuccess) { NSLog(@"Socket error: %@", [NSString stringWithUTF8String:strerror(errno)]); return nil; } _connections = [[NSMutableArray alloc] init]; _workingDirectory = workingDirectory; CFRunLoopSourceRef source = CFSocketCreateRunLoopSource(NULL, _socket, 0); assert(source != NULL); CFRunLoopAddSource(CFRunLoopGetCurrent(), source, kCFRunLoopDefaultMode); CFRelease(source); return self; } - (void)connectionDidClose:(SUTestWebServerConnection *)sender { assert(_connections != nil); assert([_connections containsObject:sender]); [_connections removeObject:sender]; } - (void)accept:(CFSocketNativeHandle)address { SUTestWebServerConnection *conn = [[SUTestWebServerConnection alloc] initWithNativeHandle:address workingDirectory:_workingDirectory delegate:self]; assert(conn != nil); if (conn) { assert(_connections != nil); [_connections addObject:conn]; } } - (void)close { for (SUTestWebServerConnection *conn in _connections) { [conn close]; } if (_socket) { CFSocketInvalidate(_socket); CFRelease(_socket); _socket = NULL; } } @end ================================================ FILE: TestApplication/SUUpdateSettingsWindowController.h ================================================ // // SUUpdateSettingsWindowController.h // Sparkle // // Created by Mayur Pawashe on 7/25/15. // Copyright (c) 2015 Sparkle Project. All rights reserved. // #import @class SPUUpdater; @interface SUUpdateSettingsWindowController : NSWindowController @property (nonatomic) SPUUpdater *updater; @end ================================================ FILE: TestApplication/SUUpdateSettingsWindowController.m ================================================ // // SUUpdateSettingsWindowController.m // Sparkle // // Created by Mayur Pawashe on 7/25/15. // Copyright (c) 2015 Sparkle Project. All rights reserved. // #import "SUUpdateSettingsWindowController.h" #import // This class binds to various updater properties in the nib @implementation SUUpdateSettingsWindowController @synthesize updater = _updater; - (NSString *)windowNibName { return NSStringFromClass([self class]); } @end ================================================ FILE: TestApplication/SUUpdateSettingsWindowController.xib ================================================ ================================================ FILE: TestApplication/Sparkle-Test-App.entitlements ================================================ com.apple.security.app-sandbox com.apple.security.network.server com.apple.security.network.client com.apple.security.temporary-exception.mach-lookup.global-name $(PRODUCT_BUNDLE_IDENTIFIER)-spks $(PRODUCT_BUNDLE_IDENTIFIER)-spki com.apple.security.temporary-exception.shared-preference.read-write $(PRODUCT_BUNDLE_IDENTIFIER) ================================================ FILE: TestApplication/TestApplication-Info.plist ================================================ CFBundleDevelopmentRegion English CFBundleExecutable ${EXECUTABLE_NAME} CFBundleIdentifier $(PRODUCT_BUNDLE_IDENTIFIER) CFBundleInfoDictionaryVersion 6.0 CFBundleName Sparkle Test App CFBundlePackageType APPL CFBundleSignature ???? CFBundleVersion 1.5.1 CFBundleShortVersionString 1.5.1 NSMainNibFile MainMenu NSPrincipalClass NSApplication SUEnableSystemProfiling LSMinimumSystemVersion $(MACOSX_DEPLOYMENT_TARGET) SUFeedURL http://localhost:1337/sparkletestcast.xml SUPublicEDKey eRFPLZuNM6m8bltmtpPX4fzKbufI1z6rKJHtgIIsllk= SUEnableInstallerLauncherService SURequireSignedFeed SUVerifyUpdateBeforeExtraction _SUEnableDebugUpdateCheckIntervals ================================================ FILE: TestApplication/ar.lproj/MainMenu.strings ================================================ /* Class = "NSMenuItem"; title = "Bring All to Front"; ObjectID = "5"; */ "5.title" = "Bring All to Front"; /* Class = "NSMenuItem"; title = "Window"; ObjectID = "19"; */ "19.title" = "Window"; /* Class = "NSMenuItem"; title = "Minimize"; ObjectID = "23"; */ "23.title" = "Minimize"; /* Class = "NSMenu"; title = "Window"; ObjectID = "24"; */ "24.title" = "Window"; /* Class = "NSMenu"; title = "MainMenu"; ObjectID = "29"; */ "29.title" = "MainMenu"; /* Class = "NSMenuItem"; title = "Sparkle Test App"; ObjectID = "56"; */ "56.title" = "Sparkle Test App"; /* Class = "NSMenu"; title = "Sparkle Test App"; ObjectID = "57"; */ "57.title" = "Sparkle Test App"; /* Class = "NSMenuItem"; title = "About Sparkle Test App"; ObjectID = "58"; */ "58.title" = "About Sparkle Test App"; /* Class = "NSMenuItem"; title = "Open..."; ObjectID = "72"; */ "72.title" = "Open..."; /* Class = "NSMenuItem"; title = "Close"; ObjectID = "73"; */ "73.title" = "Close"; /* Class = "NSMenuItem"; title = "Save"; ObjectID = "75"; */ "75.title" = "Save"; /* Class = "NSMenuItem"; title = "Page Setup…"; ObjectID = "77"; */ "77.title" = "Page Setup…"; /* Class = "NSMenuItem"; title = "Print…"; ObjectID = "78"; */ "78.title" = "Print…"; /* Class = "NSMenuItem"; title = "Save As…"; ObjectID = "80"; */ "80.title" = "Save As…"; /* Class = "NSMenu"; title = "File"; ObjectID = "81"; */ "81.title" = "File"; /* Class = "NSMenuItem"; title = "New"; ObjectID = "82"; */ "82.title" = "New"; /* Class = "NSMenuItem"; title = "File"; ObjectID = "83"; */ "83.title" = "File"; /* Class = "NSMenuItem"; title = "Help"; ObjectID = "103"; */ "103.title" = "Help"; /* Class = "NSMenu"; title = "Help"; ObjectID = "106"; */ "106.title" = "Help"; /* Class = "NSMenuItem"; title = "Sparkle Test App Help"; ObjectID = "111"; */ "111.title" = "Sparkle Test App Help"; /* Class = "NSMenuItem"; title = "Revert"; ObjectID = "112"; */ "112.title" = "Revert"; /* Class = "NSMenuItem"; title = "Open Recent"; ObjectID = "124"; */ "124.title" = "Open Recent"; /* Class = "NSMenu"; title = "Open Recent"; ObjectID = "125"; */ "125.title" = "Open Recent"; /* Class = "NSMenuItem"; title = "Clear Menu"; ObjectID = "126"; */ "126.title" = "Clear Menu"; /* Class = "NSMenu"; title = "Services"; ObjectID = "130"; */ "130.title" = "Services"; /* Class = "NSMenuItem"; title = "Services"; ObjectID = "131"; */ "131.title" = "Services"; /* Class = "NSMenuItem"; title = "Hide Sparkle Test App"; ObjectID = "134"; */ "134.title" = "Hide Sparkle Test App"; /* Class = "NSMenuItem"; title = "Quit Sparkle Test App"; ObjectID = "136"; */ "136.title" = "Quit Sparkle Test App"; /* Class = "NSMenuItem"; title = "Hide Others"; ObjectID = "145"; */ "145.title" = "Hide Others"; /* Class = "NSMenuItem"; title = "Show All"; ObjectID = "150"; */ "150.title" = "Show All"; /* Class = "NSMenuItem"; title = "Find…"; ObjectID = "154"; */ "154.title" = "Find…"; /* Class = "NSMenuItem"; title = "Jump to Selection"; ObjectID = "155"; */ "155.title" = "Jump to Selection"; /* Class = "NSMenuItem"; title = "Copy"; ObjectID = "157"; */ "157.title" = "Copy"; /* Class = "NSMenuItem"; title = "Undo"; ObjectID = "158"; */ "158.title" = "Undo"; /* Class = "NSMenu"; title = "Find"; ObjectID = "159"; */ "159.title" = "Find"; /* Class = "NSMenuItem"; title = "Cut"; ObjectID = "160"; */ "160.title" = "Cut"; /* Class = "NSMenuItem"; title = "Use Selection for Find"; ObjectID = "161"; */ "161.title" = "Use Selection for Find"; /* Class = "NSMenuItem"; title = "Find Previous"; ObjectID = "162"; */ "162.title" = "Find Previous"; /* Class = "NSMenuItem"; title = "Edit"; ObjectID = "163"; */ "163.title" = "Edit"; /* Class = "NSMenuItem"; title = "Delete"; ObjectID = "164"; */ "164.title" = "Delete"; /* Class = "NSMenuItem"; title = "Find Next"; ObjectID = "167"; */ "167.title" = "Find Next"; /* Class = "NSMenuItem"; title = "Find"; ObjectID = "168"; */ "168.title" = "Find"; /* Class = "NSMenu"; title = "Edit"; ObjectID = "169"; */ "169.title" = "Edit"; /* Class = "NSMenuItem"; title = "Paste"; ObjectID = "171"; */ "171.title" = "Paste"; /* Class = "NSMenuItem"; title = "Select All"; ObjectID = "172"; */ "172.title" = "Select All"; /* Class = "NSMenuItem"; title = "Redo"; ObjectID = "173"; */ "173.title" = "Redo"; /* Class = "NSMenuItem"; title = "Spelling"; ObjectID = "184"; */ "184.title" = "Spelling"; /* Class = "NSMenu"; title = "Spelling"; ObjectID = "185"; */ "185.title" = "Spelling"; /* Class = "NSMenuItem"; title = "Spelling…"; ObjectID = "187"; */ "187.title" = "Spelling…"; /* Class = "NSMenuItem"; title = "Check Spelling"; ObjectID = "189"; */ "189.title" = "Check Spelling"; /* Class = "NSMenuItem"; title = "Check Spelling as You Type"; ObjectID = "191"; */ "191.title" = "Check Spelling as You Type"; /* Class = "NSMenuItem"; title = "Zoom"; ObjectID = "197"; */ "197.title" = "Zoom"; /* Class = "NSMenuItem"; title = "Paste and Match Style"; ObjectID = "204"; */ "204.title" = "Paste and Match Style"; /* Class = "NSMenuItem"; title = "Check for Updates…"; ObjectID = "207"; */ "207.title" = "Check for Updates…"; ================================================ FILE: TestApplication/ca.lproj/MainMenu.strings ================================================ /* Class = "NSMenuItem"; title = "Bring All to Front"; ObjectID = "5"; */ "5.title" = "Bring All to Front"; /* Class = "NSMenuItem"; title = "Window"; ObjectID = "19"; */ "19.title" = "Window"; /* Class = "NSMenuItem"; title = "Minimize"; ObjectID = "23"; */ "23.title" = "Minimize"; /* Class = "NSMenu"; title = "Window"; ObjectID = "24"; */ "24.title" = "Window"; /* Class = "NSMenu"; title = "MainMenu"; ObjectID = "29"; */ "29.title" = "MainMenu"; /* Class = "NSMenuItem"; title = "Sparkle Test App"; ObjectID = "56"; */ "56.title" = "Sparkle Test App"; /* Class = "NSMenu"; title = "Sparkle Test App"; ObjectID = "57"; */ "57.title" = "Sparkle Test App"; /* Class = "NSMenuItem"; title = "About Sparkle Test App"; ObjectID = "58"; */ "58.title" = "About Sparkle Test App"; /* Class = "NSMenuItem"; title = "Open..."; ObjectID = "72"; */ "72.title" = "Open..."; /* Class = "NSMenuItem"; title = "Close"; ObjectID = "73"; */ "73.title" = "Close"; /* Class = "NSMenuItem"; title = "Save"; ObjectID = "75"; */ "75.title" = "Save"; /* Class = "NSMenuItem"; title = "Page Setup…"; ObjectID = "77"; */ "77.title" = "Page Setup…"; /* Class = "NSMenuItem"; title = "Print…"; ObjectID = "78"; */ "78.title" = "Print…"; /* Class = "NSMenuItem"; title = "Save As…"; ObjectID = "80"; */ "80.title" = "Save As…"; /* Class = "NSMenu"; title = "File"; ObjectID = "81"; */ "81.title" = "File"; /* Class = "NSMenuItem"; title = "New"; ObjectID = "82"; */ "82.title" = "New"; /* Class = "NSMenuItem"; title = "File"; ObjectID = "83"; */ "83.title" = "File"; /* Class = "NSMenuItem"; title = "Help"; ObjectID = "103"; */ "103.title" = "Help"; /* Class = "NSMenu"; title = "Help"; ObjectID = "106"; */ "106.title" = "Help"; /* Class = "NSMenuItem"; title = "Sparkle Test App Help"; ObjectID = "111"; */ "111.title" = "Sparkle Test App Help"; /* Class = "NSMenuItem"; title = "Revert"; ObjectID = "112"; */ "112.title" = "Revert"; /* Class = "NSMenuItem"; title = "Open Recent"; ObjectID = "124"; */ "124.title" = "Open Recent"; /* Class = "NSMenu"; title = "Open Recent"; ObjectID = "125"; */ "125.title" = "Open Recent"; /* Class = "NSMenuItem"; title = "Clear Menu"; ObjectID = "126"; */ "126.title" = "Clear Menu"; /* Class = "NSMenu"; title = "Services"; ObjectID = "130"; */ "130.title" = "Services"; /* Class = "NSMenuItem"; title = "Services"; ObjectID = "131"; */ "131.title" = "Services"; /* Class = "NSMenuItem"; title = "Hide Sparkle Test App"; ObjectID = "134"; */ "134.title" = "Hide Sparkle Test App"; /* Class = "NSMenuItem"; title = "Quit Sparkle Test App"; ObjectID = "136"; */ "136.title" = "Quit Sparkle Test App"; /* Class = "NSMenuItem"; title = "Hide Others"; ObjectID = "145"; */ "145.title" = "Hide Others"; /* Class = "NSMenuItem"; title = "Show All"; ObjectID = "150"; */ "150.title" = "Show All"; /* Class = "NSMenuItem"; title = "Find…"; ObjectID = "154"; */ "154.title" = "Find…"; /* Class = "NSMenuItem"; title = "Jump to Selection"; ObjectID = "155"; */ "155.title" = "Jump to Selection"; /* Class = "NSMenuItem"; title = "Copy"; ObjectID = "157"; */ "157.title" = "Copy"; /* Class = "NSMenuItem"; title = "Undo"; ObjectID = "158"; */ "158.title" = "Undo"; /* Class = "NSMenu"; title = "Find"; ObjectID = "159"; */ "159.title" = "Find"; /* Class = "NSMenuItem"; title = "Cut"; ObjectID = "160"; */ "160.title" = "Cut"; /* Class = "NSMenuItem"; title = "Use Selection for Find"; ObjectID = "161"; */ "161.title" = "Use Selection for Find"; /* Class = "NSMenuItem"; title = "Find Previous"; ObjectID = "162"; */ "162.title" = "Find Previous"; /* Class = "NSMenuItem"; title = "Edit"; ObjectID = "163"; */ "163.title" = "Edit"; /* Class = "NSMenuItem"; title = "Delete"; ObjectID = "164"; */ "164.title" = "Delete"; /* Class = "NSMenuItem"; title = "Find Next"; ObjectID = "167"; */ "167.title" = "Find Next"; /* Class = "NSMenuItem"; title = "Find"; ObjectID = "168"; */ "168.title" = "Find"; /* Class = "NSMenu"; title = "Edit"; ObjectID = "169"; */ "169.title" = "Edit"; /* Class = "NSMenuItem"; title = "Paste"; ObjectID = "171"; */ "171.title" = "Paste"; /* Class = "NSMenuItem"; title = "Select All"; ObjectID = "172"; */ "172.title" = "Select All"; /* Class = "NSMenuItem"; title = "Redo"; ObjectID = "173"; */ "173.title" = "Redo"; /* Class = "NSMenuItem"; title = "Spelling"; ObjectID = "184"; */ "184.title" = "Spelling"; /* Class = "NSMenu"; title = "Spelling"; ObjectID = "185"; */ "185.title" = "Spelling"; /* Class = "NSMenuItem"; title = "Spelling…"; ObjectID = "187"; */ "187.title" = "Spelling…"; /* Class = "NSMenuItem"; title = "Check Spelling"; ObjectID = "189"; */ "189.title" = "Check Spelling"; /* Class = "NSMenuItem"; title = "Check Spelling as You Type"; ObjectID = "191"; */ "191.title" = "Check Spelling as You Type"; /* Class = "NSMenuItem"; title = "Zoom"; ObjectID = "197"; */ "197.title" = "Zoom"; /* Class = "NSMenuItem"; title = "Paste and Match Style"; ObjectID = "204"; */ "204.title" = "Paste and Match Style"; /* Class = "NSMenuItem"; title = "Check for Updates…"; ObjectID = "207"; */ "207.title" = "Check for Updates…"; ================================================ FILE: TestApplication/cs.lproj/MainMenu.strings ================================================ /* Class = "NSMenuItem"; title = "Bring All to Front"; ObjectID = "5"; */ "5.title" = "Bring All to Front"; /* Class = "NSMenuItem"; title = "Window"; ObjectID = "19"; */ "19.title" = "Window"; /* Class = "NSMenuItem"; title = "Minimize"; ObjectID = "23"; */ "23.title" = "Minimize"; /* Class = "NSMenu"; title = "Window"; ObjectID = "24"; */ "24.title" = "Window"; /* Class = "NSMenu"; title = "MainMenu"; ObjectID = "29"; */ "29.title" = "MainMenu"; /* Class = "NSMenuItem"; title = "Sparkle Test App"; ObjectID = "56"; */ "56.title" = "Sparkle Test App"; /* Class = "NSMenu"; title = "Sparkle Test App"; ObjectID = "57"; */ "57.title" = "Sparkle Test App"; /* Class = "NSMenuItem"; title = "About Sparkle Test App"; ObjectID = "58"; */ "58.title" = "About Sparkle Test App"; /* Class = "NSMenuItem"; title = "Open..."; ObjectID = "72"; */ "72.title" = "Open..."; /* Class = "NSMenuItem"; title = "Close"; ObjectID = "73"; */ "73.title" = "Close"; /* Class = "NSMenuItem"; title = "Save"; ObjectID = "75"; */ "75.title" = "Save"; /* Class = "NSMenuItem"; title = "Page Setup…"; ObjectID = "77"; */ "77.title" = "Page Setup…"; /* Class = "NSMenuItem"; title = "Print…"; ObjectID = "78"; */ "78.title" = "Print…"; /* Class = "NSMenuItem"; title = "Save As…"; ObjectID = "80"; */ "80.title" = "Save As…"; /* Class = "NSMenu"; title = "File"; ObjectID = "81"; */ "81.title" = "File"; /* Class = "NSMenuItem"; title = "New"; ObjectID = "82"; */ "82.title" = "New"; /* Class = "NSMenuItem"; title = "File"; ObjectID = "83"; */ "83.title" = "File"; /* Class = "NSMenuItem"; title = "Help"; ObjectID = "103"; */ "103.title" = "Help"; /* Class = "NSMenu"; title = "Help"; ObjectID = "106"; */ "106.title" = "Help"; /* Class = "NSMenuItem"; title = "Sparkle Test App Help"; ObjectID = "111"; */ "111.title" = "Sparkle Test App Help"; /* Class = "NSMenuItem"; title = "Revert"; ObjectID = "112"; */ "112.title" = "Revert"; /* Class = "NSMenuItem"; title = "Open Recent"; ObjectID = "124"; */ "124.title" = "Open Recent"; /* Class = "NSMenu"; title = "Open Recent"; ObjectID = "125"; */ "125.title" = "Open Recent"; /* Class = "NSMenuItem"; title = "Clear Menu"; ObjectID = "126"; */ "126.title" = "Clear Menu"; /* Class = "NSMenu"; title = "Services"; ObjectID = "130"; */ "130.title" = "Services"; /* Class = "NSMenuItem"; title = "Services"; ObjectID = "131"; */ "131.title" = "Services"; /* Class = "NSMenuItem"; title = "Hide Sparkle Test App"; ObjectID = "134"; */ "134.title" = "Hide Sparkle Test App"; /* Class = "NSMenuItem"; title = "Quit Sparkle Test App"; ObjectID = "136"; */ "136.title" = "Quit Sparkle Test App"; /* Class = "NSMenuItem"; title = "Hide Others"; ObjectID = "145"; */ "145.title" = "Hide Others"; /* Class = "NSMenuItem"; title = "Show All"; ObjectID = "150"; */ "150.title" = "Show All"; /* Class = "NSMenuItem"; title = "Find…"; ObjectID = "154"; */ "154.title" = "Find…"; /* Class = "NSMenuItem"; title = "Jump to Selection"; ObjectID = "155"; */ "155.title" = "Jump to Selection"; /* Class = "NSMenuItem"; title = "Copy"; ObjectID = "157"; */ "157.title" = "Copy"; /* Class = "NSMenuItem"; title = "Undo"; ObjectID = "158"; */ "158.title" = "Undo"; /* Class = "NSMenu"; title = "Find"; ObjectID = "159"; */ "159.title" = "Find"; /* Class = "NSMenuItem"; title = "Cut"; ObjectID = "160"; */ "160.title" = "Cut"; /* Class = "NSMenuItem"; title = "Use Selection for Find"; ObjectID = "161"; */ "161.title" = "Use Selection for Find"; /* Class = "NSMenuItem"; title = "Find Previous"; ObjectID = "162"; */ "162.title" = "Find Previous"; /* Class = "NSMenuItem"; title = "Edit"; ObjectID = "163"; */ "163.title" = "Edit"; /* Class = "NSMenuItem"; title = "Delete"; ObjectID = "164"; */ "164.title" = "Delete"; /* Class = "NSMenuItem"; title = "Find Next"; ObjectID = "167"; */ "167.title" = "Find Next"; /* Class = "NSMenuItem"; title = "Find"; ObjectID = "168"; */ "168.title" = "Find"; /* Class = "NSMenu"; title = "Edit"; ObjectID = "169"; */ "169.title" = "Edit"; /* Class = "NSMenuItem"; title = "Paste"; ObjectID = "171"; */ "171.title" = "Paste"; /* Class = "NSMenuItem"; title = "Select All"; ObjectID = "172"; */ "172.title" = "Select All"; /* Class = "NSMenuItem"; title = "Redo"; ObjectID = "173"; */ "173.title" = "Redo"; /* Class = "NSMenuItem"; title = "Spelling"; ObjectID = "184"; */ "184.title" = "Spelling"; /* Class = "NSMenu"; title = "Spelling"; ObjectID = "185"; */ "185.title" = "Spelling"; /* Class = "NSMenuItem"; title = "Spelling…"; ObjectID = "187"; */ "187.title" = "Spelling…"; /* Class = "NSMenuItem"; title = "Check Spelling"; ObjectID = "189"; */ "189.title" = "Check Spelling"; /* Class = "NSMenuItem"; title = "Check Spelling as You Type"; ObjectID = "191"; */ "191.title" = "Check Spelling as You Type"; /* Class = "NSMenuItem"; title = "Zoom"; ObjectID = "197"; */ "197.title" = "Zoom"; /* Class = "NSMenuItem"; title = "Paste and Match Style"; ObjectID = "204"; */ "204.title" = "Paste and Match Style"; /* Class = "NSMenuItem"; title = "Check for Updates…"; ObjectID = "207"; */ "207.title" = "Check for Updates…"; ================================================ FILE: TestApplication/da.lproj/MainMenu.strings ================================================ /* Class = "NSMenuItem"; title = "Bring All to Front"; ObjectID = "5"; */ "5.title" = "Bring All to Front"; /* Class = "NSMenuItem"; title = "Window"; ObjectID = "19"; */ "19.title" = "Window"; /* Class = "NSMenuItem"; title = "Minimize"; ObjectID = "23"; */ "23.title" = "Minimize"; /* Class = "NSMenu"; title = "Window"; ObjectID = "24"; */ "24.title" = "Window"; /* Class = "NSMenu"; title = "MainMenu"; ObjectID = "29"; */ "29.title" = "MainMenu"; /* Class = "NSMenuItem"; title = "Sparkle Test App"; ObjectID = "56"; */ "56.title" = "Sparkle Test App"; /* Class = "NSMenu"; title = "Sparkle Test App"; ObjectID = "57"; */ "57.title" = "Sparkle Test App"; /* Class = "NSMenuItem"; title = "About Sparkle Test App"; ObjectID = "58"; */ "58.title" = "About Sparkle Test App"; /* Class = "NSMenuItem"; title = "Open..."; ObjectID = "72"; */ "72.title" = "Open..."; /* Class = "NSMenuItem"; title = "Close"; ObjectID = "73"; */ "73.title" = "Close"; /* Class = "NSMenuItem"; title = "Save"; ObjectID = "75"; */ "75.title" = "Save"; /* Class = "NSMenuItem"; title = "Page Setup…"; ObjectID = "77"; */ "77.title" = "Page Setup…"; /* Class = "NSMenuItem"; title = "Print…"; ObjectID = "78"; */ "78.title" = "Print…"; /* Class = "NSMenuItem"; title = "Save As…"; ObjectID = "80"; */ "80.title" = "Save As…"; /* Class = "NSMenu"; title = "File"; ObjectID = "81"; */ "81.title" = "File"; /* Class = "NSMenuItem"; title = "New"; ObjectID = "82"; */ "82.title" = "New"; /* Class = "NSMenuItem"; title = "File"; ObjectID = "83"; */ "83.title" = "File"; /* Class = "NSMenuItem"; title = "Help"; ObjectID = "103"; */ "103.title" = "Help"; /* Class = "NSMenu"; title = "Help"; ObjectID = "106"; */ "106.title" = "Help"; /* Class = "NSMenuItem"; title = "Sparkle Test App Help"; ObjectID = "111"; */ "111.title" = "Sparkle Test App Help"; /* Class = "NSMenuItem"; title = "Revert"; ObjectID = "112"; */ "112.title" = "Revert"; /* Class = "NSMenuItem"; title = "Open Recent"; ObjectID = "124"; */ "124.title" = "Open Recent"; /* Class = "NSMenu"; title = "Open Recent"; ObjectID = "125"; */ "125.title" = "Open Recent"; /* Class = "NSMenuItem"; title = "Clear Menu"; ObjectID = "126"; */ "126.title" = "Clear Menu"; /* Class = "NSMenu"; title = "Services"; ObjectID = "130"; */ "130.title" = "Services"; /* Class = "NSMenuItem"; title = "Services"; ObjectID = "131"; */ "131.title" = "Services"; /* Class = "NSMenuItem"; title = "Hide Sparkle Test App"; ObjectID = "134"; */ "134.title" = "Hide Sparkle Test App"; /* Class = "NSMenuItem"; title = "Quit Sparkle Test App"; ObjectID = "136"; */ "136.title" = "Quit Sparkle Test App"; /* Class = "NSMenuItem"; title = "Hide Others"; ObjectID = "145"; */ "145.title" = "Hide Others"; /* Class = "NSMenuItem"; title = "Show All"; ObjectID = "150"; */ "150.title" = "Show All"; /* Class = "NSMenuItem"; title = "Find…"; ObjectID = "154"; */ "154.title" = "Find…"; /* Class = "NSMenuItem"; title = "Jump to Selection"; ObjectID = "155"; */ "155.title" = "Jump to Selection"; /* Class = "NSMenuItem"; title = "Copy"; ObjectID = "157"; */ "157.title" = "Copy"; /* Class = "NSMenuItem"; title = "Undo"; ObjectID = "158"; */ "158.title" = "Undo"; /* Class = "NSMenu"; title = "Find"; ObjectID = "159"; */ "159.title" = "Find"; /* Class = "NSMenuItem"; title = "Cut"; ObjectID = "160"; */ "160.title" = "Cut"; /* Class = "NSMenuItem"; title = "Use Selection for Find"; ObjectID = "161"; */ "161.title" = "Use Selection for Find"; /* Class = "NSMenuItem"; title = "Find Previous"; ObjectID = "162"; */ "162.title" = "Find Previous"; /* Class = "NSMenuItem"; title = "Edit"; ObjectID = "163"; */ "163.title" = "Edit"; /* Class = "NSMenuItem"; title = "Delete"; ObjectID = "164"; */ "164.title" = "Delete"; /* Class = "NSMenuItem"; title = "Find Next"; ObjectID = "167"; */ "167.title" = "Find Next"; /* Class = "NSMenuItem"; title = "Find"; ObjectID = "168"; */ "168.title" = "Find"; /* Class = "NSMenu"; title = "Edit"; ObjectID = "169"; */ "169.title" = "Edit"; /* Class = "NSMenuItem"; title = "Paste"; ObjectID = "171"; */ "171.title" = "Paste"; /* Class = "NSMenuItem"; title = "Select All"; ObjectID = "172"; */ "172.title" = "Select All"; /* Class = "NSMenuItem"; title = "Redo"; ObjectID = "173"; */ "173.title" = "Redo"; /* Class = "NSMenuItem"; title = "Spelling"; ObjectID = "184"; */ "184.title" = "Spelling"; /* Class = "NSMenu"; title = "Spelling"; ObjectID = "185"; */ "185.title" = "Spelling"; /* Class = "NSMenuItem"; title = "Spelling…"; ObjectID = "187"; */ "187.title" = "Spelling…"; /* Class = "NSMenuItem"; title = "Check Spelling"; ObjectID = "189"; */ "189.title" = "Check Spelling"; /* Class = "NSMenuItem"; title = "Check Spelling as You Type"; ObjectID = "191"; */ "191.title" = "Check Spelling as You Type"; /* Class = "NSMenuItem"; title = "Zoom"; ObjectID = "197"; */ "197.title" = "Zoom"; /* Class = "NSMenuItem"; title = "Paste and Match Style"; ObjectID = "204"; */ "204.title" = "Paste and Match Style"; /* Class = "NSMenuItem"; title = "Check for Updates…"; ObjectID = "207"; */ "207.title" = "Check for Updates…"; ================================================ FILE: TestApplication/de.lproj/MainMenu.strings ================================================ /* Class = "NSMenuItem"; title = "Bring All to Front"; ObjectID = "5"; */ "5.title" = "Bring All to Front"; /* Class = "NSMenuItem"; title = "Window"; ObjectID = "19"; */ "19.title" = "Window"; /* Class = "NSMenuItem"; title = "Minimize"; ObjectID = "23"; */ "23.title" = "Minimize"; /* Class = "NSMenu"; title = "Window"; ObjectID = "24"; */ "24.title" = "Window"; /* Class = "NSMenu"; title = "MainMenu"; ObjectID = "29"; */ "29.title" = "MainMenu"; /* Class = "NSMenuItem"; title = "Sparkle Test App"; ObjectID = "56"; */ "56.title" = "Sparkle Test App"; /* Class = "NSMenu"; title = "Sparkle Test App"; ObjectID = "57"; */ "57.title" = "Sparkle Test App"; /* Class = "NSMenuItem"; title = "About Sparkle Test App"; ObjectID = "58"; */ "58.title" = "About Sparkle Test App"; /* Class = "NSMenuItem"; title = "Open..."; ObjectID = "72"; */ "72.title" = "Open..."; /* Class = "NSMenuItem"; title = "Close"; ObjectID = "73"; */ "73.title" = "Close"; /* Class = "NSMenuItem"; title = "Save"; ObjectID = "75"; */ "75.title" = "Save"; /* Class = "NSMenuItem"; title = "Page Setup…"; ObjectID = "77"; */ "77.title" = "Page Setup…"; /* Class = "NSMenuItem"; title = "Print…"; ObjectID = "78"; */ "78.title" = "Print…"; /* Class = "NSMenuItem"; title = "Save As…"; ObjectID = "80"; */ "80.title" = "Save As…"; /* Class = "NSMenu"; title = "File"; ObjectID = "81"; */ "81.title" = "File"; /* Class = "NSMenuItem"; title = "New"; ObjectID = "82"; */ "82.title" = "New"; /* Class = "NSMenuItem"; title = "File"; ObjectID = "83"; */ "83.title" = "File"; /* Class = "NSMenuItem"; title = "Help"; ObjectID = "103"; */ "103.title" = "Help"; /* Class = "NSMenu"; title = "Help"; ObjectID = "106"; */ "106.title" = "Help"; /* Class = "NSMenuItem"; title = "Sparkle Test App Help"; ObjectID = "111"; */ "111.title" = "Sparkle Test App Help"; /* Class = "NSMenuItem"; title = "Revert"; ObjectID = "112"; */ "112.title" = "Revert"; /* Class = "NSMenuItem"; title = "Open Recent"; ObjectID = "124"; */ "124.title" = "Open Recent"; /* Class = "NSMenu"; title = "Open Recent"; ObjectID = "125"; */ "125.title" = "Open Recent"; /* Class = "NSMenuItem"; title = "Clear Menu"; ObjectID = "126"; */ "126.title" = "Clear Menu"; /* Class = "NSMenu"; title = "Services"; ObjectID = "130"; */ "130.title" = "Services"; /* Class = "NSMenuItem"; title = "Services"; ObjectID = "131"; */ "131.title" = "Services"; /* Class = "NSMenuItem"; title = "Hide Sparkle Test App"; ObjectID = "134"; */ "134.title" = "Hide Sparkle Test App"; /* Class = "NSMenuItem"; title = "Quit Sparkle Test App"; ObjectID = "136"; */ "136.title" = "Quit Sparkle Test App"; /* Class = "NSMenuItem"; title = "Hide Others"; ObjectID = "145"; */ "145.title" = "Hide Others"; /* Class = "NSMenuItem"; title = "Show All"; ObjectID = "150"; */ "150.title" = "Show All"; /* Class = "NSMenuItem"; title = "Find…"; ObjectID = "154"; */ "154.title" = "Find…"; /* Class = "NSMenuItem"; title = "Jump to Selection"; ObjectID = "155"; */ "155.title" = "Jump to Selection"; /* Class = "NSMenuItem"; title = "Copy"; ObjectID = "157"; */ "157.title" = "Copy"; /* Class = "NSMenuItem"; title = "Undo"; ObjectID = "158"; */ "158.title" = "Undo"; /* Class = "NSMenu"; title = "Find"; ObjectID = "159"; */ "159.title" = "Find"; /* Class = "NSMenuItem"; title = "Cut"; ObjectID = "160"; */ "160.title" = "Cut"; /* Class = "NSMenuItem"; title = "Use Selection for Find"; ObjectID = "161"; */ "161.title" = "Use Selection for Find"; /* Class = "NSMenuItem"; title = "Find Previous"; ObjectID = "162"; */ "162.title" = "Find Previous"; /* Class = "NSMenuItem"; title = "Edit"; ObjectID = "163"; */ "163.title" = "Edit"; /* Class = "NSMenuItem"; title = "Delete"; ObjectID = "164"; */ "164.title" = "Delete"; /* Class = "NSMenuItem"; title = "Find Next"; ObjectID = "167"; */ "167.title" = "Find Next"; /* Class = "NSMenuItem"; title = "Find"; ObjectID = "168"; */ "168.title" = "Find"; /* Class = "NSMenu"; title = "Edit"; ObjectID = "169"; */ "169.title" = "Edit"; /* Class = "NSMenuItem"; title = "Paste"; ObjectID = "171"; */ "171.title" = "Paste"; /* Class = "NSMenuItem"; title = "Select All"; ObjectID = "172"; */ "172.title" = "Select All"; /* Class = "NSMenuItem"; title = "Redo"; ObjectID = "173"; */ "173.title" = "Redo"; /* Class = "NSMenuItem"; title = "Spelling"; ObjectID = "184"; */ "184.title" = "Spelling"; /* Class = "NSMenu"; title = "Spelling"; ObjectID = "185"; */ "185.title" = "Spelling"; /* Class = "NSMenuItem"; title = "Spelling…"; ObjectID = "187"; */ "187.title" = "Spelling…"; /* Class = "NSMenuItem"; title = "Check Spelling"; ObjectID = "189"; */ "189.title" = "Check Spelling"; /* Class = "NSMenuItem"; title = "Check Spelling as You Type"; ObjectID = "191"; */ "191.title" = "Check Spelling as You Type"; /* Class = "NSMenuItem"; title = "Zoom"; ObjectID = "197"; */ "197.title" = "Zoom"; /* Class = "NSMenuItem"; title = "Paste and Match Style"; ObjectID = "204"; */ "204.title" = "Paste and Match Style"; /* Class = "NSMenuItem"; title = "Check for Updates…"; ObjectID = "207"; */ "207.title" = "Check for Updates…"; ================================================ FILE: TestApplication/el.lproj/MainMenu.strings ================================================ /* Class = "NSMenuItem"; title = "Bring All to Front"; ObjectID = "5"; */ "5.title" = "Bring All to Front"; /* Class = "NSMenuItem"; title = "Window"; ObjectID = "19"; */ "19.title" = "Window"; /* Class = "NSMenuItem"; title = "Minimize"; ObjectID = "23"; */ "23.title" = "Minimize"; /* Class = "NSMenu"; title = "Window"; ObjectID = "24"; */ "24.title" = "Window"; /* Class = "NSMenu"; title = "MainMenu"; ObjectID = "29"; */ "29.title" = "MainMenu"; /* Class = "NSMenuItem"; title = "Sparkle Test App"; ObjectID = "56"; */ "56.title" = "Sparkle Test App"; /* Class = "NSMenu"; title = "Sparkle Test App"; ObjectID = "57"; */ "57.title" = "Sparkle Test App"; /* Class = "NSMenuItem"; title = "About Sparkle Test App"; ObjectID = "58"; */ "58.title" = "About Sparkle Test App"; /* Class = "NSMenuItem"; title = "Open..."; ObjectID = "72"; */ "72.title" = "Open..."; /* Class = "NSMenuItem"; title = "Close"; ObjectID = "73"; */ "73.title" = "Close"; /* Class = "NSMenuItem"; title = "Save"; ObjectID = "75"; */ "75.title" = "Save"; /* Class = "NSMenuItem"; title = "Page Setup…"; ObjectID = "77"; */ "77.title" = "Page Setup…"; /* Class = "NSMenuItem"; title = "Print…"; ObjectID = "78"; */ "78.title" = "Print…"; /* Class = "NSMenuItem"; title = "Save As…"; ObjectID = "80"; */ "80.title" = "Save As…"; /* Class = "NSMenu"; title = "File"; ObjectID = "81"; */ "81.title" = "File"; /* Class = "NSMenuItem"; title = "New"; ObjectID = "82"; */ "82.title" = "New"; /* Class = "NSMenuItem"; title = "File"; ObjectID = "83"; */ "83.title" = "File"; /* Class = "NSMenuItem"; title = "Help"; ObjectID = "103"; */ "103.title" = "Help"; /* Class = "NSMenu"; title = "Help"; ObjectID = "106"; */ "106.title" = "Help"; /* Class = "NSMenuItem"; title = "Sparkle Test App Help"; ObjectID = "111"; */ "111.title" = "Sparkle Test App Help"; /* Class = "NSMenuItem"; title = "Revert"; ObjectID = "112"; */ "112.title" = "Revert"; /* Class = "NSMenuItem"; title = "Open Recent"; ObjectID = "124"; */ "124.title" = "Open Recent"; /* Class = "NSMenu"; title = "Open Recent"; ObjectID = "125"; */ "125.title" = "Open Recent"; /* Class = "NSMenuItem"; title = "Clear Menu"; ObjectID = "126"; */ "126.title" = "Clear Menu"; /* Class = "NSMenu"; title = "Services"; ObjectID = "130"; */ "130.title" = "Services"; /* Class = "NSMenuItem"; title = "Services"; ObjectID = "131"; */ "131.title" = "Services"; /* Class = "NSMenuItem"; title = "Hide Sparkle Test App"; ObjectID = "134"; */ "134.title" = "Hide Sparkle Test App"; /* Class = "NSMenuItem"; title = "Quit Sparkle Test App"; ObjectID = "136"; */ "136.title" = "Quit Sparkle Test App"; /* Class = "NSMenuItem"; title = "Hide Others"; ObjectID = "145"; */ "145.title" = "Hide Others"; /* Class = "NSMenuItem"; title = "Show All"; ObjectID = "150"; */ "150.title" = "Show All"; /* Class = "NSMenuItem"; title = "Find…"; ObjectID = "154"; */ "154.title" = "Find…"; /* Class = "NSMenuItem"; title = "Jump to Selection"; ObjectID = "155"; */ "155.title" = "Jump to Selection"; /* Class = "NSMenuItem"; title = "Copy"; ObjectID = "157"; */ "157.title" = "Copy"; /* Class = "NSMenuItem"; title = "Undo"; ObjectID = "158"; */ "158.title" = "Undo"; /* Class = "NSMenu"; title = "Find"; ObjectID = "159"; */ "159.title" = "Find"; /* Class = "NSMenuItem"; title = "Cut"; ObjectID = "160"; */ "160.title" = "Cut"; /* Class = "NSMenuItem"; title = "Use Selection for Find"; ObjectID = "161"; */ "161.title" = "Use Selection for Find"; /* Class = "NSMenuItem"; title = "Find Previous"; ObjectID = "162"; */ "162.title" = "Find Previous"; /* Class = "NSMenuItem"; title = "Edit"; ObjectID = "163"; */ "163.title" = "Edit"; /* Class = "NSMenuItem"; title = "Delete"; ObjectID = "164"; */ "164.title" = "Delete"; /* Class = "NSMenuItem"; title = "Find Next"; ObjectID = "167"; */ "167.title" = "Find Next"; /* Class = "NSMenuItem"; title = "Find"; ObjectID = "168"; */ "168.title" = "Find"; /* Class = "NSMenu"; title = "Edit"; ObjectID = "169"; */ "169.title" = "Edit"; /* Class = "NSMenuItem"; title = "Paste"; ObjectID = "171"; */ "171.title" = "Paste"; /* Class = "NSMenuItem"; title = "Select All"; ObjectID = "172"; */ "172.title" = "Select All"; /* Class = "NSMenuItem"; title = "Redo"; ObjectID = "173"; */ "173.title" = "Redo"; /* Class = "NSMenuItem"; title = "Spelling"; ObjectID = "184"; */ "184.title" = "Spelling"; /* Class = "NSMenu"; title = "Spelling"; ObjectID = "185"; */ "185.title" = "Spelling"; /* Class = "NSMenuItem"; title = "Spelling…"; ObjectID = "187"; */ "187.title" = "Spelling…"; /* Class = "NSMenuItem"; title = "Check Spelling"; ObjectID = "189"; */ "189.title" = "Check Spelling"; /* Class = "NSMenuItem"; title = "Check Spelling as You Type"; ObjectID = "191"; */ "191.title" = "Check Spelling as You Type"; /* Class = "NSMenuItem"; title = "Zoom"; ObjectID = "197"; */ "197.title" = "Zoom"; /* Class = "NSMenuItem"; title = "Paste and Match Style"; ObjectID = "204"; */ "204.title" = "Paste and Match Style"; /* Class = "NSMenuItem"; title = "Check for Updates…"; ObjectID = "207"; */ "207.title" = "Check for Updates…"; ================================================ FILE: TestApplication/en.lproj/MainMenu.strings ================================================ /* Class = "NSMenuItem"; title = "Bring All to Front"; ObjectID = "5"; */ "5.title" = "Bring All to Front"; /* Class = "NSMenuItem"; title = "Window"; ObjectID = "19"; */ "19.title" = "Window"; /* Class = "NSMenuItem"; title = "Minimize"; ObjectID = "23"; */ "23.title" = "Minimize"; /* Class = "NSMenu"; title = "Window"; ObjectID = "24"; */ "24.title" = "Window"; /* Class = "NSMenu"; title = "MainMenu"; ObjectID = "29"; */ "29.title" = "MainMenu"; /* Class = "NSMenuItem"; title = "Sparkle Test App"; ObjectID = "56"; */ "56.title" = "Sparkle Test App"; /* Class = "NSMenu"; title = "Sparkle Test App"; ObjectID = "57"; */ "57.title" = "Sparkle Test App"; /* Class = "NSMenuItem"; title = "About Sparkle Test App"; ObjectID = "58"; */ "58.title" = "About Sparkle Test App"; /* Class = "NSMenuItem"; title = "Open..."; ObjectID = "72"; */ "72.title" = "Open..."; /* Class = "NSMenuItem"; title = "Close"; ObjectID = "73"; */ "73.title" = "Close"; /* Class = "NSMenuItem"; title = "Save"; ObjectID = "75"; */ "75.title" = "Save"; /* Class = "NSMenuItem"; title = "Page Setup…"; ObjectID = "77"; */ "77.title" = "Page Setup…"; /* Class = "NSMenuItem"; title = "Print…"; ObjectID = "78"; */ "78.title" = "Print…"; /* Class = "NSMenuItem"; title = "Save As…"; ObjectID = "80"; */ "80.title" = "Save As…"; /* Class = "NSMenu"; title = "File"; ObjectID = "81"; */ "81.title" = "File"; /* Class = "NSMenuItem"; title = "New"; ObjectID = "82"; */ "82.title" = "New"; /* Class = "NSMenuItem"; title = "File"; ObjectID = "83"; */ "83.title" = "File"; /* Class = "NSMenuItem"; title = "Help"; ObjectID = "103"; */ "103.title" = "Help"; /* Class = "NSMenu"; title = "Help"; ObjectID = "106"; */ "106.title" = "Help"; /* Class = "NSMenuItem"; title = "Sparkle Test App Help"; ObjectID = "111"; */ "111.title" = "Sparkle Test App Help"; /* Class = "NSMenuItem"; title = "Revert"; ObjectID = "112"; */ "112.title" = "Revert"; /* Class = "NSMenuItem"; title = "Open Recent"; ObjectID = "124"; */ "124.title" = "Open Recent"; /* Class = "NSMenu"; title = "Open Recent"; ObjectID = "125"; */ "125.title" = "Open Recent"; /* Class = "NSMenuItem"; title = "Clear Menu"; ObjectID = "126"; */ "126.title" = "Clear Menu"; /* Class = "NSMenu"; title = "Services"; ObjectID = "130"; */ "130.title" = "Services"; /* Class = "NSMenuItem"; title = "Services"; ObjectID = "131"; */ "131.title" = "Services"; /* Class = "NSMenuItem"; title = "Hide Sparkle Test App"; ObjectID = "134"; */ "134.title" = "Hide Sparkle Test App"; /* Class = "NSMenuItem"; title = "Quit Sparkle Test App"; ObjectID = "136"; */ "136.title" = "Quit Sparkle Test App"; /* Class = "NSMenuItem"; title = "Hide Others"; ObjectID = "145"; */ "145.title" = "Hide Others"; /* Class = "NSMenuItem"; title = "Show All"; ObjectID = "150"; */ "150.title" = "Show All"; /* Class = "NSMenuItem"; title = "Find…"; ObjectID = "154"; */ "154.title" = "Find…"; /* Class = "NSMenuItem"; title = "Jump to Selection"; ObjectID = "155"; */ "155.title" = "Jump to Selection"; /* Class = "NSMenuItem"; title = "Copy"; ObjectID = "157"; */ "157.title" = "Copy"; /* Class = "NSMenuItem"; title = "Undo"; ObjectID = "158"; */ "158.title" = "Undo"; /* Class = "NSMenu"; title = "Find"; ObjectID = "159"; */ "159.title" = "Find"; /* Class = "NSMenuItem"; title = "Cut"; ObjectID = "160"; */ "160.title" = "Cut"; /* Class = "NSMenuItem"; title = "Use Selection for Find"; ObjectID = "161"; */ "161.title" = "Use Selection for Find"; /* Class = "NSMenuItem"; title = "Find Previous"; ObjectID = "162"; */ "162.title" = "Find Previous"; /* Class = "NSMenuItem"; title = "Edit"; ObjectID = "163"; */ "163.title" = "Edit"; /* Class = "NSMenuItem"; title = "Delete"; ObjectID = "164"; */ "164.title" = "Delete"; /* Class = "NSMenuItem"; title = "Find Next"; ObjectID = "167"; */ "167.title" = "Find Next"; /* Class = "NSMenuItem"; title = "Find"; ObjectID = "168"; */ "168.title" = "Find"; /* Class = "NSMenu"; title = "Edit"; ObjectID = "169"; */ "169.title" = "Edit"; /* Class = "NSMenuItem"; title = "Paste"; ObjectID = "171"; */ "171.title" = "Paste"; /* Class = "NSMenuItem"; title = "Select All"; ObjectID = "172"; */ "172.title" = "Select All"; /* Class = "NSMenuItem"; title = "Redo"; ObjectID = "173"; */ "173.title" = "Redo"; /* Class = "NSMenuItem"; title = "Spelling"; ObjectID = "184"; */ "184.title" = "Spelling"; /* Class = "NSMenu"; title = "Spelling"; ObjectID = "185"; */ "185.title" = "Spelling"; /* Class = "NSMenuItem"; title = "Spelling…"; ObjectID = "187"; */ "187.title" = "Spelling…"; /* Class = "NSMenuItem"; title = "Check Spelling"; ObjectID = "189"; */ "189.title" = "Check Spelling"; /* Class = "NSMenuItem"; title = "Check Spelling as You Type"; ObjectID = "191"; */ "191.title" = "Check Spelling as You Type"; /* Class = "NSMenuItem"; title = "Zoom"; ObjectID = "197"; */ "197.title" = "Zoom"; /* Class = "NSMenuItem"; title = "Paste and Match Style"; ObjectID = "204"; */ "204.title" = "Paste and Match Style"; /* Class = "NSMenuItem"; title = "Check for Updates…"; ObjectID = "207"; */ "207.title" = "Check for Updates…"; ================================================ FILE: TestApplication/es.lproj/MainMenu.strings ================================================ /* Class = "NSMenuItem"; title = "Bring All to Front"; ObjectID = "5"; */ "5.title" = "Bring All to Front"; /* Class = "NSMenuItem"; title = "Window"; ObjectID = "19"; */ "19.title" = "Window"; /* Class = "NSMenuItem"; title = "Minimize"; ObjectID = "23"; */ "23.title" = "Minimize"; /* Class = "NSMenu"; title = "Window"; ObjectID = "24"; */ "24.title" = "Window"; /* Class = "NSMenu"; title = "MainMenu"; ObjectID = "29"; */ "29.title" = "MainMenu"; /* Class = "NSMenuItem"; title = "Sparkle Test App"; ObjectID = "56"; */ "56.title" = "Sparkle Test App"; /* Class = "NSMenu"; title = "Sparkle Test App"; ObjectID = "57"; */ "57.title" = "Sparkle Test App"; /* Class = "NSMenuItem"; title = "About Sparkle Test App"; ObjectID = "58"; */ "58.title" = "About Sparkle Test App"; /* Class = "NSMenuItem"; title = "Open..."; ObjectID = "72"; */ "72.title" = "Open..."; /* Class = "NSMenuItem"; title = "Close"; ObjectID = "73"; */ "73.title" = "Close"; /* Class = "NSMenuItem"; title = "Save"; ObjectID = "75"; */ "75.title" = "Save"; /* Class = "NSMenuItem"; title = "Page Setup…"; ObjectID = "77"; */ "77.title" = "Page Setup…"; /* Class = "NSMenuItem"; title = "Print…"; ObjectID = "78"; */ "78.title" = "Print…"; /* Class = "NSMenuItem"; title = "Save As…"; ObjectID = "80"; */ "80.title" = "Save As…"; /* Class = "NSMenu"; title = "File"; ObjectID = "81"; */ "81.title" = "File"; /* Class = "NSMenuItem"; title = "New"; ObjectID = "82"; */ "82.title" = "New"; /* Class = "NSMenuItem"; title = "File"; ObjectID = "83"; */ "83.title" = "File"; /* Class = "NSMenuItem"; title = "Help"; ObjectID = "103"; */ "103.title" = "Help"; /* Class = "NSMenu"; title = "Help"; ObjectID = "106"; */ "106.title" = "Help"; /* Class = "NSMenuItem"; title = "Sparkle Test App Help"; ObjectID = "111"; */ "111.title" = "Sparkle Test App Help"; /* Class = "NSMenuItem"; title = "Revert"; ObjectID = "112"; */ "112.title" = "Revert"; /* Class = "NSMenuItem"; title = "Open Recent"; ObjectID = "124"; */ "124.title" = "Open Recent"; /* Class = "NSMenu"; title = "Open Recent"; ObjectID = "125"; */ "125.title" = "Open Recent"; /* Class = "NSMenuItem"; title = "Clear Menu"; ObjectID = "126"; */ "126.title" = "Clear Menu"; /* Class = "NSMenu"; title = "Services"; ObjectID = "130"; */ "130.title" = "Services"; /* Class = "NSMenuItem"; title = "Services"; ObjectID = "131"; */ "131.title" = "Services"; /* Class = "NSMenuItem"; title = "Hide Sparkle Test App"; ObjectID = "134"; */ "134.title" = "Hide Sparkle Test App"; /* Class = "NSMenuItem"; title = "Quit Sparkle Test App"; ObjectID = "136"; */ "136.title" = "Quit Sparkle Test App"; /* Class = "NSMenuItem"; title = "Hide Others"; ObjectID = "145"; */ "145.title" = "Hide Others"; /* Class = "NSMenuItem"; title = "Show All"; ObjectID = "150"; */ "150.title" = "Show All"; /* Class = "NSMenuItem"; title = "Find…"; ObjectID = "154"; */ "154.title" = "Find…"; /* Class = "NSMenuItem"; title = "Jump to Selection"; ObjectID = "155"; */ "155.title" = "Jump to Selection"; /* Class = "NSMenuItem"; title = "Copy"; ObjectID = "157"; */ "157.title" = "Copy"; /* Class = "NSMenuItem"; title = "Undo"; ObjectID = "158"; */ "158.title" = "Undo"; /* Class = "NSMenu"; title = "Find"; ObjectID = "159"; */ "159.title" = "Find"; /* Class = "NSMenuItem"; title = "Cut"; ObjectID = "160"; */ "160.title" = "Cut"; /* Class = "NSMenuItem"; title = "Use Selection for Find"; ObjectID = "161"; */ "161.title" = "Use Selection for Find"; /* Class = "NSMenuItem"; title = "Find Previous"; ObjectID = "162"; */ "162.title" = "Find Previous"; /* Class = "NSMenuItem"; title = "Edit"; ObjectID = "163"; */ "163.title" = "Edit"; /* Class = "NSMenuItem"; title = "Delete"; ObjectID = "164"; */ "164.title" = "Delete"; /* Class = "NSMenuItem"; title = "Find Next"; ObjectID = "167"; */ "167.title" = "Find Next"; /* Class = "NSMenuItem"; title = "Find"; ObjectID = "168"; */ "168.title" = "Find"; /* Class = "NSMenu"; title = "Edit"; ObjectID = "169"; */ "169.title" = "Edit"; /* Class = "NSMenuItem"; title = "Paste"; ObjectID = "171"; */ "171.title" = "Paste"; /* Class = "NSMenuItem"; title = "Select All"; ObjectID = "172"; */ "172.title" = "Select All"; /* Class = "NSMenuItem"; title = "Redo"; ObjectID = "173"; */ "173.title" = "Redo"; /* Class = "NSMenuItem"; title = "Spelling"; ObjectID = "184"; */ "184.title" = "Spelling"; /* Class = "NSMenu"; title = "Spelling"; ObjectID = "185"; */ "185.title" = "Spelling"; /* Class = "NSMenuItem"; title = "Spelling…"; ObjectID = "187"; */ "187.title" = "Spelling…"; /* Class = "NSMenuItem"; title = "Check Spelling"; ObjectID = "189"; */ "189.title" = "Check Spelling"; /* Class = "NSMenuItem"; title = "Check Spelling as You Type"; ObjectID = "191"; */ "191.title" = "Check Spelling as You Type"; /* Class = "NSMenuItem"; title = "Zoom"; ObjectID = "197"; */ "197.title" = "Zoom"; /* Class = "NSMenuItem"; title = "Paste and Match Style"; ObjectID = "204"; */ "204.title" = "Paste and Match Style"; /* Class = "NSMenuItem"; title = "Check for Updates…"; ObjectID = "207"; */ "207.title" = "Check for Updates…"; ================================================ FILE: TestApplication/fa.lproj/MainMenu.strings ================================================ /* Class = "NSMenuItem"; title = "Bring All to Front"; ObjectID = "5"; */ "5.title" = "Bring All to Front"; /* Class = "NSMenuItem"; title = "Window"; ObjectID = "19"; */ "19.title" = "Window"; /* Class = "NSMenuItem"; title = "Minimize"; ObjectID = "23"; */ "23.title" = "Minimize"; /* Class = "NSMenu"; title = "Window"; ObjectID = "24"; */ "24.title" = "Window"; /* Class = "NSMenu"; title = "MainMenu"; ObjectID = "29"; */ "29.title" = "MainMenu"; /* Class = "NSMenuItem"; title = "Sparkle Test App"; ObjectID = "56"; */ "56.title" = "Sparkle Test App"; /* Class = "NSMenu"; title = "Sparkle Test App"; ObjectID = "57"; */ "57.title" = "Sparkle Test App"; /* Class = "NSMenuItem"; title = "About Sparkle Test App"; ObjectID = "58"; */ "58.title" = "About Sparkle Test App"; /* Class = "NSMenuItem"; title = "Open..."; ObjectID = "72"; */ "72.title" = "Open..."; /* Class = "NSMenuItem"; title = "Close"; ObjectID = "73"; */ "73.title" = "Close"; /* Class = "NSMenuItem"; title = "Save"; ObjectID = "75"; */ "75.title" = "Save"; /* Class = "NSMenuItem"; title = "Page Setup…"; ObjectID = "77"; */ "77.title" = "Page Setup…"; /* Class = "NSMenuItem"; title = "Print…"; ObjectID = "78"; */ "78.title" = "Print…"; /* Class = "NSMenuItem"; title = "Save As…"; ObjectID = "80"; */ "80.title" = "Save As…"; /* Class = "NSMenu"; title = "File"; ObjectID = "81"; */ "81.title" = "File"; /* Class = "NSMenuItem"; title = "New"; ObjectID = "82"; */ "82.title" = "New"; /* Class = "NSMenuItem"; title = "File"; ObjectID = "83"; */ "83.title" = "File"; /* Class = "NSMenuItem"; title = "Help"; ObjectID = "103"; */ "103.title" = "Help"; /* Class = "NSMenu"; title = "Help"; ObjectID = "106"; */ "106.title" = "Help"; /* Class = "NSMenuItem"; title = "Sparkle Test App Help"; ObjectID = "111"; */ "111.title" = "Sparkle Test App Help"; /* Class = "NSMenuItem"; title = "Revert"; ObjectID = "112"; */ "112.title" = "Revert"; /* Class = "NSMenuItem"; title = "Open Recent"; ObjectID = "124"; */ "124.title" = "Open Recent"; /* Class = "NSMenu"; title = "Open Recent"; ObjectID = "125"; */ "125.title" = "Open Recent"; /* Class = "NSMenuItem"; title = "Clear Menu"; ObjectID = "126"; */ "126.title" = "Clear Menu"; /* Class = "NSMenu"; title = "Services"; ObjectID = "130"; */ "130.title" = "Services"; /* Class = "NSMenuItem"; title = "Services"; ObjectID = "131"; */ "131.title" = "Services"; /* Class = "NSMenuItem"; title = "Hide Sparkle Test App"; ObjectID = "134"; */ "134.title" = "Hide Sparkle Test App"; /* Class = "NSMenuItem"; title = "Quit Sparkle Test App"; ObjectID = "136"; */ "136.title" = "Quit Sparkle Test App"; /* Class = "NSMenuItem"; title = "Hide Others"; ObjectID = "145"; */ "145.title" = "Hide Others"; /* Class = "NSMenuItem"; title = "Show All"; ObjectID = "150"; */ "150.title" = "Show All"; /* Class = "NSMenuItem"; title = "Find…"; ObjectID = "154"; */ "154.title" = "Find…"; /* Class = "NSMenuItem"; title = "Jump to Selection"; ObjectID = "155"; */ "155.title" = "Jump to Selection"; /* Class = "NSMenuItem"; title = "Copy"; ObjectID = "157"; */ "157.title" = "Copy"; /* Class = "NSMenuItem"; title = "Undo"; ObjectID = "158"; */ "158.title" = "Undo"; /* Class = "NSMenu"; title = "Find"; ObjectID = "159"; */ "159.title" = "Find"; /* Class = "NSMenuItem"; title = "Cut"; ObjectID = "160"; */ "160.title" = "Cut"; /* Class = "NSMenuItem"; title = "Use Selection for Find"; ObjectID = "161"; */ "161.title" = "Use Selection for Find"; /* Class = "NSMenuItem"; title = "Find Previous"; ObjectID = "162"; */ "162.title" = "Find Previous"; /* Class = "NSMenuItem"; title = "Edit"; ObjectID = "163"; */ "163.title" = "Edit"; /* Class = "NSMenuItem"; title = "Delete"; ObjectID = "164"; */ "164.title" = "Delete"; /* Class = "NSMenuItem"; title = "Find Next"; ObjectID = "167"; */ "167.title" = "Find Next"; /* Class = "NSMenuItem"; title = "Find"; ObjectID = "168"; */ "168.title" = "Find"; /* Class = "NSMenu"; title = "Edit"; ObjectID = "169"; */ "169.title" = "Edit"; /* Class = "NSMenuItem"; title = "Paste"; ObjectID = "171"; */ "171.title" = "Paste"; /* Class = "NSMenuItem"; title = "Select All"; ObjectID = "172"; */ "172.title" = "Select All"; /* Class = "NSMenuItem"; title = "Redo"; ObjectID = "173"; */ "173.title" = "Redo"; /* Class = "NSMenuItem"; title = "Spelling"; ObjectID = "184"; */ "184.title" = "Spelling"; /* Class = "NSMenu"; title = "Spelling"; ObjectID = "185"; */ "185.title" = "Spelling"; /* Class = "NSMenuItem"; title = "Spelling…"; ObjectID = "187"; */ "187.title" = "Spelling…"; /* Class = "NSMenuItem"; title = "Check Spelling"; ObjectID = "189"; */ "189.title" = "Check Spelling"; /* Class = "NSMenuItem"; title = "Check Spelling as You Type"; ObjectID = "191"; */ "191.title" = "Check Spelling as You Type"; /* Class = "NSMenuItem"; title = "Zoom"; ObjectID = "197"; */ "197.title" = "Zoom"; /* Class = "NSMenuItem"; title = "Paste and Match Style"; ObjectID = "204"; */ "204.title" = "Paste and Match Style"; /* Class = "NSMenuItem"; title = "Check for Updates…"; ObjectID = "207"; */ "207.title" = "Check for Updates…"; ================================================ FILE: TestApplication/fi.lproj/MainMenu.strings ================================================ /* Class = "NSMenuItem"; title = "Bring All to Front"; ObjectID = "5"; */ "5.title" = "Bring All to Front"; /* Class = "NSMenuItem"; title = "Window"; ObjectID = "19"; */ "19.title" = "Window"; /* Class = "NSMenuItem"; title = "Minimize"; ObjectID = "23"; */ "23.title" = "Minimize"; /* Class = "NSMenu"; title = "Window"; ObjectID = "24"; */ "24.title" = "Window"; /* Class = "NSMenu"; title = "MainMenu"; ObjectID = "29"; */ "29.title" = "MainMenu"; /* Class = "NSMenuItem"; title = "Sparkle Test App"; ObjectID = "56"; */ "56.title" = "Sparkle Test App"; /* Class = "NSMenu"; title = "Sparkle Test App"; ObjectID = "57"; */ "57.title" = "Sparkle Test App"; /* Class = "NSMenuItem"; title = "About Sparkle Test App"; ObjectID = "58"; */ "58.title" = "About Sparkle Test App"; /* Class = "NSMenuItem"; title = "Open..."; ObjectID = "72"; */ "72.title" = "Open..."; /* Class = "NSMenuItem"; title = "Close"; ObjectID = "73"; */ "73.title" = "Close"; /* Class = "NSMenuItem"; title = "Save"; ObjectID = "75"; */ "75.title" = "Save"; /* Class = "NSMenuItem"; title = "Page Setup…"; ObjectID = "77"; */ "77.title" = "Page Setup…"; /* Class = "NSMenuItem"; title = "Print…"; ObjectID = "78"; */ "78.title" = "Print…"; /* Class = "NSMenuItem"; title = "Save As…"; ObjectID = "80"; */ "80.title" = "Save As…"; /* Class = "NSMenu"; title = "File"; ObjectID = "81"; */ "81.title" = "File"; /* Class = "NSMenuItem"; title = "New"; ObjectID = "82"; */ "82.title" = "New"; /* Class = "NSMenuItem"; title = "File"; ObjectID = "83"; */ "83.title" = "File"; /* Class = "NSMenuItem"; title = "Help"; ObjectID = "103"; */ "103.title" = "Help"; /* Class = "NSMenu"; title = "Help"; ObjectID = "106"; */ "106.title" = "Help"; /* Class = "NSMenuItem"; title = "Sparkle Test App Help"; ObjectID = "111"; */ "111.title" = "Sparkle Test App Help"; /* Class = "NSMenuItem"; title = "Revert"; ObjectID = "112"; */ "112.title" = "Revert"; /* Class = "NSMenuItem"; title = "Open Recent"; ObjectID = "124"; */ "124.title" = "Open Recent"; /* Class = "NSMenu"; title = "Open Recent"; ObjectID = "125"; */ "125.title" = "Open Recent"; /* Class = "NSMenuItem"; title = "Clear Menu"; ObjectID = "126"; */ "126.title" = "Clear Menu"; /* Class = "NSMenu"; title = "Services"; ObjectID = "130"; */ "130.title" = "Services"; /* Class = "NSMenuItem"; title = "Services"; ObjectID = "131"; */ "131.title" = "Services"; /* Class = "NSMenuItem"; title = "Hide Sparkle Test App"; ObjectID = "134"; */ "134.title" = "Hide Sparkle Test App"; /* Class = "NSMenuItem"; title = "Quit Sparkle Test App"; ObjectID = "136"; */ "136.title" = "Quit Sparkle Test App"; /* Class = "NSMenuItem"; title = "Hide Others"; ObjectID = "145"; */ "145.title" = "Hide Others"; /* Class = "NSMenuItem"; title = "Show All"; ObjectID = "150"; */ "150.title" = "Show All"; /* Class = "NSMenuItem"; title = "Find…"; ObjectID = "154"; */ "154.title" = "Find…"; /* Class = "NSMenuItem"; title = "Jump to Selection"; ObjectID = "155"; */ "155.title" = "Jump to Selection"; /* Class = "NSMenuItem"; title = "Copy"; ObjectID = "157"; */ "157.title" = "Copy"; /* Class = "NSMenuItem"; title = "Undo"; ObjectID = "158"; */ "158.title" = "Undo"; /* Class = "NSMenu"; title = "Find"; ObjectID = "159"; */ "159.title" = "Find"; /* Class = "NSMenuItem"; title = "Cut"; ObjectID = "160"; */ "160.title" = "Cut"; /* Class = "NSMenuItem"; title = "Use Selection for Find"; ObjectID = "161"; */ "161.title" = "Use Selection for Find"; /* Class = "NSMenuItem"; title = "Find Previous"; ObjectID = "162"; */ "162.title" = "Find Previous"; /* Class = "NSMenuItem"; title = "Edit"; ObjectID = "163"; */ "163.title" = "Edit"; /* Class = "NSMenuItem"; title = "Delete"; ObjectID = "164"; */ "164.title" = "Delete"; /* Class = "NSMenuItem"; title = "Find Next"; ObjectID = "167"; */ "167.title" = "Find Next"; /* Class = "NSMenuItem"; title = "Find"; ObjectID = "168"; */ "168.title" = "Find"; /* Class = "NSMenu"; title = "Edit"; ObjectID = "169"; */ "169.title" = "Edit"; /* Class = "NSMenuItem"; title = "Paste"; ObjectID = "171"; */ "171.title" = "Paste"; /* Class = "NSMenuItem"; title = "Select All"; ObjectID = "172"; */ "172.title" = "Select All"; /* Class = "NSMenuItem"; title = "Redo"; ObjectID = "173"; */ "173.title" = "Redo"; /* Class = "NSMenuItem"; title = "Spelling"; ObjectID = "184"; */ "184.title" = "Spelling"; /* Class = "NSMenu"; title = "Spelling"; ObjectID = "185"; */ "185.title" = "Spelling"; /* Class = "NSMenuItem"; title = "Spelling…"; ObjectID = "187"; */ "187.title" = "Spelling…"; /* Class = "NSMenuItem"; title = "Check Spelling"; ObjectID = "189"; */ "189.title" = "Check Spelling"; /* Class = "NSMenuItem"; title = "Check Spelling as You Type"; ObjectID = "191"; */ "191.title" = "Check Spelling as You Type"; /* Class = "NSMenuItem"; title = "Zoom"; ObjectID = "197"; */ "197.title" = "Zoom"; /* Class = "NSMenuItem"; title = "Paste and Match Style"; ObjectID = "204"; */ "204.title" = "Paste and Match Style"; /* Class = "NSMenuItem"; title = "Check for Updates…"; ObjectID = "207"; */ "207.title" = "Check for Updates…"; ================================================ FILE: TestApplication/fr.lproj/MainMenu.strings ================================================ /* Class = "NSMenuItem"; title = "Bring All to Front"; ObjectID = "5"; */ "5.title" = "Bring All to Front"; /* Class = "NSMenuItem"; title = "Window"; ObjectID = "19"; */ "19.title" = "Window"; /* Class = "NSMenuItem"; title = "Minimize"; ObjectID = "23"; */ "23.title" = "Minimize"; /* Class = "NSMenu"; title = "Window"; ObjectID = "24"; */ "24.title" = "Window"; /* Class = "NSMenu"; title = "MainMenu"; ObjectID = "29"; */ "29.title" = "MainMenu"; /* Class = "NSMenuItem"; title = "Sparkle Test App"; ObjectID = "56"; */ "56.title" = "Sparkle Test App"; /* Class = "NSMenu"; title = "Sparkle Test App"; ObjectID = "57"; */ "57.title" = "Sparkle Test App"; /* Class = "NSMenuItem"; title = "About Sparkle Test App"; ObjectID = "58"; */ "58.title" = "About Sparkle Test App"; /* Class = "NSMenuItem"; title = "Open..."; ObjectID = "72"; */ "72.title" = "Open..."; /* Class = "NSMenuItem"; title = "Close"; ObjectID = "73"; */ "73.title" = "Close"; /* Class = "NSMenuItem"; title = "Save"; ObjectID = "75"; */ "75.title" = "Save"; /* Class = "NSMenuItem"; title = "Page Setup…"; ObjectID = "77"; */ "77.title" = "Page Setup…"; /* Class = "NSMenuItem"; title = "Print…"; ObjectID = "78"; */ "78.title" = "Print…"; /* Class = "NSMenuItem"; title = "Save As…"; ObjectID = "80"; */ "80.title" = "Save As…"; /* Class = "NSMenu"; title = "File"; ObjectID = "81"; */ "81.title" = "File"; /* Class = "NSMenuItem"; title = "New"; ObjectID = "82"; */ "82.title" = "New"; /* Class = "NSMenuItem"; title = "File"; ObjectID = "83"; */ "83.title" = "File"; /* Class = "NSMenuItem"; title = "Help"; ObjectID = "103"; */ "103.title" = "Help"; /* Class = "NSMenu"; title = "Help"; ObjectID = "106"; */ "106.title" = "Help"; /* Class = "NSMenuItem"; title = "Sparkle Test App Help"; ObjectID = "111"; */ "111.title" = "Sparkle Test App Help"; /* Class = "NSMenuItem"; title = "Revert"; ObjectID = "112"; */ "112.title" = "Revert"; /* Class = "NSMenuItem"; title = "Open Recent"; ObjectID = "124"; */ "124.title" = "Open Recent"; /* Class = "NSMenu"; title = "Open Recent"; ObjectID = "125"; */ "125.title" = "Open Recent"; /* Class = "NSMenuItem"; title = "Clear Menu"; ObjectID = "126"; */ "126.title" = "Clear Menu"; /* Class = "NSMenu"; title = "Services"; ObjectID = "130"; */ "130.title" = "Services"; /* Class = "NSMenuItem"; title = "Services"; ObjectID = "131"; */ "131.title" = "Services"; /* Class = "NSMenuItem"; title = "Hide Sparkle Test App"; ObjectID = "134"; */ "134.title" = "Hide Sparkle Test App"; /* Class = "NSMenuItem"; title = "Quit Sparkle Test App"; ObjectID = "136"; */ "136.title" = "Quit Sparkle Test App"; /* Class = "NSMenuItem"; title = "Hide Others"; ObjectID = "145"; */ "145.title" = "Hide Others"; /* Class = "NSMenuItem"; title = "Show All"; ObjectID = "150"; */ "150.title" = "Show All"; /* Class = "NSMenuItem"; title = "Find…"; ObjectID = "154"; */ "154.title" = "Find…"; /* Class = "NSMenuItem"; title = "Jump to Selection"; ObjectID = "155"; */ "155.title" = "Jump to Selection"; /* Class = "NSMenuItem"; title = "Copy"; ObjectID = "157"; */ "157.title" = "Copy"; /* Class = "NSMenuItem"; title = "Undo"; ObjectID = "158"; */ "158.title" = "Undo"; /* Class = "NSMenu"; title = "Find"; ObjectID = "159"; */ "159.title" = "Find"; /* Class = "NSMenuItem"; title = "Cut"; ObjectID = "160"; */ "160.title" = "Cut"; /* Class = "NSMenuItem"; title = "Use Selection for Find"; ObjectID = "161"; */ "161.title" = "Use Selection for Find"; /* Class = "NSMenuItem"; title = "Find Previous"; ObjectID = "162"; */ "162.title" = "Find Previous"; /* Class = "NSMenuItem"; title = "Edit"; ObjectID = "163"; */ "163.title" = "Edit"; /* Class = "NSMenuItem"; title = "Delete"; ObjectID = "164"; */ "164.title" = "Delete"; /* Class = "NSMenuItem"; title = "Find Next"; ObjectID = "167"; */ "167.title" = "Find Next"; /* Class = "NSMenuItem"; title = "Find"; ObjectID = "168"; */ "168.title" = "Find"; /* Class = "NSMenu"; title = "Edit"; ObjectID = "169"; */ "169.title" = "Edit"; /* Class = "NSMenuItem"; title = "Paste"; ObjectID = "171"; */ "171.title" = "Paste"; /* Class = "NSMenuItem"; title = "Select All"; ObjectID = "172"; */ "172.title" = "Select All"; /* Class = "NSMenuItem"; title = "Redo"; ObjectID = "173"; */ "173.title" = "Redo"; /* Class = "NSMenuItem"; title = "Spelling"; ObjectID = "184"; */ "184.title" = "Spelling"; /* Class = "NSMenu"; title = "Spelling"; ObjectID = "185"; */ "185.title" = "Spelling"; /* Class = "NSMenuItem"; title = "Spelling…"; ObjectID = "187"; */ "187.title" = "Spelling…"; /* Class = "NSMenuItem"; title = "Check Spelling"; ObjectID = "189"; */ "189.title" = "Check Spelling"; /* Class = "NSMenuItem"; title = "Check Spelling as You Type"; ObjectID = "191"; */ "191.title" = "Check Spelling as You Type"; /* Class = "NSMenuItem"; title = "Zoom"; ObjectID = "197"; */ "197.title" = "Zoom"; /* Class = "NSMenuItem"; title = "Paste and Match Style"; ObjectID = "204"; */ "204.title" = "Paste and Match Style"; /* Class = "NSMenuItem"; title = "Check for Updates…"; ObjectID = "207"; */ "207.title" = "Check for Updates…"; ================================================ FILE: TestApplication/he.lproj/MainMenu.strings ================================================ /* Class = "NSMenuItem"; title = "Bring All to Front"; ObjectID = "5"; */ "5.title" = "Bring All to Front"; /* Class = "NSMenuItem"; title = "Window"; ObjectID = "19"; */ "19.title" = "Window"; /* Class = "NSMenuItem"; title = "Minimize"; ObjectID = "23"; */ "23.title" = "Minimize"; /* Class = "NSMenu"; title = "Window"; ObjectID = "24"; */ "24.title" = "Window"; /* Class = "NSMenu"; title = "MainMenu"; ObjectID = "29"; */ "29.title" = "MainMenu"; /* Class = "NSMenuItem"; title = "Sparkle Test App"; ObjectID = "56"; */ "56.title" = "Sparkle Test App"; /* Class = "NSMenu"; title = "Sparkle Test App"; ObjectID = "57"; */ "57.title" = "Sparkle Test App"; /* Class = "NSMenuItem"; title = "About Sparkle Test App"; ObjectID = "58"; */ "58.title" = "About Sparkle Test App"; /* Class = "NSMenuItem"; title = "Open..."; ObjectID = "72"; */ "72.title" = "Open..."; /* Class = "NSMenuItem"; title = "Close"; ObjectID = "73"; */ "73.title" = "Close"; /* Class = "NSMenuItem"; title = "Save"; ObjectID = "75"; */ "75.title" = "Save"; /* Class = "NSMenuItem"; title = "Page Setup…"; ObjectID = "77"; */ "77.title" = "Page Setup…"; /* Class = "NSMenuItem"; title = "Print…"; ObjectID = "78"; */ "78.title" = "Print…"; /* Class = "NSMenuItem"; title = "Save As…"; ObjectID = "80"; */ "80.title" = "Save As…"; /* Class = "NSMenu"; title = "File"; ObjectID = "81"; */ "81.title" = "File"; /* Class = "NSMenuItem"; title = "New"; ObjectID = "82"; */ "82.title" = "New"; /* Class = "NSMenuItem"; title = "File"; ObjectID = "83"; */ "83.title" = "File"; /* Class = "NSMenuItem"; title = "Help"; ObjectID = "103"; */ "103.title" = "Help"; /* Class = "NSMenu"; title = "Help"; ObjectID = "106"; */ "106.title" = "Help"; /* Class = "NSMenuItem"; title = "Sparkle Test App Help"; ObjectID = "111"; */ "111.title" = "Sparkle Test App Help"; /* Class = "NSMenuItem"; title = "Revert"; ObjectID = "112"; */ "112.title" = "Revert"; /* Class = "NSMenuItem"; title = "Open Recent"; ObjectID = "124"; */ "124.title" = "Open Recent"; /* Class = "NSMenu"; title = "Open Recent"; ObjectID = "125"; */ "125.title" = "Open Recent"; /* Class = "NSMenuItem"; title = "Clear Menu"; ObjectID = "126"; */ "126.title" = "Clear Menu"; /* Class = "NSMenu"; title = "Services"; ObjectID = "130"; */ "130.title" = "Services"; /* Class = "NSMenuItem"; title = "Services"; ObjectID = "131"; */ "131.title" = "Services"; /* Class = "NSMenuItem"; title = "Hide Sparkle Test App"; ObjectID = "134"; */ "134.title" = "Hide Sparkle Test App"; /* Class = "NSMenuItem"; title = "Quit Sparkle Test App"; ObjectID = "136"; */ "136.title" = "Quit Sparkle Test App"; /* Class = "NSMenuItem"; title = "Hide Others"; ObjectID = "145"; */ "145.title" = "Hide Others"; /* Class = "NSMenuItem"; title = "Show All"; ObjectID = "150"; */ "150.title" = "Show All"; /* Class = "NSMenuItem"; title = "Find…"; ObjectID = "154"; */ "154.title" = "Find…"; /* Class = "NSMenuItem"; title = "Jump to Selection"; ObjectID = "155"; */ "155.title" = "Jump to Selection"; /* Class = "NSMenuItem"; title = "Copy"; ObjectID = "157"; */ "157.title" = "Copy"; /* Class = "NSMenuItem"; title = "Undo"; ObjectID = "158"; */ "158.title" = "Undo"; /* Class = "NSMenu"; title = "Find"; ObjectID = "159"; */ "159.title" = "Find"; /* Class = "NSMenuItem"; title = "Cut"; ObjectID = "160"; */ "160.title" = "Cut"; /* Class = "NSMenuItem"; title = "Use Selection for Find"; ObjectID = "161"; */ "161.title" = "Use Selection for Find"; /* Class = "NSMenuItem"; title = "Find Previous"; ObjectID = "162"; */ "162.title" = "Find Previous"; /* Class = "NSMenuItem"; title = "Edit"; ObjectID = "163"; */ "163.title" = "Edit"; /* Class = "NSMenuItem"; title = "Delete"; ObjectID = "164"; */ "164.title" = "Delete"; /* Class = "NSMenuItem"; title = "Find Next"; ObjectID = "167"; */ "167.title" = "Find Next"; /* Class = "NSMenuItem"; title = "Find"; ObjectID = "168"; */ "168.title" = "Find"; /* Class = "NSMenu"; title = "Edit"; ObjectID = "169"; */ "169.title" = "Edit"; /* Class = "NSMenuItem"; title = "Paste"; ObjectID = "171"; */ "171.title" = "Paste"; /* Class = "NSMenuItem"; title = "Select All"; ObjectID = "172"; */ "172.title" = "Select All"; /* Class = "NSMenuItem"; title = "Redo"; ObjectID = "173"; */ "173.title" = "Redo"; /* Class = "NSMenuItem"; title = "Spelling"; ObjectID = "184"; */ "184.title" = "Spelling"; /* Class = "NSMenu"; title = "Spelling"; ObjectID = "185"; */ "185.title" = "Spelling"; /* Class = "NSMenuItem"; title = "Spelling…"; ObjectID = "187"; */ "187.title" = "Spelling…"; /* Class = "NSMenuItem"; title = "Check Spelling"; ObjectID = "189"; */ "189.title" = "Check Spelling"; /* Class = "NSMenuItem"; title = "Check Spelling as You Type"; ObjectID = "191"; */ "191.title" = "Check Spelling as You Type"; /* Class = "NSMenuItem"; title = "Zoom"; ObjectID = "197"; */ "197.title" = "Zoom"; /* Class = "NSMenuItem"; title = "Paste and Match Style"; ObjectID = "204"; */ "204.title" = "Paste and Match Style"; /* Class = "NSMenuItem"; title = "Check for Updates…"; ObjectID = "207"; */ "207.title" = "Check for Updates…"; ================================================ FILE: TestApplication/hr.lproj/MainMenu.strings ================================================ /* Class = "NSMenuItem"; title = "Bring All to Front"; ObjectID = "5"; */ "5.title" = "Bring All to Front"; /* Class = "NSMenuItem"; title = "Window"; ObjectID = "19"; */ "19.title" = "Window"; /* Class = "NSMenuItem"; title = "Minimize"; ObjectID = "23"; */ "23.title" = "Minimize"; /* Class = "NSMenu"; title = "Window"; ObjectID = "24"; */ "24.title" = "Window"; /* Class = "NSMenu"; title = "MainMenu"; ObjectID = "29"; */ "29.title" = "MainMenu"; /* Class = "NSMenuItem"; title = "Sparkle Test App"; ObjectID = "56"; */ "56.title" = "Sparkle Test App"; /* Class = "NSMenu"; title = "Sparkle Test App"; ObjectID = "57"; */ "57.title" = "Sparkle Test App"; /* Class = "NSMenuItem"; title = "About Sparkle Test App"; ObjectID = "58"; */ "58.title" = "About Sparkle Test App"; /* Class = "NSMenuItem"; title = "Open..."; ObjectID = "72"; */ "72.title" = "Open..."; /* Class = "NSMenuItem"; title = "Close"; ObjectID = "73"; */ "73.title" = "Close"; /* Class = "NSMenuItem"; title = "Save"; ObjectID = "75"; */ "75.title" = "Save"; /* Class = "NSMenuItem"; title = "Page Setup…"; ObjectID = "77"; */ "77.title" = "Page Setup…"; /* Class = "NSMenuItem"; title = "Print…"; ObjectID = "78"; */ "78.title" = "Print…"; /* Class = "NSMenuItem"; title = "Save As…"; ObjectID = "80"; */ "80.title" = "Save As…"; /* Class = "NSMenu"; title = "File"; ObjectID = "81"; */ "81.title" = "File"; /* Class = "NSMenuItem"; title = "New"; ObjectID = "82"; */ "82.title" = "New"; /* Class = "NSMenuItem"; title = "File"; ObjectID = "83"; */ "83.title" = "File"; /* Class = "NSMenuItem"; title = "Help"; ObjectID = "103"; */ "103.title" = "Help"; /* Class = "NSMenu"; title = "Help"; ObjectID = "106"; */ "106.title" = "Help"; /* Class = "NSMenuItem"; title = "Sparkle Test App Help"; ObjectID = "111"; */ "111.title" = "Sparkle Test App Help"; /* Class = "NSMenuItem"; title = "Revert"; ObjectID = "112"; */ "112.title" = "Revert"; /* Class = "NSMenuItem"; title = "Open Recent"; ObjectID = "124"; */ "124.title" = "Open Recent"; /* Class = "NSMenu"; title = "Open Recent"; ObjectID = "125"; */ "125.title" = "Open Recent"; /* Class = "NSMenuItem"; title = "Clear Menu"; ObjectID = "126"; */ "126.title" = "Clear Menu"; /* Class = "NSMenu"; title = "Services"; ObjectID = "130"; */ "130.title" = "Services"; /* Class = "NSMenuItem"; title = "Services"; ObjectID = "131"; */ "131.title" = "Services"; /* Class = "NSMenuItem"; title = "Hide Sparkle Test App"; ObjectID = "134"; */ "134.title" = "Hide Sparkle Test App"; /* Class = "NSMenuItem"; title = "Quit Sparkle Test App"; ObjectID = "136"; */ "136.title" = "Quit Sparkle Test App"; /* Class = "NSMenuItem"; title = "Hide Others"; ObjectID = "145"; */ "145.title" = "Hide Others"; /* Class = "NSMenuItem"; title = "Show All"; ObjectID = "150"; */ "150.title" = "Show All"; /* Class = "NSMenuItem"; title = "Find…"; ObjectID = "154"; */ "154.title" = "Find…"; /* Class = "NSMenuItem"; title = "Jump to Selection"; ObjectID = "155"; */ "155.title" = "Jump to Selection"; /* Class = "NSMenuItem"; title = "Copy"; ObjectID = "157"; */ "157.title" = "Copy"; /* Class = "NSMenuItem"; title = "Undo"; ObjectID = "158"; */ "158.title" = "Undo"; /* Class = "NSMenu"; title = "Find"; ObjectID = "159"; */ "159.title" = "Find"; /* Class = "NSMenuItem"; title = "Cut"; ObjectID = "160"; */ "160.title" = "Cut"; /* Class = "NSMenuItem"; title = "Use Selection for Find"; ObjectID = "161"; */ "161.title" = "Use Selection for Find"; /* Class = "NSMenuItem"; title = "Find Previous"; ObjectID = "162"; */ "162.title" = "Find Previous"; /* Class = "NSMenuItem"; title = "Edit"; ObjectID = "163"; */ "163.title" = "Edit"; /* Class = "NSMenuItem"; title = "Delete"; ObjectID = "164"; */ "164.title" = "Delete"; /* Class = "NSMenuItem"; title = "Find Next"; ObjectID = "167"; */ "167.title" = "Find Next"; /* Class = "NSMenuItem"; title = "Find"; ObjectID = "168"; */ "168.title" = "Find"; /* Class = "NSMenu"; title = "Edit"; ObjectID = "169"; */ "169.title" = "Edit"; /* Class = "NSMenuItem"; title = "Paste"; ObjectID = "171"; */ "171.title" = "Paste"; /* Class = "NSMenuItem"; title = "Select All"; ObjectID = "172"; */ "172.title" = "Select All"; /* Class = "NSMenuItem"; title = "Redo"; ObjectID = "173"; */ "173.title" = "Redo"; /* Class = "NSMenuItem"; title = "Spelling"; ObjectID = "184"; */ "184.title" = "Spelling"; /* Class = "NSMenu"; title = "Spelling"; ObjectID = "185"; */ "185.title" = "Spelling"; /* Class = "NSMenuItem"; title = "Spelling…"; ObjectID = "187"; */ "187.title" = "Spelling…"; /* Class = "NSMenuItem"; title = "Check Spelling"; ObjectID = "189"; */ "189.title" = "Check Spelling"; /* Class = "NSMenuItem"; title = "Check Spelling as You Type"; ObjectID = "191"; */ "191.title" = "Check Spelling as You Type"; /* Class = "NSMenuItem"; title = "Zoom"; ObjectID = "197"; */ "197.title" = "Zoom"; /* Class = "NSMenuItem"; title = "Paste and Match Style"; ObjectID = "204"; */ "204.title" = "Paste and Match Style"; /* Class = "NSMenuItem"; title = "Check for Updates…"; ObjectID = "207"; */ "207.title" = "Check for Updates…"; ================================================ FILE: TestApplication/hu.lproj/MainMenu.strings ================================================ /* Class = "NSMenuItem"; title = "Bring All to Front"; ObjectID = "5"; */ "5.title" = "Bring All to Front"; /* Class = "NSMenuItem"; title = "Window"; ObjectID = "19"; */ "19.title" = "Window"; /* Class = "NSMenuItem"; title = "Minimize"; ObjectID = "23"; */ "23.title" = "Minimize"; /* Class = "NSMenu"; title = "Window"; ObjectID = "24"; */ "24.title" = "Window"; /* Class = "NSMenu"; title = "MainMenu"; ObjectID = "29"; */ "29.title" = "MainMenu"; /* Class = "NSMenuItem"; title = "Sparkle Test App"; ObjectID = "56"; */ "56.title" = "Sparkle Test App"; /* Class = "NSMenu"; title = "Sparkle Test App"; ObjectID = "57"; */ "57.title" = "Sparkle Test App"; /* Class = "NSMenuItem"; title = "About Sparkle Test App"; ObjectID = "58"; */ "58.title" = "About Sparkle Test App"; /* Class = "NSMenuItem"; title = "Open..."; ObjectID = "72"; */ "72.title" = "Open..."; /* Class = "NSMenuItem"; title = "Close"; ObjectID = "73"; */ "73.title" = "Close"; /* Class = "NSMenuItem"; title = "Save"; ObjectID = "75"; */ "75.title" = "Save"; /* Class = "NSMenuItem"; title = "Page Setup…"; ObjectID = "77"; */ "77.title" = "Page Setup…"; /* Class = "NSMenuItem"; title = "Print…"; ObjectID = "78"; */ "78.title" = "Print…"; /* Class = "NSMenuItem"; title = "Save As…"; ObjectID = "80"; */ "80.title" = "Save As…"; /* Class = "NSMenu"; title = "File"; ObjectID = "81"; */ "81.title" = "File"; /* Class = "NSMenuItem"; title = "New"; ObjectID = "82"; */ "82.title" = "New"; /* Class = "NSMenuItem"; title = "File"; ObjectID = "83"; */ "83.title" = "File"; /* Class = "NSMenuItem"; title = "Help"; ObjectID = "103"; */ "103.title" = "Help"; /* Class = "NSMenu"; title = "Help"; ObjectID = "106"; */ "106.title" = "Help"; /* Class = "NSMenuItem"; title = "Sparkle Test App Help"; ObjectID = "111"; */ "111.title" = "Sparkle Test App Help"; /* Class = "NSMenuItem"; title = "Revert"; ObjectID = "112"; */ "112.title" = "Revert"; /* Class = "NSMenuItem"; title = "Open Recent"; ObjectID = "124"; */ "124.title" = "Open Recent"; /* Class = "NSMenu"; title = "Open Recent"; ObjectID = "125"; */ "125.title" = "Open Recent"; /* Class = "NSMenuItem"; title = "Clear Menu"; ObjectID = "126"; */ "126.title" = "Clear Menu"; /* Class = "NSMenu"; title = "Services"; ObjectID = "130"; */ "130.title" = "Services"; /* Class = "NSMenuItem"; title = "Services"; ObjectID = "131"; */ "131.title" = "Services"; /* Class = "NSMenuItem"; title = "Hide Sparkle Test App"; ObjectID = "134"; */ "134.title" = "Hide Sparkle Test App"; /* Class = "NSMenuItem"; title = "Quit Sparkle Test App"; ObjectID = "136"; */ "136.title" = "Quit Sparkle Test App"; /* Class = "NSMenuItem"; title = "Hide Others"; ObjectID = "145"; */ "145.title" = "Hide Others"; /* Class = "NSMenuItem"; title = "Show All"; ObjectID = "150"; */ "150.title" = "Show All"; /* Class = "NSMenuItem"; title = "Find…"; ObjectID = "154"; */ "154.title" = "Find…"; /* Class = "NSMenuItem"; title = "Jump to Selection"; ObjectID = "155"; */ "155.title" = "Jump to Selection"; /* Class = "NSMenuItem"; title = "Copy"; ObjectID = "157"; */ "157.title" = "Copy"; /* Class = "NSMenuItem"; title = "Undo"; ObjectID = "158"; */ "158.title" = "Undo"; /* Class = "NSMenu"; title = "Find"; ObjectID = "159"; */ "159.title" = "Find"; /* Class = "NSMenuItem"; title = "Cut"; ObjectID = "160"; */ "160.title" = "Cut"; /* Class = "NSMenuItem"; title = "Use Selection for Find"; ObjectID = "161"; */ "161.title" = "Use Selection for Find"; /* Class = "NSMenuItem"; title = "Find Previous"; ObjectID = "162"; */ "162.title" = "Find Previous"; /* Class = "NSMenuItem"; title = "Edit"; ObjectID = "163"; */ "163.title" = "Edit"; /* Class = "NSMenuItem"; title = "Delete"; ObjectID = "164"; */ "164.title" = "Delete"; /* Class = "NSMenuItem"; title = "Find Next"; ObjectID = "167"; */ "167.title" = "Find Next"; /* Class = "NSMenuItem"; title = "Find"; ObjectID = "168"; */ "168.title" = "Find"; /* Class = "NSMenu"; title = "Edit"; ObjectID = "169"; */ "169.title" = "Edit"; /* Class = "NSMenuItem"; title = "Paste"; ObjectID = "171"; */ "171.title" = "Paste"; /* Class = "NSMenuItem"; title = "Select All"; ObjectID = "172"; */ "172.title" = "Select All"; /* Class = "NSMenuItem"; title = "Redo"; ObjectID = "173"; */ "173.title" = "Redo"; /* Class = "NSMenuItem"; title = "Spelling"; ObjectID = "184"; */ "184.title" = "Spelling"; /* Class = "NSMenu"; title = "Spelling"; ObjectID = "185"; */ "185.title" = "Spelling"; /* Class = "NSMenuItem"; title = "Spelling…"; ObjectID = "187"; */ "187.title" = "Spelling…"; /* Class = "NSMenuItem"; title = "Check Spelling"; ObjectID = "189"; */ "189.title" = "Check Spelling"; /* Class = "NSMenuItem"; title = "Check Spelling as You Type"; ObjectID = "191"; */ "191.title" = "Check Spelling as You Type"; /* Class = "NSMenuItem"; title = "Zoom"; ObjectID = "197"; */ "197.title" = "Zoom"; /* Class = "NSMenuItem"; title = "Paste and Match Style"; ObjectID = "204"; */ "204.title" = "Paste and Match Style"; /* Class = "NSMenuItem"; title = "Check for Updates…"; ObjectID = "207"; */ "207.title" = "Check for Updates…"; ================================================ FILE: TestApplication/is.lproj/MainMenu.strings ================================================ /* Class = "NSMenuItem"; title = "Bring All to Front"; ObjectID = "5"; */ "5.title" = "Bring All to Front"; /* Class = "NSMenuItem"; title = "Window"; ObjectID = "19"; */ "19.title" = "Window"; /* Class = "NSMenuItem"; title = "Minimize"; ObjectID = "23"; */ "23.title" = "Minimize"; /* Class = "NSMenu"; title = "Window"; ObjectID = "24"; */ "24.title" = "Window"; /* Class = "NSMenu"; title = "MainMenu"; ObjectID = "29"; */ "29.title" = "MainMenu"; /* Class = "NSMenuItem"; title = "Sparkle Test App"; ObjectID = "56"; */ "56.title" = "Sparkle Test App"; /* Class = "NSMenu"; title = "Sparkle Test App"; ObjectID = "57"; */ "57.title" = "Sparkle Test App"; /* Class = "NSMenuItem"; title = "About Sparkle Test App"; ObjectID = "58"; */ "58.title" = "About Sparkle Test App"; /* Class = "NSMenuItem"; title = "Open..."; ObjectID = "72"; */ "72.title" = "Open..."; /* Class = "NSMenuItem"; title = "Close"; ObjectID = "73"; */ "73.title" = "Close"; /* Class = "NSMenuItem"; title = "Save"; ObjectID = "75"; */ "75.title" = "Save"; /* Class = "NSMenuItem"; title = "Page Setup…"; ObjectID = "77"; */ "77.title" = "Page Setup…"; /* Class = "NSMenuItem"; title = "Print…"; ObjectID = "78"; */ "78.title" = "Print…"; /* Class = "NSMenuItem"; title = "Save As…"; ObjectID = "80"; */ "80.title" = "Save As…"; /* Class = "NSMenu"; title = "File"; ObjectID = "81"; */ "81.title" = "File"; /* Class = "NSMenuItem"; title = "New"; ObjectID = "82"; */ "82.title" = "New"; /* Class = "NSMenuItem"; title = "File"; ObjectID = "83"; */ "83.title" = "File"; /* Class = "NSMenuItem"; title = "Help"; ObjectID = "103"; */ "103.title" = "Help"; /* Class = "NSMenu"; title = "Help"; ObjectID = "106"; */ "106.title" = "Help"; /* Class = "NSMenuItem"; title = "Sparkle Test App Help"; ObjectID = "111"; */ "111.title" = "Sparkle Test App Help"; /* Class = "NSMenuItem"; title = "Revert"; ObjectID = "112"; */ "112.title" = "Revert"; /* Class = "NSMenuItem"; title = "Open Recent"; ObjectID = "124"; */ "124.title" = "Open Recent"; /* Class = "NSMenu"; title = "Open Recent"; ObjectID = "125"; */ "125.title" = "Open Recent"; /* Class = "NSMenuItem"; title = "Clear Menu"; ObjectID = "126"; */ "126.title" = "Clear Menu"; /* Class = "NSMenu"; title = "Services"; ObjectID = "130"; */ "130.title" = "Services"; /* Class = "NSMenuItem"; title = "Services"; ObjectID = "131"; */ "131.title" = "Services"; /* Class = "NSMenuItem"; title = "Hide Sparkle Test App"; ObjectID = "134"; */ "134.title" = "Hide Sparkle Test App"; /* Class = "NSMenuItem"; title = "Quit Sparkle Test App"; ObjectID = "136"; */ "136.title" = "Quit Sparkle Test App"; /* Class = "NSMenuItem"; title = "Hide Others"; ObjectID = "145"; */ "145.title" = "Hide Others"; /* Class = "NSMenuItem"; title = "Show All"; ObjectID = "150"; */ "150.title" = "Show All"; /* Class = "NSMenuItem"; title = "Find…"; ObjectID = "154"; */ "154.title" = "Find…"; /* Class = "NSMenuItem"; title = "Jump to Selection"; ObjectID = "155"; */ "155.title" = "Jump to Selection"; /* Class = "NSMenuItem"; title = "Copy"; ObjectID = "157"; */ "157.title" = "Copy"; /* Class = "NSMenuItem"; title = "Undo"; ObjectID = "158"; */ "158.title" = "Undo"; /* Class = "NSMenu"; title = "Find"; ObjectID = "159"; */ "159.title" = "Find"; /* Class = "NSMenuItem"; title = "Cut"; ObjectID = "160"; */ "160.title" = "Cut"; /* Class = "NSMenuItem"; title = "Use Selection for Find"; ObjectID = "161"; */ "161.title" = "Use Selection for Find"; /* Class = "NSMenuItem"; title = "Find Previous"; ObjectID = "162"; */ "162.title" = "Find Previous"; /* Class = "NSMenuItem"; title = "Edit"; ObjectID = "163"; */ "163.title" = "Edit"; /* Class = "NSMenuItem"; title = "Delete"; ObjectID = "164"; */ "164.title" = "Delete"; /* Class = "NSMenuItem"; title = "Find Next"; ObjectID = "167"; */ "167.title" = "Find Next"; /* Class = "NSMenuItem"; title = "Find"; ObjectID = "168"; */ "168.title" = "Find"; /* Class = "NSMenu"; title = "Edit"; ObjectID = "169"; */ "169.title" = "Edit"; /* Class = "NSMenuItem"; title = "Paste"; ObjectID = "171"; */ "171.title" = "Paste"; /* Class = "NSMenuItem"; title = "Select All"; ObjectID = "172"; */ "172.title" = "Select All"; /* Class = "NSMenuItem"; title = "Redo"; ObjectID = "173"; */ "173.title" = "Redo"; /* Class = "NSMenuItem"; title = "Spelling"; ObjectID = "184"; */ "184.title" = "Spelling"; /* Class = "NSMenu"; title = "Spelling"; ObjectID = "185"; */ "185.title" = "Spelling"; /* Class = "NSMenuItem"; title = "Spelling…"; ObjectID = "187"; */ "187.title" = "Spelling…"; /* Class = "NSMenuItem"; title = "Check Spelling"; ObjectID = "189"; */ "189.title" = "Check Spelling"; /* Class = "NSMenuItem"; title = "Check Spelling as You Type"; ObjectID = "191"; */ "191.title" = "Check Spelling as You Type"; /* Class = "NSMenuItem"; title = "Zoom"; ObjectID = "197"; */ "197.title" = "Zoom"; /* Class = "NSMenuItem"; title = "Paste and Match Style"; ObjectID = "204"; */ "204.title" = "Paste and Match Style"; /* Class = "NSMenuItem"; title = "Check for Updates…"; ObjectID = "207"; */ "207.title" = "Check for Updates…"; ================================================ FILE: TestApplication/it.lproj/MainMenu.strings ================================================ /* Class = "NSMenuItem"; title = "Bring All to Front"; ObjectID = "5"; */ "5.title" = "Bring All to Front"; /* Class = "NSMenuItem"; title = "Window"; ObjectID = "19"; */ "19.title" = "Window"; /* Class = "NSMenuItem"; title = "Minimize"; ObjectID = "23"; */ "23.title" = "Minimize"; /* Class = "NSMenu"; title = "Window"; ObjectID = "24"; */ "24.title" = "Window"; /* Class = "NSMenu"; title = "MainMenu"; ObjectID = "29"; */ "29.title" = "MainMenu"; /* Class = "NSMenuItem"; title = "Sparkle Test App"; ObjectID = "56"; */ "56.title" = "Sparkle Test App"; /* Class = "NSMenu"; title = "Sparkle Test App"; ObjectID = "57"; */ "57.title" = "Sparkle Test App"; /* Class = "NSMenuItem"; title = "About Sparkle Test App"; ObjectID = "58"; */ "58.title" = "About Sparkle Test App"; /* Class = "NSMenuItem"; title = "Open..."; ObjectID = "72"; */ "72.title" = "Open..."; /* Class = "NSMenuItem"; title = "Close"; ObjectID = "73"; */ "73.title" = "Close"; /* Class = "NSMenuItem"; title = "Save"; ObjectID = "75"; */ "75.title" = "Save"; /* Class = "NSMenuItem"; title = "Page Setup…"; ObjectID = "77"; */ "77.title" = "Page Setup…"; /* Class = "NSMenuItem"; title = "Print…"; ObjectID = "78"; */ "78.title" = "Print…"; /* Class = "NSMenuItem"; title = "Save As…"; ObjectID = "80"; */ "80.title" = "Save As…"; /* Class = "NSMenu"; title = "File"; ObjectID = "81"; */ "81.title" = "File"; /* Class = "NSMenuItem"; title = "New"; ObjectID = "82"; */ "82.title" = "New"; /* Class = "NSMenuItem"; title = "File"; ObjectID = "83"; */ "83.title" = "File"; /* Class = "NSMenuItem"; title = "Help"; ObjectID = "103"; */ "103.title" = "Help"; /* Class = "NSMenu"; title = "Help"; ObjectID = "106"; */ "106.title" = "Help"; /* Class = "NSMenuItem"; title = "Sparkle Test App Help"; ObjectID = "111"; */ "111.title" = "Sparkle Test App Help"; /* Class = "NSMenuItem"; title = "Revert"; ObjectID = "112"; */ "112.title" = "Revert"; /* Class = "NSMenuItem"; title = "Open Recent"; ObjectID = "124"; */ "124.title" = "Open Recent"; /* Class = "NSMenu"; title = "Open Recent"; ObjectID = "125"; */ "125.title" = "Open Recent"; /* Class = "NSMenuItem"; title = "Clear Menu"; ObjectID = "126"; */ "126.title" = "Clear Menu"; /* Class = "NSMenu"; title = "Services"; ObjectID = "130"; */ "130.title" = "Services"; /* Class = "NSMenuItem"; title = "Services"; ObjectID = "131"; */ "131.title" = "Services"; /* Class = "NSMenuItem"; title = "Hide Sparkle Test App"; ObjectID = "134"; */ "134.title" = "Hide Sparkle Test App"; /* Class = "NSMenuItem"; title = "Quit Sparkle Test App"; ObjectID = "136"; */ "136.title" = "Quit Sparkle Test App"; /* Class = "NSMenuItem"; title = "Hide Others"; ObjectID = "145"; */ "145.title" = "Hide Others"; /* Class = "NSMenuItem"; title = "Show All"; ObjectID = "150"; */ "150.title" = "Show All"; /* Class = "NSMenuItem"; title = "Find…"; ObjectID = "154"; */ "154.title" = "Find…"; /* Class = "NSMenuItem"; title = "Jump to Selection"; ObjectID = "155"; */ "155.title" = "Jump to Selection"; /* Class = "NSMenuItem"; title = "Copy"; ObjectID = "157"; */ "157.title" = "Copy"; /* Class = "NSMenuItem"; title = "Undo"; ObjectID = "158"; */ "158.title" = "Undo"; /* Class = "NSMenu"; title = "Find"; ObjectID = "159"; */ "159.title" = "Find"; /* Class = "NSMenuItem"; title = "Cut"; ObjectID = "160"; */ "160.title" = "Cut"; /* Class = "NSMenuItem"; title = "Use Selection for Find"; ObjectID = "161"; */ "161.title" = "Use Selection for Find"; /* Class = "NSMenuItem"; title = "Find Previous"; ObjectID = "162"; */ "162.title" = "Find Previous"; /* Class = "NSMenuItem"; title = "Edit"; ObjectID = "163"; */ "163.title" = "Edit"; /* Class = "NSMenuItem"; title = "Delete"; ObjectID = "164"; */ "164.title" = "Delete"; /* Class = "NSMenuItem"; title = "Find Next"; ObjectID = "167"; */ "167.title" = "Find Next"; /* Class = "NSMenuItem"; title = "Find"; ObjectID = "168"; */ "168.title" = "Find"; /* Class = "NSMenu"; title = "Edit"; ObjectID = "169"; */ "169.title" = "Edit"; /* Class = "NSMenuItem"; title = "Paste"; ObjectID = "171"; */ "171.title" = "Paste"; /* Class = "NSMenuItem"; title = "Select All"; ObjectID = "172"; */ "172.title" = "Select All"; /* Class = "NSMenuItem"; title = "Redo"; ObjectID = "173"; */ "173.title" = "Redo"; /* Class = "NSMenuItem"; title = "Spelling"; ObjectID = "184"; */ "184.title" = "Spelling"; /* Class = "NSMenu"; title = "Spelling"; ObjectID = "185"; */ "185.title" = "Spelling"; /* Class = "NSMenuItem"; title = "Spelling…"; ObjectID = "187"; */ "187.title" = "Spelling…"; /* Class = "NSMenuItem"; title = "Check Spelling"; ObjectID = "189"; */ "189.title" = "Check Spelling"; /* Class = "NSMenuItem"; title = "Check Spelling as You Type"; ObjectID = "191"; */ "191.title" = "Check Spelling as You Type"; /* Class = "NSMenuItem"; title = "Zoom"; ObjectID = "197"; */ "197.title" = "Zoom"; /* Class = "NSMenuItem"; title = "Paste and Match Style"; ObjectID = "204"; */ "204.title" = "Paste and Match Style"; /* Class = "NSMenuItem"; title = "Check for Updates…"; ObjectID = "207"; */ "207.title" = "Check for Updates…"; ================================================ FILE: TestApplication/ja.lproj/MainMenu.strings ================================================ /* Class = "NSMenuItem"; title = "Bring All to Front"; ObjectID = "5"; */ "5.title" = "Bring All to Front"; /* Class = "NSMenuItem"; title = "Window"; ObjectID = "19"; */ "19.title" = "Window"; /* Class = "NSMenuItem"; title = "Minimize"; ObjectID = "23"; */ "23.title" = "Minimize"; /* Class = "NSMenu"; title = "Window"; ObjectID = "24"; */ "24.title" = "Window"; /* Class = "NSMenu"; title = "MainMenu"; ObjectID = "29"; */ "29.title" = "MainMenu"; /* Class = "NSMenuItem"; title = "Sparkle Test App"; ObjectID = "56"; */ "56.title" = "Sparkle Test App"; /* Class = "NSMenu"; title = "Sparkle Test App"; ObjectID = "57"; */ "57.title" = "Sparkle Test App"; /* Class = "NSMenuItem"; title = "About Sparkle Test App"; ObjectID = "58"; */ "58.title" = "About Sparkle Test App"; /* Class = "NSMenuItem"; title = "Open..."; ObjectID = "72"; */ "72.title" = "Open..."; /* Class = "NSMenuItem"; title = "Close"; ObjectID = "73"; */ "73.title" = "Close"; /* Class = "NSMenuItem"; title = "Save"; ObjectID = "75"; */ "75.title" = "Save"; /* Class = "NSMenuItem"; title = "Page Setup…"; ObjectID = "77"; */ "77.title" = "Page Setup…"; /* Class = "NSMenuItem"; title = "Print…"; ObjectID = "78"; */ "78.title" = "Print…"; /* Class = "NSMenuItem"; title = "Save As…"; ObjectID = "80"; */ "80.title" = "Save As…"; /* Class = "NSMenu"; title = "File"; ObjectID = "81"; */ "81.title" = "File"; /* Class = "NSMenuItem"; title = "New"; ObjectID = "82"; */ "82.title" = "New"; /* Class = "NSMenuItem"; title = "File"; ObjectID = "83"; */ "83.title" = "File"; /* Class = "NSMenuItem"; title = "Help"; ObjectID = "103"; */ "103.title" = "Help"; /* Class = "NSMenu"; title = "Help"; ObjectID = "106"; */ "106.title" = "Help"; /* Class = "NSMenuItem"; title = "Sparkle Test App Help"; ObjectID = "111"; */ "111.title" = "Sparkle Test App Help"; /* Class = "NSMenuItem"; title = "Revert"; ObjectID = "112"; */ "112.title" = "Revert"; /* Class = "NSMenuItem"; title = "Open Recent"; ObjectID = "124"; */ "124.title" = "Open Recent"; /* Class = "NSMenu"; title = "Open Recent"; ObjectID = "125"; */ "125.title" = "Open Recent"; /* Class = "NSMenuItem"; title = "Clear Menu"; ObjectID = "126"; */ "126.title" = "Clear Menu"; /* Class = "NSMenu"; title = "Services"; ObjectID = "130"; */ "130.title" = "Services"; /* Class = "NSMenuItem"; title = "Services"; ObjectID = "131"; */ "131.title" = "Services"; /* Class = "NSMenuItem"; title = "Hide Sparkle Test App"; ObjectID = "134"; */ "134.title" = "Hide Sparkle Test App"; /* Class = "NSMenuItem"; title = "Quit Sparkle Test App"; ObjectID = "136"; */ "136.title" = "Quit Sparkle Test App"; /* Class = "NSMenuItem"; title = "Hide Others"; ObjectID = "145"; */ "145.title" = "Hide Others"; /* Class = "NSMenuItem"; title = "Show All"; ObjectID = "150"; */ "150.title" = "Show All"; /* Class = "NSMenuItem"; title = "Find…"; ObjectID = "154"; */ "154.title" = "Find…"; /* Class = "NSMenuItem"; title = "Jump to Selection"; ObjectID = "155"; */ "155.title" = "Jump to Selection"; /* Class = "NSMenuItem"; title = "Copy"; ObjectID = "157"; */ "157.title" = "Copy"; /* Class = "NSMenuItem"; title = "Undo"; ObjectID = "158"; */ "158.title" = "Undo"; /* Class = "NSMenu"; title = "Find"; ObjectID = "159"; */ "159.title" = "Find"; /* Class = "NSMenuItem"; title = "Cut"; ObjectID = "160"; */ "160.title" = "Cut"; /* Class = "NSMenuItem"; title = "Use Selection for Find"; ObjectID = "161"; */ "161.title" = "Use Selection for Find"; /* Class = "NSMenuItem"; title = "Find Previous"; ObjectID = "162"; */ "162.title" = "Find Previous"; /* Class = "NSMenuItem"; title = "Edit"; ObjectID = "163"; */ "163.title" = "Edit"; /* Class = "NSMenuItem"; title = "Delete"; ObjectID = "164"; */ "164.title" = "Delete"; /* Class = "NSMenuItem"; title = "Find Next"; ObjectID = "167"; */ "167.title" = "Find Next"; /* Class = "NSMenuItem"; title = "Find"; ObjectID = "168"; */ "168.title" = "Find"; /* Class = "NSMenu"; title = "Edit"; ObjectID = "169"; */ "169.title" = "Edit"; /* Class = "NSMenuItem"; title = "Paste"; ObjectID = "171"; */ "171.title" = "Paste"; /* Class = "NSMenuItem"; title = "Select All"; ObjectID = "172"; */ "172.title" = "Select All"; /* Class = "NSMenuItem"; title = "Redo"; ObjectID = "173"; */ "173.title" = "Redo"; /* Class = "NSMenuItem"; title = "Spelling"; ObjectID = "184"; */ "184.title" = "Spelling"; /* Class = "NSMenu"; title = "Spelling"; ObjectID = "185"; */ "185.title" = "Spelling"; /* Class = "NSMenuItem"; title = "Spelling…"; ObjectID = "187"; */ "187.title" = "Spelling…"; /* Class = "NSMenuItem"; title = "Check Spelling"; ObjectID = "189"; */ "189.title" = "Check Spelling"; /* Class = "NSMenuItem"; title = "Check Spelling as You Type"; ObjectID = "191"; */ "191.title" = "Check Spelling as You Type"; /* Class = "NSMenuItem"; title = "Zoom"; ObjectID = "197"; */ "197.title" = "Zoom"; /* Class = "NSMenuItem"; title = "Paste and Match Style"; ObjectID = "204"; */ "204.title" = "Paste and Match Style"; /* Class = "NSMenuItem"; title = "Check for Updates…"; ObjectID = "207"; */ "207.title" = "Check for Updates…"; ================================================ FILE: TestApplication/ko.lproj/MainMenu.strings ================================================ /* Class = "NSMenuItem"; title = "Bring All to Front"; ObjectID = "5"; */ "5.title" = "Bring All to Front"; /* Class = "NSMenuItem"; title = "Window"; ObjectID = "19"; */ "19.title" = "Window"; /* Class = "NSMenuItem"; title = "Minimize"; ObjectID = "23"; */ "23.title" = "Minimize"; /* Class = "NSMenu"; title = "Window"; ObjectID = "24"; */ "24.title" = "Window"; /* Class = "NSMenu"; title = "MainMenu"; ObjectID = "29"; */ "29.title" = "MainMenu"; /* Class = "NSMenuItem"; title = "Sparkle Test App"; ObjectID = "56"; */ "56.title" = "Sparkle Test App"; /* Class = "NSMenu"; title = "Sparkle Test App"; ObjectID = "57"; */ "57.title" = "Sparkle Test App"; /* Class = "NSMenuItem"; title = "About Sparkle Test App"; ObjectID = "58"; */ "58.title" = "About Sparkle Test App"; /* Class = "NSMenuItem"; title = "Open..."; ObjectID = "72"; */ "72.title" = "Open..."; /* Class = "NSMenuItem"; title = "Close"; ObjectID = "73"; */ "73.title" = "Close"; /* Class = "NSMenuItem"; title = "Save"; ObjectID = "75"; */ "75.title" = "Save"; /* Class = "NSMenuItem"; title = "Page Setup…"; ObjectID = "77"; */ "77.title" = "Page Setup…"; /* Class = "NSMenuItem"; title = "Print…"; ObjectID = "78"; */ "78.title" = "Print…"; /* Class = "NSMenuItem"; title = "Save As…"; ObjectID = "80"; */ "80.title" = "Save As…"; /* Class = "NSMenu"; title = "File"; ObjectID = "81"; */ "81.title" = "File"; /* Class = "NSMenuItem"; title = "New"; ObjectID = "82"; */ "82.title" = "New"; /* Class = "NSMenuItem"; title = "File"; ObjectID = "83"; */ "83.title" = "File"; /* Class = "NSMenuItem"; title = "Help"; ObjectID = "103"; */ "103.title" = "Help"; /* Class = "NSMenu"; title = "Help"; ObjectID = "106"; */ "106.title" = "Help"; /* Class = "NSMenuItem"; title = "Sparkle Test App Help"; ObjectID = "111"; */ "111.title" = "Sparkle Test App Help"; /* Class = "NSMenuItem"; title = "Revert"; ObjectID = "112"; */ "112.title" = "Revert"; /* Class = "NSMenuItem"; title = "Open Recent"; ObjectID = "124"; */ "124.title" = "Open Recent"; /* Class = "NSMenu"; title = "Open Recent"; ObjectID = "125"; */ "125.title" = "Open Recent"; /* Class = "NSMenuItem"; title = "Clear Menu"; ObjectID = "126"; */ "126.title" = "Clear Menu"; /* Class = "NSMenu"; title = "Services"; ObjectID = "130"; */ "130.title" = "Services"; /* Class = "NSMenuItem"; title = "Services"; ObjectID = "131"; */ "131.title" = "Services"; /* Class = "NSMenuItem"; title = "Hide Sparkle Test App"; ObjectID = "134"; */ "134.title" = "Hide Sparkle Test App"; /* Class = "NSMenuItem"; title = "Quit Sparkle Test App"; ObjectID = "136"; */ "136.title" = "Quit Sparkle Test App"; /* Class = "NSMenuItem"; title = "Hide Others"; ObjectID = "145"; */ "145.title" = "Hide Others"; /* Class = "NSMenuItem"; title = "Show All"; ObjectID = "150"; */ "150.title" = "Show All"; /* Class = "NSMenuItem"; title = "Find…"; ObjectID = "154"; */ "154.title" = "Find…"; /* Class = "NSMenuItem"; title = "Jump to Selection"; ObjectID = "155"; */ "155.title" = "Jump to Selection"; /* Class = "NSMenuItem"; title = "Copy"; ObjectID = "157"; */ "157.title" = "Copy"; /* Class = "NSMenuItem"; title = "Undo"; ObjectID = "158"; */ "158.title" = "Undo"; /* Class = "NSMenu"; title = "Find"; ObjectID = "159"; */ "159.title" = "Find"; /* Class = "NSMenuItem"; title = "Cut"; ObjectID = "160"; */ "160.title" = "Cut"; /* Class = "NSMenuItem"; title = "Use Selection for Find"; ObjectID = "161"; */ "161.title" = "Use Selection for Find"; /* Class = "NSMenuItem"; title = "Find Previous"; ObjectID = "162"; */ "162.title" = "Find Previous"; /* Class = "NSMenuItem"; title = "Edit"; ObjectID = "163"; */ "163.title" = "Edit"; /* Class = "NSMenuItem"; title = "Delete"; ObjectID = "164"; */ "164.title" = "Delete"; /* Class = "NSMenuItem"; title = "Find Next"; ObjectID = "167"; */ "167.title" = "Find Next"; /* Class = "NSMenuItem"; title = "Find"; ObjectID = "168"; */ "168.title" = "Find"; /* Class = "NSMenu"; title = "Edit"; ObjectID = "169"; */ "169.title" = "Edit"; /* Class = "NSMenuItem"; title = "Paste"; ObjectID = "171"; */ "171.title" = "Paste"; /* Class = "NSMenuItem"; title = "Select All"; ObjectID = "172"; */ "172.title" = "Select All"; /* Class = "NSMenuItem"; title = "Redo"; ObjectID = "173"; */ "173.title" = "Redo"; /* Class = "NSMenuItem"; title = "Spelling"; ObjectID = "184"; */ "184.title" = "Spelling"; /* Class = "NSMenu"; title = "Spelling"; ObjectID = "185"; */ "185.title" = "Spelling"; /* Class = "NSMenuItem"; title = "Spelling…"; ObjectID = "187"; */ "187.title" = "Spelling…"; /* Class = "NSMenuItem"; title = "Check Spelling"; ObjectID = "189"; */ "189.title" = "Check Spelling"; /* Class = "NSMenuItem"; title = "Check Spelling as You Type"; ObjectID = "191"; */ "191.title" = "Check Spelling as You Type"; /* Class = "NSMenuItem"; title = "Zoom"; ObjectID = "197"; */ "197.title" = "Zoom"; /* Class = "NSMenuItem"; title = "Paste and Match Style"; ObjectID = "204"; */ "204.title" = "Paste and Match Style"; /* Class = "NSMenuItem"; title = "Check for Updates…"; ObjectID = "207"; */ "207.title" = "Check for Updates…"; ================================================ FILE: TestApplication/main.m ================================================ // // main.m // Sparkle // // Created by Andy Matuschak on 3/12/06. // Copyright Andy Matuschak 2006. All rights reserved. // #import int main(int argc, const char *argv[]) { return NSApplicationMain(argc, argv); } ================================================ FILE: TestApplication/nb.lproj/MainMenu.strings ================================================ /* Class = "NSMenuItem"; title = "Bring All to Front"; ObjectID = "5"; */ "5.title" = "Bring All to Front"; /* Class = "NSMenuItem"; title = "Window"; ObjectID = "19"; */ "19.title" = "Window"; /* Class = "NSMenuItem"; title = "Minimize"; ObjectID = "23"; */ "23.title" = "Minimize"; /* Class = "NSMenu"; title = "Window"; ObjectID = "24"; */ "24.title" = "Window"; /* Class = "NSMenu"; title = "MainMenu"; ObjectID = "29"; */ "29.title" = "MainMenu"; /* Class = "NSMenuItem"; title = "Sparkle Test App"; ObjectID = "56"; */ "56.title" = "Sparkle Test App"; /* Class = "NSMenu"; title = "Sparkle Test App"; ObjectID = "57"; */ "57.title" = "Sparkle Test App"; /* Class = "NSMenuItem"; title = "About Sparkle Test App"; ObjectID = "58"; */ "58.title" = "About Sparkle Test App"; /* Class = "NSMenuItem"; title = "Open..."; ObjectID = "72"; */ "72.title" = "Open..."; /* Class = "NSMenuItem"; title = "Close"; ObjectID = "73"; */ "73.title" = "Close"; /* Class = "NSMenuItem"; title = "Save"; ObjectID = "75"; */ "75.title" = "Save"; /* Class = "NSMenuItem"; title = "Page Setup…"; ObjectID = "77"; */ "77.title" = "Page Setup…"; /* Class = "NSMenuItem"; title = "Print…"; ObjectID = "78"; */ "78.title" = "Print…"; /* Class = "NSMenuItem"; title = "Save As…"; ObjectID = "80"; */ "80.title" = "Save As…"; /* Class = "NSMenu"; title = "File"; ObjectID = "81"; */ "81.title" = "File"; /* Class = "NSMenuItem"; title = "New"; ObjectID = "82"; */ "82.title" = "New"; /* Class = "NSMenuItem"; title = "File"; ObjectID = "83"; */ "83.title" = "File"; /* Class = "NSMenuItem"; title = "Help"; ObjectID = "103"; */ "103.title" = "Help"; /* Class = "NSMenu"; title = "Help"; ObjectID = "106"; */ "106.title" = "Help"; /* Class = "NSMenuItem"; title = "Sparkle Test App Help"; ObjectID = "111"; */ "111.title" = "Sparkle Test App Help"; /* Class = "NSMenuItem"; title = "Revert"; ObjectID = "112"; */ "112.title" = "Revert"; /* Class = "NSMenuItem"; title = "Open Recent"; ObjectID = "124"; */ "124.title" = "Open Recent"; /* Class = "NSMenu"; title = "Open Recent"; ObjectID = "125"; */ "125.title" = "Open Recent"; /* Class = "NSMenuItem"; title = "Clear Menu"; ObjectID = "126"; */ "126.title" = "Clear Menu"; /* Class = "NSMenu"; title = "Services"; ObjectID = "130"; */ "130.title" = "Services"; /* Class = "NSMenuItem"; title = "Services"; ObjectID = "131"; */ "131.title" = "Services"; /* Class = "NSMenuItem"; title = "Hide Sparkle Test App"; ObjectID = "134"; */ "134.title" = "Hide Sparkle Test App"; /* Class = "NSMenuItem"; title = "Quit Sparkle Test App"; ObjectID = "136"; */ "136.title" = "Quit Sparkle Test App"; /* Class = "NSMenuItem"; title = "Hide Others"; ObjectID = "145"; */ "145.title" = "Hide Others"; /* Class = "NSMenuItem"; title = "Show All"; ObjectID = "150"; */ "150.title" = "Show All"; /* Class = "NSMenuItem"; title = "Find…"; ObjectID = "154"; */ "154.title" = "Find…"; /* Class = "NSMenuItem"; title = "Jump to Selection"; ObjectID = "155"; */ "155.title" = "Jump to Selection"; /* Class = "NSMenuItem"; title = "Copy"; ObjectID = "157"; */ "157.title" = "Copy"; /* Class = "NSMenuItem"; title = "Undo"; ObjectID = "158"; */ "158.title" = "Undo"; /* Class = "NSMenu"; title = "Find"; ObjectID = "159"; */ "159.title" = "Find"; /* Class = "NSMenuItem"; title = "Cut"; ObjectID = "160"; */ "160.title" = "Cut"; /* Class = "NSMenuItem"; title = "Use Selection for Find"; ObjectID = "161"; */ "161.title" = "Use Selection for Find"; /* Class = "NSMenuItem"; title = "Find Previous"; ObjectID = "162"; */ "162.title" = "Find Previous"; /* Class = "NSMenuItem"; title = "Edit"; ObjectID = "163"; */ "163.title" = "Edit"; /* Class = "NSMenuItem"; title = "Delete"; ObjectID = "164"; */ "164.title" = "Delete"; /* Class = "NSMenuItem"; title = "Find Next"; ObjectID = "167"; */ "167.title" = "Find Next"; /* Class = "NSMenuItem"; title = "Find"; ObjectID = "168"; */ "168.title" = "Find"; /* Class = "NSMenu"; title = "Edit"; ObjectID = "169"; */ "169.title" = "Edit"; /* Class = "NSMenuItem"; title = "Paste"; ObjectID = "171"; */ "171.title" = "Paste"; /* Class = "NSMenuItem"; title = "Select All"; ObjectID = "172"; */ "172.title" = "Select All"; /* Class = "NSMenuItem"; title = "Redo"; ObjectID = "173"; */ "173.title" = "Redo"; /* Class = "NSMenuItem"; title = "Spelling"; ObjectID = "184"; */ "184.title" = "Spelling"; /* Class = "NSMenu"; title = "Spelling"; ObjectID = "185"; */ "185.title" = "Spelling"; /* Class = "NSMenuItem"; title = "Spelling…"; ObjectID = "187"; */ "187.title" = "Spelling…"; /* Class = "NSMenuItem"; title = "Check Spelling"; ObjectID = "189"; */ "189.title" = "Check Spelling"; /* Class = "NSMenuItem"; title = "Check Spelling as You Type"; ObjectID = "191"; */ "191.title" = "Check Spelling as You Type"; /* Class = "NSMenuItem"; title = "Zoom"; ObjectID = "197"; */ "197.title" = "Zoom"; /* Class = "NSMenuItem"; title = "Paste and Match Style"; ObjectID = "204"; */ "204.title" = "Paste and Match Style"; /* Class = "NSMenuItem"; title = "Check for Updates…"; ObjectID = "207"; */ "207.title" = "Check for Updates…"; ================================================ FILE: TestApplication/nl.lproj/MainMenu.strings ================================================ /* Class = "NSMenuItem"; title = "Bring All to Front"; ObjectID = "5"; */ "5.title" = "Bring All to Front"; /* Class = "NSMenuItem"; title = "Window"; ObjectID = "19"; */ "19.title" = "Window"; /* Class = "NSMenuItem"; title = "Minimize"; ObjectID = "23"; */ "23.title" = "Minimize"; /* Class = "NSMenu"; title = "Window"; ObjectID = "24"; */ "24.title" = "Window"; /* Class = "NSMenu"; title = "MainMenu"; ObjectID = "29"; */ "29.title" = "MainMenu"; /* Class = "NSMenuItem"; title = "Sparkle Test App"; ObjectID = "56"; */ "56.title" = "Sparkle Test App"; /* Class = "NSMenu"; title = "Sparkle Test App"; ObjectID = "57"; */ "57.title" = "Sparkle Test App"; /* Class = "NSMenuItem"; title = "About Sparkle Test App"; ObjectID = "58"; */ "58.title" = "About Sparkle Test App"; /* Class = "NSMenuItem"; title = "Open..."; ObjectID = "72"; */ "72.title" = "Open..."; /* Class = "NSMenuItem"; title = "Close"; ObjectID = "73"; */ "73.title" = "Close"; /* Class = "NSMenuItem"; title = "Save"; ObjectID = "75"; */ "75.title" = "Save"; /* Class = "NSMenuItem"; title = "Page Setup…"; ObjectID = "77"; */ "77.title" = "Page Setup…"; /* Class = "NSMenuItem"; title = "Print…"; ObjectID = "78"; */ "78.title" = "Print…"; /* Class = "NSMenuItem"; title = "Save As…"; ObjectID = "80"; */ "80.title" = "Save As…"; /* Class = "NSMenu"; title = "File"; ObjectID = "81"; */ "81.title" = "File"; /* Class = "NSMenuItem"; title = "New"; ObjectID = "82"; */ "82.title" = "New"; /* Class = "NSMenuItem"; title = "File"; ObjectID = "83"; */ "83.title" = "File"; /* Class = "NSMenuItem"; title = "Help"; ObjectID = "103"; */ "103.title" = "Help"; /* Class = "NSMenu"; title = "Help"; ObjectID = "106"; */ "106.title" = "Help"; /* Class = "NSMenuItem"; title = "Sparkle Test App Help"; ObjectID = "111"; */ "111.title" = "Sparkle Test App Help"; /* Class = "NSMenuItem"; title = "Revert"; ObjectID = "112"; */ "112.title" = "Revert"; /* Class = "NSMenuItem"; title = "Open Recent"; ObjectID = "124"; */ "124.title" = "Open Recent"; /* Class = "NSMenu"; title = "Open Recent"; ObjectID = "125"; */ "125.title" = "Open Recent"; /* Class = "NSMenuItem"; title = "Clear Menu"; ObjectID = "126"; */ "126.title" = "Clear Menu"; /* Class = "NSMenu"; title = "Services"; ObjectID = "130"; */ "130.title" = "Services"; /* Class = "NSMenuItem"; title = "Services"; ObjectID = "131"; */ "131.title" = "Services"; /* Class = "NSMenuItem"; title = "Hide Sparkle Test App"; ObjectID = "134"; */ "134.title" = "Hide Sparkle Test App"; /* Class = "NSMenuItem"; title = "Quit Sparkle Test App"; ObjectID = "136"; */ "136.title" = "Quit Sparkle Test App"; /* Class = "NSMenuItem"; title = "Hide Others"; ObjectID = "145"; */ "145.title" = "Hide Others"; /* Class = "NSMenuItem"; title = "Show All"; ObjectID = "150"; */ "150.title" = "Show All"; /* Class = "NSMenuItem"; title = "Find…"; ObjectID = "154"; */ "154.title" = "Find…"; /* Class = "NSMenuItem"; title = "Jump to Selection"; ObjectID = "155"; */ "155.title" = "Jump to Selection"; /* Class = "NSMenuItem"; title = "Copy"; ObjectID = "157"; */ "157.title" = "Copy"; /* Class = "NSMenuItem"; title = "Undo"; ObjectID = "158"; */ "158.title" = "Undo"; /* Class = "NSMenu"; title = "Find"; ObjectID = "159"; */ "159.title" = "Find"; /* Class = "NSMenuItem"; title = "Cut"; ObjectID = "160"; */ "160.title" = "Cut"; /* Class = "NSMenuItem"; title = "Use Selection for Find"; ObjectID = "161"; */ "161.title" = "Use Selection for Find"; /* Class = "NSMenuItem"; title = "Find Previous"; ObjectID = "162"; */ "162.title" = "Find Previous"; /* Class = "NSMenuItem"; title = "Edit"; ObjectID = "163"; */ "163.title" = "Edit"; /* Class = "NSMenuItem"; title = "Delete"; ObjectID = "164"; */ "164.title" = "Delete"; /* Class = "NSMenuItem"; title = "Find Next"; ObjectID = "167"; */ "167.title" = "Find Next"; /* Class = "NSMenuItem"; title = "Find"; ObjectID = "168"; */ "168.title" = "Find"; /* Class = "NSMenu"; title = "Edit"; ObjectID = "169"; */ "169.title" = "Edit"; /* Class = "NSMenuItem"; title = "Paste"; ObjectID = "171"; */ "171.title" = "Paste"; /* Class = "NSMenuItem"; title = "Select All"; ObjectID = "172"; */ "172.title" = "Select All"; /* Class = "NSMenuItem"; title = "Redo"; ObjectID = "173"; */ "173.title" = "Redo"; /* Class = "NSMenuItem"; title = "Spelling"; ObjectID = "184"; */ "184.title" = "Spelling"; /* Class = "NSMenu"; title = "Spelling"; ObjectID = "185"; */ "185.title" = "Spelling"; /* Class = "NSMenuItem"; title = "Spelling…"; ObjectID = "187"; */ "187.title" = "Spelling…"; /* Class = "NSMenuItem"; title = "Check Spelling"; ObjectID = "189"; */ "189.title" = "Check Spelling"; /* Class = "NSMenuItem"; title = "Check Spelling as You Type"; ObjectID = "191"; */ "191.title" = "Check Spelling as You Type"; /* Class = "NSMenuItem"; title = "Zoom"; ObjectID = "197"; */ "197.title" = "Zoom"; /* Class = "NSMenuItem"; title = "Paste and Match Style"; ObjectID = "204"; */ "204.title" = "Paste and Match Style"; /* Class = "NSMenuItem"; title = "Check for Updates…"; ObjectID = "207"; */ "207.title" = "Check for Updates…"; ================================================ FILE: TestApplication/nn.lproj/MainMenu.strings ================================================ /* Class = "NSMenuItem"; title = "Bring All to Front"; ObjectID = "5"; */ "5.title" = "Bring All to Front"; /* Class = "NSMenuItem"; title = "Window"; ObjectID = "19"; */ "19.title" = "Window"; /* Class = "NSMenuItem"; title = "Minimize"; ObjectID = "23"; */ "23.title" = "Minimize"; /* Class = "NSMenu"; title = "Window"; ObjectID = "24"; */ "24.title" = "Window"; /* Class = "NSMenu"; title = "MainMenu"; ObjectID = "29"; */ "29.title" = "MainMenu"; /* Class = "NSMenuItem"; title = "Sparkle Test App"; ObjectID = "56"; */ "56.title" = "Sparkle Test App"; /* Class = "NSMenu"; title = "Sparkle Test App"; ObjectID = "57"; */ "57.title" = "Sparkle Test App"; /* Class = "NSMenuItem"; title = "About Sparkle Test App"; ObjectID = "58"; */ "58.title" = "About Sparkle Test App"; /* Class = "NSMenuItem"; title = "Open..."; ObjectID = "72"; */ "72.title" = "Open..."; /* Class = "NSMenuItem"; title = "Close"; ObjectID = "73"; */ "73.title" = "Close"; /* Class = "NSMenuItem"; title = "Save"; ObjectID = "75"; */ "75.title" = "Save"; /* Class = "NSMenuItem"; title = "Page Setup…"; ObjectID = "77"; */ "77.title" = "Page Setup…"; /* Class = "NSMenuItem"; title = "Print…"; ObjectID = "78"; */ "78.title" = "Print…"; /* Class = "NSMenuItem"; title = "Save As…"; ObjectID = "80"; */ "80.title" = "Save As…"; /* Class = "NSMenu"; title = "File"; ObjectID = "81"; */ "81.title" = "File"; /* Class = "NSMenuItem"; title = "New"; ObjectID = "82"; */ "82.title" = "New"; /* Class = "NSMenuItem"; title = "File"; ObjectID = "83"; */ "83.title" = "File"; /* Class = "NSMenuItem"; title = "Help"; ObjectID = "103"; */ "103.title" = "Help"; /* Class = "NSMenu"; title = "Help"; ObjectID = "106"; */ "106.title" = "Help"; /* Class = "NSMenuItem"; title = "Sparkle Test App Help"; ObjectID = "111"; */ "111.title" = "Sparkle Test App Help"; /* Class = "NSMenuItem"; title = "Revert"; ObjectID = "112"; */ "112.title" = "Revert"; /* Class = "NSMenuItem"; title = "Open Recent"; ObjectID = "124"; */ "124.title" = "Open Recent"; /* Class = "NSMenu"; title = "Open Recent"; ObjectID = "125"; */ "125.title" = "Open Recent"; /* Class = "NSMenuItem"; title = "Clear Menu"; ObjectID = "126"; */ "126.title" = "Clear Menu"; /* Class = "NSMenu"; title = "Services"; ObjectID = "130"; */ "130.title" = "Services"; /* Class = "NSMenuItem"; title = "Services"; ObjectID = "131"; */ "131.title" = "Services"; /* Class = "NSMenuItem"; title = "Hide Sparkle Test App"; ObjectID = "134"; */ "134.title" = "Hide Sparkle Test App"; /* Class = "NSMenuItem"; title = "Quit Sparkle Test App"; ObjectID = "136"; */ "136.title" = "Quit Sparkle Test App"; /* Class = "NSMenuItem"; title = "Hide Others"; ObjectID = "145"; */ "145.title" = "Hide Others"; /* Class = "NSMenuItem"; title = "Show All"; ObjectID = "150"; */ "150.title" = "Show All"; /* Class = "NSMenuItem"; title = "Find…"; ObjectID = "154"; */ "154.title" = "Find…"; /* Class = "NSMenuItem"; title = "Jump to Selection"; ObjectID = "155"; */ "155.title" = "Jump to Selection"; /* Class = "NSMenuItem"; title = "Copy"; ObjectID = "157"; */ "157.title" = "Copy"; /* Class = "NSMenuItem"; title = "Undo"; ObjectID = "158"; */ "158.title" = "Undo"; /* Class = "NSMenu"; title = "Find"; ObjectID = "159"; */ "159.title" = "Find"; /* Class = "NSMenuItem"; title = "Cut"; ObjectID = "160"; */ "160.title" = "Cut"; /* Class = "NSMenuItem"; title = "Use Selection for Find"; ObjectID = "161"; */ "161.title" = "Use Selection for Find"; /* Class = "NSMenuItem"; title = "Find Previous"; ObjectID = "162"; */ "162.title" = "Find Previous"; /* Class = "NSMenuItem"; title = "Edit"; ObjectID = "163"; */ "163.title" = "Edit"; /* Class = "NSMenuItem"; title = "Delete"; ObjectID = "164"; */ "164.title" = "Delete"; /* Class = "NSMenuItem"; title = "Find Next"; ObjectID = "167"; */ "167.title" = "Find Next"; /* Class = "NSMenuItem"; title = "Find"; ObjectID = "168"; */ "168.title" = "Find"; /* Class = "NSMenu"; title = "Edit"; ObjectID = "169"; */ "169.title" = "Edit"; /* Class = "NSMenuItem"; title = "Paste"; ObjectID = "171"; */ "171.title" = "Paste"; /* Class = "NSMenuItem"; title = "Select All"; ObjectID = "172"; */ "172.title" = "Select All"; /* Class = "NSMenuItem"; title = "Redo"; ObjectID = "173"; */ "173.title" = "Redo"; /* Class = "NSMenuItem"; title = "Spelling"; ObjectID = "184"; */ "184.title" = "Spelling"; /* Class = "NSMenu"; title = "Spelling"; ObjectID = "185"; */ "185.title" = "Spelling"; /* Class = "NSMenuItem"; title = "Spelling…"; ObjectID = "187"; */ "187.title" = "Spelling…"; /* Class = "NSMenuItem"; title = "Check Spelling"; ObjectID = "189"; */ "189.title" = "Check Spelling"; /* Class = "NSMenuItem"; title = "Check Spelling as You Type"; ObjectID = "191"; */ "191.title" = "Check Spelling as You Type"; /* Class = "NSMenuItem"; title = "Zoom"; ObjectID = "197"; */ "197.title" = "Zoom"; /* Class = "NSMenuItem"; title = "Paste and Match Style"; ObjectID = "204"; */ "204.title" = "Paste and Match Style"; /* Class = "NSMenuItem"; title = "Check for Updates…"; ObjectID = "207"; */ "207.title" = "Check for Updates…"; ================================================ FILE: TestApplication/pl.lproj/MainMenu.strings ================================================ /* Class = "NSMenuItem"; title = "Bring All to Front"; ObjectID = "5"; */ "5.title" = "Bring All to Front"; /* Class = "NSMenuItem"; title = "Window"; ObjectID = "19"; */ "19.title" = "Window"; /* Class = "NSMenuItem"; title = "Minimize"; ObjectID = "23"; */ "23.title" = "Minimize"; /* Class = "NSMenu"; title = "Window"; ObjectID = "24"; */ "24.title" = "Window"; /* Class = "NSMenu"; title = "MainMenu"; ObjectID = "29"; */ "29.title" = "MainMenu"; /* Class = "NSMenuItem"; title = "Sparkle Test App"; ObjectID = "56"; */ "56.title" = "Sparkle Test App"; /* Class = "NSMenu"; title = "Sparkle Test App"; ObjectID = "57"; */ "57.title" = "Sparkle Test App"; /* Class = "NSMenuItem"; title = "About Sparkle Test App"; ObjectID = "58"; */ "58.title" = "About Sparkle Test App"; /* Class = "NSMenuItem"; title = "Open..."; ObjectID = "72"; */ "72.title" = "Open..."; /* Class = "NSMenuItem"; title = "Close"; ObjectID = "73"; */ "73.title" = "Close"; /* Class = "NSMenuItem"; title = "Save"; ObjectID = "75"; */ "75.title" = "Save"; /* Class = "NSMenuItem"; title = "Page Setup…"; ObjectID = "77"; */ "77.title" = "Page Setup…"; /* Class = "NSMenuItem"; title = "Print…"; ObjectID = "78"; */ "78.title" = "Print…"; /* Class = "NSMenuItem"; title = "Save As…"; ObjectID = "80"; */ "80.title" = "Save As…"; /* Class = "NSMenu"; title = "File"; ObjectID = "81"; */ "81.title" = "File"; /* Class = "NSMenuItem"; title = "New"; ObjectID = "82"; */ "82.title" = "New"; /* Class = "NSMenuItem"; title = "File"; ObjectID = "83"; */ "83.title" = "File"; /* Class = "NSMenuItem"; title = "Help"; ObjectID = "103"; */ "103.title" = "Help"; /* Class = "NSMenu"; title = "Help"; ObjectID = "106"; */ "106.title" = "Help"; /* Class = "NSMenuItem"; title = "Sparkle Test App Help"; ObjectID = "111"; */ "111.title" = "Sparkle Test App Help"; /* Class = "NSMenuItem"; title = "Revert"; ObjectID = "112"; */ "112.title" = "Revert"; /* Class = "NSMenuItem"; title = "Open Recent"; ObjectID = "124"; */ "124.title" = "Open Recent"; /* Class = "NSMenu"; title = "Open Recent"; ObjectID = "125"; */ "125.title" = "Open Recent"; /* Class = "NSMenuItem"; title = "Clear Menu"; ObjectID = "126"; */ "126.title" = "Clear Menu"; /* Class = "NSMenu"; title = "Services"; ObjectID = "130"; */ "130.title" = "Services"; /* Class = "NSMenuItem"; title = "Services"; ObjectID = "131"; */ "131.title" = "Services"; /* Class = "NSMenuItem"; title = "Hide Sparkle Test App"; ObjectID = "134"; */ "134.title" = "Hide Sparkle Test App"; /* Class = "NSMenuItem"; title = "Quit Sparkle Test App"; ObjectID = "136"; */ "136.title" = "Quit Sparkle Test App"; /* Class = "NSMenuItem"; title = "Hide Others"; ObjectID = "145"; */ "145.title" = "Hide Others"; /* Class = "NSMenuItem"; title = "Show All"; ObjectID = "150"; */ "150.title" = "Show All"; /* Class = "NSMenuItem"; title = "Find…"; ObjectID = "154"; */ "154.title" = "Find…"; /* Class = "NSMenuItem"; title = "Jump to Selection"; ObjectID = "155"; */ "155.title" = "Jump to Selection"; /* Class = "NSMenuItem"; title = "Copy"; ObjectID = "157"; */ "157.title" = "Copy"; /* Class = "NSMenuItem"; title = "Undo"; ObjectID = "158"; */ "158.title" = "Undo"; /* Class = "NSMenu"; title = "Find"; ObjectID = "159"; */ "159.title" = "Find"; /* Class = "NSMenuItem"; title = "Cut"; ObjectID = "160"; */ "160.title" = "Cut"; /* Class = "NSMenuItem"; title = "Use Selection for Find"; ObjectID = "161"; */ "161.title" = "Use Selection for Find"; /* Class = "NSMenuItem"; title = "Find Previous"; ObjectID = "162"; */ "162.title" = "Find Previous"; /* Class = "NSMenuItem"; title = "Edit"; ObjectID = "163"; */ "163.title" = "Edit"; /* Class = "NSMenuItem"; title = "Delete"; ObjectID = "164"; */ "164.title" = "Delete"; /* Class = "NSMenuItem"; title = "Find Next"; ObjectID = "167"; */ "167.title" = "Find Next"; /* Class = "NSMenuItem"; title = "Find"; ObjectID = "168"; */ "168.title" = "Find"; /* Class = "NSMenu"; title = "Edit"; ObjectID = "169"; */ "169.title" = "Edit"; /* Class = "NSMenuItem"; title = "Paste"; ObjectID = "171"; */ "171.title" = "Paste"; /* Class = "NSMenuItem"; title = "Select All"; ObjectID = "172"; */ "172.title" = "Select All"; /* Class = "NSMenuItem"; title = "Redo"; ObjectID = "173"; */ "173.title" = "Redo"; /* Class = "NSMenuItem"; title = "Spelling"; ObjectID = "184"; */ "184.title" = "Spelling"; /* Class = "NSMenu"; title = "Spelling"; ObjectID = "185"; */ "185.title" = "Spelling"; /* Class = "NSMenuItem"; title = "Spelling…"; ObjectID = "187"; */ "187.title" = "Spelling…"; /* Class = "NSMenuItem"; title = "Check Spelling"; ObjectID = "189"; */ "189.title" = "Check Spelling"; /* Class = "NSMenuItem"; title = "Check Spelling as You Type"; ObjectID = "191"; */ "191.title" = "Check Spelling as You Type"; /* Class = "NSMenuItem"; title = "Zoom"; ObjectID = "197"; */ "197.title" = "Zoom"; /* Class = "NSMenuItem"; title = "Paste and Match Style"; ObjectID = "204"; */ "204.title" = "Paste and Match Style"; /* Class = "NSMenuItem"; title = "Check for Updates…"; ObjectID = "207"; */ "207.title" = "Check for Updates…"; ================================================ FILE: TestApplication/pt-BR.lproj/MainMenu.strings ================================================ /* Class = "NSMenuItem"; title = "Bring All to Front"; ObjectID = "5"; */ "5.title" = "Bring All to Front"; /* Class = "NSMenuItem"; title = "Window"; ObjectID = "19"; */ "19.title" = "Window"; /* Class = "NSMenuItem"; title = "Minimize"; ObjectID = "23"; */ "23.title" = "Minimize"; /* Class = "NSMenu"; title = "Window"; ObjectID = "24"; */ "24.title" = "Window"; /* Class = "NSMenu"; title = "MainMenu"; ObjectID = "29"; */ "29.title" = "MainMenu"; /* Class = "NSMenuItem"; title = "Sparkle Test App"; ObjectID = "56"; */ "56.title" = "Sparkle Test App"; /* Class = "NSMenu"; title = "Sparkle Test App"; ObjectID = "57"; */ "57.title" = "Sparkle Test App"; /* Class = "NSMenuItem"; title = "About Sparkle Test App"; ObjectID = "58"; */ "58.title" = "About Sparkle Test App"; /* Class = "NSMenuItem"; title = "Open..."; ObjectID = "72"; */ "72.title" = "Open..."; /* Class = "NSMenuItem"; title = "Close"; ObjectID = "73"; */ "73.title" = "Close"; /* Class = "NSMenuItem"; title = "Save"; ObjectID = "75"; */ "75.title" = "Save"; /* Class = "NSMenuItem"; title = "Page Setup…"; ObjectID = "77"; */ "77.title" = "Page Setup…"; /* Class = "NSMenuItem"; title = "Print…"; ObjectID = "78"; */ "78.title" = "Print…"; /* Class = "NSMenuItem"; title = "Save As…"; ObjectID = "80"; */ "80.title" = "Save As…"; /* Class = "NSMenu"; title = "File"; ObjectID = "81"; */ "81.title" = "File"; /* Class = "NSMenuItem"; title = "New"; ObjectID = "82"; */ "82.title" = "New"; /* Class = "NSMenuItem"; title = "File"; ObjectID = "83"; */ "83.title" = "File"; /* Class = "NSMenuItem"; title = "Help"; ObjectID = "103"; */ "103.title" = "Help"; /* Class = "NSMenu"; title = "Help"; ObjectID = "106"; */ "106.title" = "Help"; /* Class = "NSMenuItem"; title = "Sparkle Test App Help"; ObjectID = "111"; */ "111.title" = "Sparkle Test App Help"; /* Class = "NSMenuItem"; title = "Revert"; ObjectID = "112"; */ "112.title" = "Revert"; /* Class = "NSMenuItem"; title = "Open Recent"; ObjectID = "124"; */ "124.title" = "Open Recent"; /* Class = "NSMenu"; title = "Open Recent"; ObjectID = "125"; */ "125.title" = "Open Recent"; /* Class = "NSMenuItem"; title = "Clear Menu"; ObjectID = "126"; */ "126.title" = "Clear Menu"; /* Class = "NSMenu"; title = "Services"; ObjectID = "130"; */ "130.title" = "Services"; /* Class = "NSMenuItem"; title = "Services"; ObjectID = "131"; */ "131.title" = "Services"; /* Class = "NSMenuItem"; title = "Hide Sparkle Test App"; ObjectID = "134"; */ "134.title" = "Hide Sparkle Test App"; /* Class = "NSMenuItem"; title = "Quit Sparkle Test App"; ObjectID = "136"; */ "136.title" = "Quit Sparkle Test App"; /* Class = "NSMenuItem"; title = "Hide Others"; ObjectID = "145"; */ "145.title" = "Hide Others"; /* Class = "NSMenuItem"; title = "Show All"; ObjectID = "150"; */ "150.title" = "Show All"; /* Class = "NSMenuItem"; title = "Find…"; ObjectID = "154"; */ "154.title" = "Find…"; /* Class = "NSMenuItem"; title = "Jump to Selection"; ObjectID = "155"; */ "155.title" = "Jump to Selection"; /* Class = "NSMenuItem"; title = "Copy"; ObjectID = "157"; */ "157.title" = "Copy"; /* Class = "NSMenuItem"; title = "Undo"; ObjectID = "158"; */ "158.title" = "Undo"; /* Class = "NSMenu"; title = "Find"; ObjectID = "159"; */ "159.title" = "Find"; /* Class = "NSMenuItem"; title = "Cut"; ObjectID = "160"; */ "160.title" = "Cut"; /* Class = "NSMenuItem"; title = "Use Selection for Find"; ObjectID = "161"; */ "161.title" = "Use Selection for Find"; /* Class = "NSMenuItem"; title = "Find Previous"; ObjectID = "162"; */ "162.title" = "Find Previous"; /* Class = "NSMenuItem"; title = "Edit"; ObjectID = "163"; */ "163.title" = "Edit"; /* Class = "NSMenuItem"; title = "Delete"; ObjectID = "164"; */ "164.title" = "Delete"; /* Class = "NSMenuItem"; title = "Find Next"; ObjectID = "167"; */ "167.title" = "Find Next"; /* Class = "NSMenuItem"; title = "Find"; ObjectID = "168"; */ "168.title" = "Find"; /* Class = "NSMenu"; title = "Edit"; ObjectID = "169"; */ "169.title" = "Edit"; /* Class = "NSMenuItem"; title = "Paste"; ObjectID = "171"; */ "171.title" = "Paste"; /* Class = "NSMenuItem"; title = "Select All"; ObjectID = "172"; */ "172.title" = "Select All"; /* Class = "NSMenuItem"; title = "Redo"; ObjectID = "173"; */ "173.title" = "Redo"; /* Class = "NSMenuItem"; title = "Spelling"; ObjectID = "184"; */ "184.title" = "Spelling"; /* Class = "NSMenu"; title = "Spelling"; ObjectID = "185"; */ "185.title" = "Spelling"; /* Class = "NSMenuItem"; title = "Spelling…"; ObjectID = "187"; */ "187.title" = "Spelling…"; /* Class = "NSMenuItem"; title = "Check Spelling"; ObjectID = "189"; */ "189.title" = "Check Spelling"; /* Class = "NSMenuItem"; title = "Check Spelling as You Type"; ObjectID = "191"; */ "191.title" = "Check Spelling as You Type"; /* Class = "NSMenuItem"; title = "Zoom"; ObjectID = "197"; */ "197.title" = "Zoom"; /* Class = "NSMenuItem"; title = "Paste and Match Style"; ObjectID = "204"; */ "204.title" = "Paste and Match Style"; /* Class = "NSMenuItem"; title = "Check for Updates…"; ObjectID = "207"; */ "207.title" = "Check for Updates…"; ================================================ FILE: TestApplication/pt-PT.lproj/MainMenu.strings ================================================ /* Class = "NSMenuItem"; title = "Bring All to Front"; ObjectID = "5"; */ "5.title" = "Bring All to Front"; /* Class = "NSMenuItem"; title = "Window"; ObjectID = "19"; */ "19.title" = "Window"; /* Class = "NSMenuItem"; title = "Minimize"; ObjectID = "23"; */ "23.title" = "Minimize"; /* Class = "NSMenu"; title = "Window"; ObjectID = "24"; */ "24.title" = "Window"; /* Class = "NSMenu"; title = "MainMenu"; ObjectID = "29"; */ "29.title" = "MainMenu"; /* Class = "NSMenuItem"; title = "Sparkle Test App"; ObjectID = "56"; */ "56.title" = "Sparkle Test App"; /* Class = "NSMenu"; title = "Sparkle Test App"; ObjectID = "57"; */ "57.title" = "Sparkle Test App"; /* Class = "NSMenuItem"; title = "About Sparkle Test App"; ObjectID = "58"; */ "58.title" = "About Sparkle Test App"; /* Class = "NSMenuItem"; title = "Open..."; ObjectID = "72"; */ "72.title" = "Open..."; /* Class = "NSMenuItem"; title = "Close"; ObjectID = "73"; */ "73.title" = "Close"; /* Class = "NSMenuItem"; title = "Save"; ObjectID = "75"; */ "75.title" = "Save"; /* Class = "NSMenuItem"; title = "Page Setup…"; ObjectID = "77"; */ "77.title" = "Page Setup…"; /* Class = "NSMenuItem"; title = "Print…"; ObjectID = "78"; */ "78.title" = "Print…"; /* Class = "NSMenuItem"; title = "Save As…"; ObjectID = "80"; */ "80.title" = "Save As…"; /* Class = "NSMenu"; title = "File"; ObjectID = "81"; */ "81.title" = "File"; /* Class = "NSMenuItem"; title = "New"; ObjectID = "82"; */ "82.title" = "New"; /* Class = "NSMenuItem"; title = "File"; ObjectID = "83"; */ "83.title" = "File"; /* Class = "NSMenuItem"; title = "Help"; ObjectID = "103"; */ "103.title" = "Help"; /* Class = "NSMenu"; title = "Help"; ObjectID = "106"; */ "106.title" = "Help"; /* Class = "NSMenuItem"; title = "Sparkle Test App Help"; ObjectID = "111"; */ "111.title" = "Sparkle Test App Help"; /* Class = "NSMenuItem"; title = "Revert"; ObjectID = "112"; */ "112.title" = "Revert"; /* Class = "NSMenuItem"; title = "Open Recent"; ObjectID = "124"; */ "124.title" = "Open Recent"; /* Class = "NSMenu"; title = "Open Recent"; ObjectID = "125"; */ "125.title" = "Open Recent"; /* Class = "NSMenuItem"; title = "Clear Menu"; ObjectID = "126"; */ "126.title" = "Clear Menu"; /* Class = "NSMenu"; title = "Services"; ObjectID = "130"; */ "130.title" = "Services"; /* Class = "NSMenuItem"; title = "Services"; ObjectID = "131"; */ "131.title" = "Services"; /* Class = "NSMenuItem"; title = "Hide Sparkle Test App"; ObjectID = "134"; */ "134.title" = "Hide Sparkle Test App"; /* Class = "NSMenuItem"; title = "Quit Sparkle Test App"; ObjectID = "136"; */ "136.title" = "Quit Sparkle Test App"; /* Class = "NSMenuItem"; title = "Hide Others"; ObjectID = "145"; */ "145.title" = "Hide Others"; /* Class = "NSMenuItem"; title = "Show All"; ObjectID = "150"; */ "150.title" = "Show All"; /* Class = "NSMenuItem"; title = "Find…"; ObjectID = "154"; */ "154.title" = "Find…"; /* Class = "NSMenuItem"; title = "Jump to Selection"; ObjectID = "155"; */ "155.title" = "Jump to Selection"; /* Class = "NSMenuItem"; title = "Copy"; ObjectID = "157"; */ "157.title" = "Copy"; /* Class = "NSMenuItem"; title = "Undo"; ObjectID = "158"; */ "158.title" = "Undo"; /* Class = "NSMenu"; title = "Find"; ObjectID = "159"; */ "159.title" = "Find"; /* Class = "NSMenuItem"; title = "Cut"; ObjectID = "160"; */ "160.title" = "Cut"; /* Class = "NSMenuItem"; title = "Use Selection for Find"; ObjectID = "161"; */ "161.title" = "Use Selection for Find"; /* Class = "NSMenuItem"; title = "Find Previous"; ObjectID = "162"; */ "162.title" = "Find Previous"; /* Class = "NSMenuItem"; title = "Edit"; ObjectID = "163"; */ "163.title" = "Edit"; /* Class = "NSMenuItem"; title = "Delete"; ObjectID = "164"; */ "164.title" = "Delete"; /* Class = "NSMenuItem"; title = "Find Next"; ObjectID = "167"; */ "167.title" = "Find Next"; /* Class = "NSMenuItem"; title = "Find"; ObjectID = "168"; */ "168.title" = "Find"; /* Class = "NSMenu"; title = "Edit"; ObjectID = "169"; */ "169.title" = "Edit"; /* Class = "NSMenuItem"; title = "Paste"; ObjectID = "171"; */ "171.title" = "Paste"; /* Class = "NSMenuItem"; title = "Select All"; ObjectID = "172"; */ "172.title" = "Select All"; /* Class = "NSMenuItem"; title = "Redo"; ObjectID = "173"; */ "173.title" = "Redo"; /* Class = "NSMenuItem"; title = "Spelling"; ObjectID = "184"; */ "184.title" = "Spelling"; /* Class = "NSMenu"; title = "Spelling"; ObjectID = "185"; */ "185.title" = "Spelling"; /* Class = "NSMenuItem"; title = "Spelling…"; ObjectID = "187"; */ "187.title" = "Spelling…"; /* Class = "NSMenuItem"; title = "Check Spelling"; ObjectID = "189"; */ "189.title" = "Check Spelling"; /* Class = "NSMenuItem"; title = "Check Spelling as You Type"; ObjectID = "191"; */ "191.title" = "Check Spelling as You Type"; /* Class = "NSMenuItem"; title = "Zoom"; ObjectID = "197"; */ "197.title" = "Zoom"; /* Class = "NSMenuItem"; title = "Paste and Match Style"; ObjectID = "204"; */ "204.title" = "Paste and Match Style"; /* Class = "NSMenuItem"; title = "Check for Updates…"; ObjectID = "207"; */ "207.title" = "Check for Updates…"; ================================================ FILE: TestApplication/ro.lproj/MainMenu.strings ================================================ /* Class = "NSMenuItem"; title = "Bring All to Front"; ObjectID = "5"; */ "5.title" = "Bring All to Front"; /* Class = "NSMenuItem"; title = "Window"; ObjectID = "19"; */ "19.title" = "Window"; /* Class = "NSMenuItem"; title = "Minimize"; ObjectID = "23"; */ "23.title" = "Minimize"; /* Class = "NSMenu"; title = "Window"; ObjectID = "24"; */ "24.title" = "Window"; /* Class = "NSMenu"; title = "MainMenu"; ObjectID = "29"; */ "29.title" = "MainMenu"; /* Class = "NSMenuItem"; title = "Sparkle Test App"; ObjectID = "56"; */ "56.title" = "Sparkle Test App"; /* Class = "NSMenu"; title = "Sparkle Test App"; ObjectID = "57"; */ "57.title" = "Sparkle Test App"; /* Class = "NSMenuItem"; title = "About Sparkle Test App"; ObjectID = "58"; */ "58.title" = "About Sparkle Test App"; /* Class = "NSMenuItem"; title = "Open..."; ObjectID = "72"; */ "72.title" = "Open..."; /* Class = "NSMenuItem"; title = "Close"; ObjectID = "73"; */ "73.title" = "Close"; /* Class = "NSMenuItem"; title = "Save"; ObjectID = "75"; */ "75.title" = "Save"; /* Class = "NSMenuItem"; title = "Page Setup…"; ObjectID = "77"; */ "77.title" = "Page Setup…"; /* Class = "NSMenuItem"; title = "Print…"; ObjectID = "78"; */ "78.title" = "Print…"; /* Class = "NSMenuItem"; title = "Save As…"; ObjectID = "80"; */ "80.title" = "Save As…"; /* Class = "NSMenu"; title = "File"; ObjectID = "81"; */ "81.title" = "File"; /* Class = "NSMenuItem"; title = "New"; ObjectID = "82"; */ "82.title" = "New"; /* Class = "NSMenuItem"; title = "File"; ObjectID = "83"; */ "83.title" = "File"; /* Class = "NSMenuItem"; title = "Help"; ObjectID = "103"; */ "103.title" = "Help"; /* Class = "NSMenu"; title = "Help"; ObjectID = "106"; */ "106.title" = "Help"; /* Class = "NSMenuItem"; title = "Sparkle Test App Help"; ObjectID = "111"; */ "111.title" = "Sparkle Test App Help"; /* Class = "NSMenuItem"; title = "Revert"; ObjectID = "112"; */ "112.title" = "Revert"; /* Class = "NSMenuItem"; title = "Open Recent"; ObjectID = "124"; */ "124.title" = "Open Recent"; /* Class = "NSMenu"; title = "Open Recent"; ObjectID = "125"; */ "125.title" = "Open Recent"; /* Class = "NSMenuItem"; title = "Clear Menu"; ObjectID = "126"; */ "126.title" = "Clear Menu"; /* Class = "NSMenu"; title = "Services"; ObjectID = "130"; */ "130.title" = "Services"; /* Class = "NSMenuItem"; title = "Services"; ObjectID = "131"; */ "131.title" = "Services"; /* Class = "NSMenuItem"; title = "Hide Sparkle Test App"; ObjectID = "134"; */ "134.title" = "Hide Sparkle Test App"; /* Class = "NSMenuItem"; title = "Quit Sparkle Test App"; ObjectID = "136"; */ "136.title" = "Quit Sparkle Test App"; /* Class = "NSMenuItem"; title = "Hide Others"; ObjectID = "145"; */ "145.title" = "Hide Others"; /* Class = "NSMenuItem"; title = "Show All"; ObjectID = "150"; */ "150.title" = "Show All"; /* Class = "NSMenuItem"; title = "Find…"; ObjectID = "154"; */ "154.title" = "Find…"; /* Class = "NSMenuItem"; title = "Jump to Selection"; ObjectID = "155"; */ "155.title" = "Jump to Selection"; /* Class = "NSMenuItem"; title = "Copy"; ObjectID = "157"; */ "157.title" = "Copy"; /* Class = "NSMenuItem"; title = "Undo"; ObjectID = "158"; */ "158.title" = "Undo"; /* Class = "NSMenu"; title = "Find"; ObjectID = "159"; */ "159.title" = "Find"; /* Class = "NSMenuItem"; title = "Cut"; ObjectID = "160"; */ "160.title" = "Cut"; /* Class = "NSMenuItem"; title = "Use Selection for Find"; ObjectID = "161"; */ "161.title" = "Use Selection for Find"; /* Class = "NSMenuItem"; title = "Find Previous"; ObjectID = "162"; */ "162.title" = "Find Previous"; /* Class = "NSMenuItem"; title = "Edit"; ObjectID = "163"; */ "163.title" = "Edit"; /* Class = "NSMenuItem"; title = "Delete"; ObjectID = "164"; */ "164.title" = "Delete"; /* Class = "NSMenuItem"; title = "Find Next"; ObjectID = "167"; */ "167.title" = "Find Next"; /* Class = "NSMenuItem"; title = "Find"; ObjectID = "168"; */ "168.title" = "Find"; /* Class = "NSMenu"; title = "Edit"; ObjectID = "169"; */ "169.title" = "Edit"; /* Class = "NSMenuItem"; title = "Paste"; ObjectID = "171"; */ "171.title" = "Paste"; /* Class = "NSMenuItem"; title = "Select All"; ObjectID = "172"; */ "172.title" = "Select All"; /* Class = "NSMenuItem"; title = "Redo"; ObjectID = "173"; */ "173.title" = "Redo"; /* Class = "NSMenuItem"; title = "Spelling"; ObjectID = "184"; */ "184.title" = "Spelling"; /* Class = "NSMenu"; title = "Spelling"; ObjectID = "185"; */ "185.title" = "Spelling"; /* Class = "NSMenuItem"; title = "Spelling…"; ObjectID = "187"; */ "187.title" = "Spelling…"; /* Class = "NSMenuItem"; title = "Check Spelling"; ObjectID = "189"; */ "189.title" = "Check Spelling"; /* Class = "NSMenuItem"; title = "Check Spelling as You Type"; ObjectID = "191"; */ "191.title" = "Check Spelling as You Type"; /* Class = "NSMenuItem"; title = "Zoom"; ObjectID = "197"; */ "197.title" = "Zoom"; /* Class = "NSMenuItem"; title = "Paste and Match Style"; ObjectID = "204"; */ "204.title" = "Paste and Match Style"; /* Class = "NSMenuItem"; title = "Check for Updates…"; ObjectID = "207"; */ "207.title" = "Check for Updates…"; ================================================ FILE: TestApplication/ru.lproj/MainMenu.strings ================================================ /* Class = "NSMenuItem"; title = "Bring All to Front"; ObjectID = "5"; */ "5.title" = "Bring All to Front"; /* Class = "NSMenuItem"; title = "Window"; ObjectID = "19"; */ "19.title" = "Window"; /* Class = "NSMenuItem"; title = "Minimize"; ObjectID = "23"; */ "23.title" = "Minimize"; /* Class = "NSMenu"; title = "Window"; ObjectID = "24"; */ "24.title" = "Window"; /* Class = "NSMenu"; title = "MainMenu"; ObjectID = "29"; */ "29.title" = "MainMenu"; /* Class = "NSMenuItem"; title = "Sparkle Test App"; ObjectID = "56"; */ "56.title" = "Sparkle Test App"; /* Class = "NSMenu"; title = "Sparkle Test App"; ObjectID = "57"; */ "57.title" = "Sparkle Test App"; /* Class = "NSMenuItem"; title = "About Sparkle Test App"; ObjectID = "58"; */ "58.title" = "About Sparkle Test App"; /* Class = "NSMenuItem"; title = "Open..."; ObjectID = "72"; */ "72.title" = "Open..."; /* Class = "NSMenuItem"; title = "Close"; ObjectID = "73"; */ "73.title" = "Close"; /* Class = "NSMenuItem"; title = "Save"; ObjectID = "75"; */ "75.title" = "Save"; /* Class = "NSMenuItem"; title = "Page Setup…"; ObjectID = "77"; */ "77.title" = "Page Setup…"; /* Class = "NSMenuItem"; title = "Print…"; ObjectID = "78"; */ "78.title" = "Print…"; /* Class = "NSMenuItem"; title = "Save As…"; ObjectID = "80"; */ "80.title" = "Save As…"; /* Class = "NSMenu"; title = "File"; ObjectID = "81"; */ "81.title" = "File"; /* Class = "NSMenuItem"; title = "New"; ObjectID = "82"; */ "82.title" = "New"; /* Class = "NSMenuItem"; title = "File"; ObjectID = "83"; */ "83.title" = "File"; /* Class = "NSMenuItem"; title = "Help"; ObjectID = "103"; */ "103.title" = "Help"; /* Class = "NSMenu"; title = "Help"; ObjectID = "106"; */ "106.title" = "Help"; /* Class = "NSMenuItem"; title = "Sparkle Test App Help"; ObjectID = "111"; */ "111.title" = "Sparkle Test App Help"; /* Class = "NSMenuItem"; title = "Revert"; ObjectID = "112"; */ "112.title" = "Revert"; /* Class = "NSMenuItem"; title = "Open Recent"; ObjectID = "124"; */ "124.title" = "Open Recent"; /* Class = "NSMenu"; title = "Open Recent"; ObjectID = "125"; */ "125.title" = "Open Recent"; /* Class = "NSMenuItem"; title = "Clear Menu"; ObjectID = "126"; */ "126.title" = "Clear Menu"; /* Class = "NSMenu"; title = "Services"; ObjectID = "130"; */ "130.title" = "Services"; /* Class = "NSMenuItem"; title = "Services"; ObjectID = "131"; */ "131.title" = "Services"; /* Class = "NSMenuItem"; title = "Hide Sparkle Test App"; ObjectID = "134"; */ "134.title" = "Hide Sparkle Test App"; /* Class = "NSMenuItem"; title = "Quit Sparkle Test App"; ObjectID = "136"; */ "136.title" = "Quit Sparkle Test App"; /* Class = "NSMenuItem"; title = "Hide Others"; ObjectID = "145"; */ "145.title" = "Hide Others"; /* Class = "NSMenuItem"; title = "Show All"; ObjectID = "150"; */ "150.title" = "Show All"; /* Class = "NSMenuItem"; title = "Find…"; ObjectID = "154"; */ "154.title" = "Find…"; /* Class = "NSMenuItem"; title = "Jump to Selection"; ObjectID = "155"; */ "155.title" = "Jump to Selection"; /* Class = "NSMenuItem"; title = "Copy"; ObjectID = "157"; */ "157.title" = "Copy"; /* Class = "NSMenuItem"; title = "Undo"; ObjectID = "158"; */ "158.title" = "Undo"; /* Class = "NSMenu"; title = "Find"; ObjectID = "159"; */ "159.title" = "Find"; /* Class = "NSMenuItem"; title = "Cut"; ObjectID = "160"; */ "160.title" = "Cut"; /* Class = "NSMenuItem"; title = "Use Selection for Find"; ObjectID = "161"; */ "161.title" = "Use Selection for Find"; /* Class = "NSMenuItem"; title = "Find Previous"; ObjectID = "162"; */ "162.title" = "Find Previous"; /* Class = "NSMenuItem"; title = "Edit"; ObjectID = "163"; */ "163.title" = "Edit"; /* Class = "NSMenuItem"; title = "Delete"; ObjectID = "164"; */ "164.title" = "Delete"; /* Class = "NSMenuItem"; title = "Find Next"; ObjectID = "167"; */ "167.title" = "Find Next"; /* Class = "NSMenuItem"; title = "Find"; ObjectID = "168"; */ "168.title" = "Find"; /* Class = "NSMenu"; title = "Edit"; ObjectID = "169"; */ "169.title" = "Edit"; /* Class = "NSMenuItem"; title = "Paste"; ObjectID = "171"; */ "171.title" = "Paste"; /* Class = "NSMenuItem"; title = "Select All"; ObjectID = "172"; */ "172.title" = "Select All"; /* Class = "NSMenuItem"; title = "Redo"; ObjectID = "173"; */ "173.title" = "Redo"; /* Class = "NSMenuItem"; title = "Spelling"; ObjectID = "184"; */ "184.title" = "Spelling"; /* Class = "NSMenu"; title = "Spelling"; ObjectID = "185"; */ "185.title" = "Spelling"; /* Class = "NSMenuItem"; title = "Spelling…"; ObjectID = "187"; */ "187.title" = "Spelling…"; /* Class = "NSMenuItem"; title = "Check Spelling"; ObjectID = "189"; */ "189.title" = "Check Spelling"; /* Class = "NSMenuItem"; title = "Check Spelling as You Type"; ObjectID = "191"; */ "191.title" = "Check Spelling as You Type"; /* Class = "NSMenuItem"; title = "Zoom"; ObjectID = "197"; */ "197.title" = "Zoom"; /* Class = "NSMenuItem"; title = "Paste and Match Style"; ObjectID = "204"; */ "204.title" = "Paste and Match Style"; /* Class = "NSMenuItem"; title = "Check for Updates…"; ObjectID = "207"; */ "207.title" = "Check for Updates…"; ================================================ FILE: TestApplication/sk.lproj/MainMenu.strings ================================================ /* Class = "NSMenuItem"; title = "Bring All to Front"; ObjectID = "5"; */ "5.title" = "Bring All to Front"; /* Class = "NSMenuItem"; title = "Window"; ObjectID = "19"; */ "19.title" = "Window"; /* Class = "NSMenuItem"; title = "Minimize"; ObjectID = "23"; */ "23.title" = "Minimize"; /* Class = "NSMenu"; title = "Window"; ObjectID = "24"; */ "24.title" = "Window"; /* Class = "NSMenu"; title = "MainMenu"; ObjectID = "29"; */ "29.title" = "MainMenu"; /* Class = "NSMenuItem"; title = "Sparkle Test App"; ObjectID = "56"; */ "56.title" = "Sparkle Test App"; /* Class = "NSMenu"; title = "Sparkle Test App"; ObjectID = "57"; */ "57.title" = "Sparkle Test App"; /* Class = "NSMenuItem"; title = "About Sparkle Test App"; ObjectID = "58"; */ "58.title" = "About Sparkle Test App"; /* Class = "NSMenuItem"; title = "Open..."; ObjectID = "72"; */ "72.title" = "Open..."; /* Class = "NSMenuItem"; title = "Close"; ObjectID = "73"; */ "73.title" = "Close"; /* Class = "NSMenuItem"; title = "Save"; ObjectID = "75"; */ "75.title" = "Save"; /* Class = "NSMenuItem"; title = "Page Setup…"; ObjectID = "77"; */ "77.title" = "Page Setup…"; /* Class = "NSMenuItem"; title = "Print…"; ObjectID = "78"; */ "78.title" = "Print…"; /* Class = "NSMenuItem"; title = "Save As…"; ObjectID = "80"; */ "80.title" = "Save As…"; /* Class = "NSMenu"; title = "File"; ObjectID = "81"; */ "81.title" = "File"; /* Class = "NSMenuItem"; title = "New"; ObjectID = "82"; */ "82.title" = "New"; /* Class = "NSMenuItem"; title = "File"; ObjectID = "83"; */ "83.title" = "File"; /* Class = "NSMenuItem"; title = "Help"; ObjectID = "103"; */ "103.title" = "Help"; /* Class = "NSMenu"; title = "Help"; ObjectID = "106"; */ "106.title" = "Help"; /* Class = "NSMenuItem"; title = "Sparkle Test App Help"; ObjectID = "111"; */ "111.title" = "Sparkle Test App Help"; /* Class = "NSMenuItem"; title = "Revert"; ObjectID = "112"; */ "112.title" = "Revert"; /* Class = "NSMenuItem"; title = "Open Recent"; ObjectID = "124"; */ "124.title" = "Open Recent"; /* Class = "NSMenu"; title = "Open Recent"; ObjectID = "125"; */ "125.title" = "Open Recent"; /* Class = "NSMenuItem"; title = "Clear Menu"; ObjectID = "126"; */ "126.title" = "Clear Menu"; /* Class = "NSMenu"; title = "Services"; ObjectID = "130"; */ "130.title" = "Services"; /* Class = "NSMenuItem"; title = "Services"; ObjectID = "131"; */ "131.title" = "Services"; /* Class = "NSMenuItem"; title = "Hide Sparkle Test App"; ObjectID = "134"; */ "134.title" = "Hide Sparkle Test App"; /* Class = "NSMenuItem"; title = "Quit Sparkle Test App"; ObjectID = "136"; */ "136.title" = "Quit Sparkle Test App"; /* Class = "NSMenuItem"; title = "Hide Others"; ObjectID = "145"; */ "145.title" = "Hide Others"; /* Class = "NSMenuItem"; title = "Show All"; ObjectID = "150"; */ "150.title" = "Show All"; /* Class = "NSMenuItem"; title = "Find…"; ObjectID = "154"; */ "154.title" = "Find…"; /* Class = "NSMenuItem"; title = "Jump to Selection"; ObjectID = "155"; */ "155.title" = "Jump to Selection"; /* Class = "NSMenuItem"; title = "Copy"; ObjectID = "157"; */ "157.title" = "Copy"; /* Class = "NSMenuItem"; title = "Undo"; ObjectID = "158"; */ "158.title" = "Undo"; /* Class = "NSMenu"; title = "Find"; ObjectID = "159"; */ "159.title" = "Find"; /* Class = "NSMenuItem"; title = "Cut"; ObjectID = "160"; */ "160.title" = "Cut"; /* Class = "NSMenuItem"; title = "Use Selection for Find"; ObjectID = "161"; */ "161.title" = "Use Selection for Find"; /* Class = "NSMenuItem"; title = "Find Previous"; ObjectID = "162"; */ "162.title" = "Find Previous"; /* Class = "NSMenuItem"; title = "Edit"; ObjectID = "163"; */ "163.title" = "Edit"; /* Class = "NSMenuItem"; title = "Delete"; ObjectID = "164"; */ "164.title" = "Delete"; /* Class = "NSMenuItem"; title = "Find Next"; ObjectID = "167"; */ "167.title" = "Find Next"; /* Class = "NSMenuItem"; title = "Find"; ObjectID = "168"; */ "168.title" = "Find"; /* Class = "NSMenu"; title = "Edit"; ObjectID = "169"; */ "169.title" = "Edit"; /* Class = "NSMenuItem"; title = "Paste"; ObjectID = "171"; */ "171.title" = "Paste"; /* Class = "NSMenuItem"; title = "Select All"; ObjectID = "172"; */ "172.title" = "Select All"; /* Class = "NSMenuItem"; title = "Redo"; ObjectID = "173"; */ "173.title" = "Redo"; /* Class = "NSMenuItem"; title = "Spelling"; ObjectID = "184"; */ "184.title" = "Spelling"; /* Class = "NSMenu"; title = "Spelling"; ObjectID = "185"; */ "185.title" = "Spelling"; /* Class = "NSMenuItem"; title = "Spelling…"; ObjectID = "187"; */ "187.title" = "Spelling…"; /* Class = "NSMenuItem"; title = "Check Spelling"; ObjectID = "189"; */ "189.title" = "Check Spelling"; /* Class = "NSMenuItem"; title = "Check Spelling as You Type"; ObjectID = "191"; */ "191.title" = "Check Spelling as You Type"; /* Class = "NSMenuItem"; title = "Zoom"; ObjectID = "197"; */ "197.title" = "Zoom"; /* Class = "NSMenuItem"; title = "Paste and Match Style"; ObjectID = "204"; */ "204.title" = "Paste and Match Style"; /* Class = "NSMenuItem"; title = "Check for Updates…"; ObjectID = "207"; */ "207.title" = "Check for Updates…"; ================================================ FILE: TestApplication/sl.lproj/MainMenu.strings ================================================ /* Class = "NSMenuItem"; title = "Bring All to Front"; ObjectID = "5"; */ "5.title" = "Bring All to Front"; /* Class = "NSMenuItem"; title = "Window"; ObjectID = "19"; */ "19.title" = "Window"; /* Class = "NSMenuItem"; title = "Minimize"; ObjectID = "23"; */ "23.title" = "Minimize"; /* Class = "NSMenu"; title = "Window"; ObjectID = "24"; */ "24.title" = "Window"; /* Class = "NSMenu"; title = "MainMenu"; ObjectID = "29"; */ "29.title" = "MainMenu"; /* Class = "NSMenuItem"; title = "Sparkle Test App"; ObjectID = "56"; */ "56.title" = "Sparkle Test App"; /* Class = "NSMenu"; title = "Sparkle Test App"; ObjectID = "57"; */ "57.title" = "Sparkle Test App"; /* Class = "NSMenuItem"; title = "About Sparkle Test App"; ObjectID = "58"; */ "58.title" = "About Sparkle Test App"; /* Class = "NSMenuItem"; title = "Open..."; ObjectID = "72"; */ "72.title" = "Open..."; /* Class = "NSMenuItem"; title = "Close"; ObjectID = "73"; */ "73.title" = "Close"; /* Class = "NSMenuItem"; title = "Save"; ObjectID = "75"; */ "75.title" = "Save"; /* Class = "NSMenuItem"; title = "Page Setup…"; ObjectID = "77"; */ "77.title" = "Page Setup…"; /* Class = "NSMenuItem"; title = "Print…"; ObjectID = "78"; */ "78.title" = "Print…"; /* Class = "NSMenuItem"; title = "Save As…"; ObjectID = "80"; */ "80.title" = "Save As…"; /* Class = "NSMenu"; title = "File"; ObjectID = "81"; */ "81.title" = "File"; /* Class = "NSMenuItem"; title = "New"; ObjectID = "82"; */ "82.title" = "New"; /* Class = "NSMenuItem"; title = "File"; ObjectID = "83"; */ "83.title" = "File"; /* Class = "NSMenuItem"; title = "Help"; ObjectID = "103"; */ "103.title" = "Help"; /* Class = "NSMenu"; title = "Help"; ObjectID = "106"; */ "106.title" = "Help"; /* Class = "NSMenuItem"; title = "Sparkle Test App Help"; ObjectID = "111"; */ "111.title" = "Sparkle Test App Help"; /* Class = "NSMenuItem"; title = "Revert"; ObjectID = "112"; */ "112.title" = "Revert"; /* Class = "NSMenuItem"; title = "Open Recent"; ObjectID = "124"; */ "124.title" = "Open Recent"; /* Class = "NSMenu"; title = "Open Recent"; ObjectID = "125"; */ "125.title" = "Open Recent"; /* Class = "NSMenuItem"; title = "Clear Menu"; ObjectID = "126"; */ "126.title" = "Clear Menu"; /* Class = "NSMenu"; title = "Services"; ObjectID = "130"; */ "130.title" = "Services"; /* Class = "NSMenuItem"; title = "Services"; ObjectID = "131"; */ "131.title" = "Services"; /* Class = "NSMenuItem"; title = "Hide Sparkle Test App"; ObjectID = "134"; */ "134.title" = "Hide Sparkle Test App"; /* Class = "NSMenuItem"; title = "Quit Sparkle Test App"; ObjectID = "136"; */ "136.title" = "Quit Sparkle Test App"; /* Class = "NSMenuItem"; title = "Hide Others"; ObjectID = "145"; */ "145.title" = "Hide Others"; /* Class = "NSMenuItem"; title = "Show All"; ObjectID = "150"; */ "150.title" = "Show All"; /* Class = "NSMenuItem"; title = "Find…"; ObjectID = "154"; */ "154.title" = "Find…"; /* Class = "NSMenuItem"; title = "Jump to Selection"; ObjectID = "155"; */ "155.title" = "Jump to Selection"; /* Class = "NSMenuItem"; title = "Copy"; ObjectID = "157"; */ "157.title" = "Copy"; /* Class = "NSMenuItem"; title = "Undo"; ObjectID = "158"; */ "158.title" = "Undo"; /* Class = "NSMenu"; title = "Find"; ObjectID = "159"; */ "159.title" = "Find"; /* Class = "NSMenuItem"; title = "Cut"; ObjectID = "160"; */ "160.title" = "Cut"; /* Class = "NSMenuItem"; title = "Use Selection for Find"; ObjectID = "161"; */ "161.title" = "Use Selection for Find"; /* Class = "NSMenuItem"; title = "Find Previous"; ObjectID = "162"; */ "162.title" = "Find Previous"; /* Class = "NSMenuItem"; title = "Edit"; ObjectID = "163"; */ "163.title" = "Edit"; /* Class = "NSMenuItem"; title = "Delete"; ObjectID = "164"; */ "164.title" = "Delete"; /* Class = "NSMenuItem"; title = "Find Next"; ObjectID = "167"; */ "167.title" = "Find Next"; /* Class = "NSMenuItem"; title = "Find"; ObjectID = "168"; */ "168.title" = "Find"; /* Class = "NSMenu"; title = "Edit"; ObjectID = "169"; */ "169.title" = "Edit"; /* Class = "NSMenuItem"; title = "Paste"; ObjectID = "171"; */ "171.title" = "Paste"; /* Class = "NSMenuItem"; title = "Select All"; ObjectID = "172"; */ "172.title" = "Select All"; /* Class = "NSMenuItem"; title = "Redo"; ObjectID = "173"; */ "173.title" = "Redo"; /* Class = "NSMenuItem"; title = "Spelling"; ObjectID = "184"; */ "184.title" = "Spelling"; /* Class = "NSMenu"; title = "Spelling"; ObjectID = "185"; */ "185.title" = "Spelling"; /* Class = "NSMenuItem"; title = "Spelling…"; ObjectID = "187"; */ "187.title" = "Spelling…"; /* Class = "NSMenuItem"; title = "Check Spelling"; ObjectID = "189"; */ "189.title" = "Check Spelling"; /* Class = "NSMenuItem"; title = "Check Spelling as You Type"; ObjectID = "191"; */ "191.title" = "Check Spelling as You Type"; /* Class = "NSMenuItem"; title = "Zoom"; ObjectID = "197"; */ "197.title" = "Zoom"; /* Class = "NSMenuItem"; title = "Paste and Match Style"; ObjectID = "204"; */ "204.title" = "Paste and Match Style"; /* Class = "NSMenuItem"; title = "Check for Updates…"; ObjectID = "207"; */ "207.title" = "Check for Updates…"; ================================================ FILE: TestApplication/sparkletestcast.xml ================================================ Sparkle Test App Changelog Most recent changes with links to updates. en Version 2.2 2.2 https://sparkle-project.org automatic releasenotes.html
  • Lorem ipsum dolor sit amet, consectetur adipiscing elit.
  • ]]>
    Sat, 26 Jul 2014 15:20:11 +0000
    Version 2.1 2.1 https://sparkle-project.org delta releasenotes.html Markdown is also useful for applications that cannot embed WKWebView directly, such as apps without a download client entitlement, or Catalyst applications. > >> In quotes you can also have nested quotes, and the lines in nested quotes can be long enough to span over the next few lines, assuming that the lines are long enough. >> >> And that is how nested quotes can work. A known limitation is properly styling lists in blockquotes so I'd suggest to avoid that. > After this paragraph, you should see extra spacing from using a line break. --- Code blocks from markdown are also supported using a monospace font: ```sh me@Compy ~/D/P/Sparkle> ls Autoupdate/ Carthage-dev.json Configurations/ generate_appcast/ InstallerLauncher/ Package.swift Sparkle/ TestAppHelper/ Vendor/ bin/ CHANGELOG DerivedData/ generate_keys/ InstallerStatus/ README.markdown sparkle-cli/ TestApplication/ BinaryDelta/ CODE_OF_CONDUCT.md Documentation/ INSTALL LICENSE Resources/ Sparkle.podspec Tests/ build/ common_cli/ Downloader/ InstallerConnection/ Makefile sign_update/ Sparkle.xcodeproj/ UITests/ ``` Note though, Sparkle's markdown support does not support tables or images. It needs to work without loading additional resources and without switching to TextKit 1. ## Version 2.0 * **Redesigned Interface**: Enjoy a refreshed, modern UI that improves navigation and streamlines workflows across the app. * **Cloud Sync Support**: Projects and settings can now be synced securely across multiple devices using your account. * **Customizable Shortcuts**: Create and manage keyboard shortcuts for almost any action. * Improved launch performance and reduced memory usage during startup. * Fixed an issue where notifications could appear twice in some cases. * Addressed several minor visual glitches in dark mode.]]> Sat, 26 Jul 2014 15:20:11 +0000 Version 2.0 2.0 https://sparkle-project.org releasenotes.html strong { font-weight: 600; } ul li { margin-bottom: 0.2em; }

    Version 2.0

    • Redesigned Interface: Enjoy a refreshed, modern UI that improves navigation and streamlines workflows across the app.
    • Cloud Sync Support: Projects and settings can now be synced securely across multiple devices using your account.
    • Customizable Shortcuts: Create and manage keyboard shortcuts for almost any action.
    • Improved launch performance and reduced memory usage during startup.
    • Fixed an issue where notifications could appear twice in some cases.
    • Addressed several minor visual glitches in dark mode.
    ]]>
    Sat, 26 Jul 2014 15:20:11 +0000
    ================================================ FILE: TestApplication/sv.lproj/MainMenu.strings ================================================ /* Class = "NSMenuItem"; title = "Bring All to Front"; ObjectID = "5"; */ "5.title" = "Bring All to Front"; /* Class = "NSMenuItem"; title = "Window"; ObjectID = "19"; */ "19.title" = "Window"; /* Class = "NSMenuItem"; title = "Minimize"; ObjectID = "23"; */ "23.title" = "Minimize"; /* Class = "NSMenu"; title = "Window"; ObjectID = "24"; */ "24.title" = "Window"; /* Class = "NSMenu"; title = "MainMenu"; ObjectID = "29"; */ "29.title" = "MainMenu"; /* Class = "NSMenuItem"; title = "Sparkle Test App"; ObjectID = "56"; */ "56.title" = "Sparkle Test App"; /* Class = "NSMenu"; title = "Sparkle Test App"; ObjectID = "57"; */ "57.title" = "Sparkle Test App"; /* Class = "NSMenuItem"; title = "About Sparkle Test App"; ObjectID = "58"; */ "58.title" = "About Sparkle Test App"; /* Class = "NSMenuItem"; title = "Open..."; ObjectID = "72"; */ "72.title" = "Open..."; /* Class = "NSMenuItem"; title = "Close"; ObjectID = "73"; */ "73.title" = "Close"; /* Class = "NSMenuItem"; title = "Save"; ObjectID = "75"; */ "75.title" = "Save"; /* Class = "NSMenuItem"; title = "Page Setup…"; ObjectID = "77"; */ "77.title" = "Page Setup…"; /* Class = "NSMenuItem"; title = "Print…"; ObjectID = "78"; */ "78.title" = "Print…"; /* Class = "NSMenuItem"; title = "Save As…"; ObjectID = "80"; */ "80.title" = "Save As…"; /* Class = "NSMenu"; title = "File"; ObjectID = "81"; */ "81.title" = "File"; /* Class = "NSMenuItem"; title = "New"; ObjectID = "82"; */ "82.title" = "New"; /* Class = "NSMenuItem"; title = "File"; ObjectID = "83"; */ "83.title" = "File"; /* Class = "NSMenuItem"; title = "Help"; ObjectID = "103"; */ "103.title" = "Help"; /* Class = "NSMenu"; title = "Help"; ObjectID = "106"; */ "106.title" = "Help"; /* Class = "NSMenuItem"; title = "Sparkle Test App Help"; ObjectID = "111"; */ "111.title" = "Sparkle Test App Help"; /* Class = "NSMenuItem"; title = "Revert"; ObjectID = "112"; */ "112.title" = "Revert"; /* Class = "NSMenuItem"; title = "Open Recent"; ObjectID = "124"; */ "124.title" = "Open Recent"; /* Class = "NSMenu"; title = "Open Recent"; ObjectID = "125"; */ "125.title" = "Open Recent"; /* Class = "NSMenuItem"; title = "Clear Menu"; ObjectID = "126"; */ "126.title" = "Clear Menu"; /* Class = "NSMenu"; title = "Services"; ObjectID = "130"; */ "130.title" = "Services"; /* Class = "NSMenuItem"; title = "Services"; ObjectID = "131"; */ "131.title" = "Services"; /* Class = "NSMenuItem"; title = "Hide Sparkle Test App"; ObjectID = "134"; */ "134.title" = "Hide Sparkle Test App"; /* Class = "NSMenuItem"; title = "Quit Sparkle Test App"; ObjectID = "136"; */ "136.title" = "Quit Sparkle Test App"; /* Class = "NSMenuItem"; title = "Hide Others"; ObjectID = "145"; */ "145.title" = "Hide Others"; /* Class = "NSMenuItem"; title = "Show All"; ObjectID = "150"; */ "150.title" = "Show All"; /* Class = "NSMenuItem"; title = "Find…"; ObjectID = "154"; */ "154.title" = "Find…"; /* Class = "NSMenuItem"; title = "Jump to Selection"; ObjectID = "155"; */ "155.title" = "Jump to Selection"; /* Class = "NSMenuItem"; title = "Copy"; ObjectID = "157"; */ "157.title" = "Copy"; /* Class = "NSMenuItem"; title = "Undo"; ObjectID = "158"; */ "158.title" = "Undo"; /* Class = "NSMenu"; title = "Find"; ObjectID = "159"; */ "159.title" = "Find"; /* Class = "NSMenuItem"; title = "Cut"; ObjectID = "160"; */ "160.title" = "Cut"; /* Class = "NSMenuItem"; title = "Use Selection for Find"; ObjectID = "161"; */ "161.title" = "Use Selection for Find"; /* Class = "NSMenuItem"; title = "Find Previous"; ObjectID = "162"; */ "162.title" = "Find Previous"; /* Class = "NSMenuItem"; title = "Edit"; ObjectID = "163"; */ "163.title" = "Edit"; /* Class = "NSMenuItem"; title = "Delete"; ObjectID = "164"; */ "164.title" = "Delete"; /* Class = "NSMenuItem"; title = "Find Next"; ObjectID = "167"; */ "167.title" = "Find Next"; /* Class = "NSMenuItem"; title = "Find"; ObjectID = "168"; */ "168.title" = "Find"; /* Class = "NSMenu"; title = "Edit"; ObjectID = "169"; */ "169.title" = "Edit"; /* Class = "NSMenuItem"; title = "Paste"; ObjectID = "171"; */ "171.title" = "Paste"; /* Class = "NSMenuItem"; title = "Select All"; ObjectID = "172"; */ "172.title" = "Select All"; /* Class = "NSMenuItem"; title = "Redo"; ObjectID = "173"; */ "173.title" = "Redo"; /* Class = "NSMenuItem"; title = "Spelling"; ObjectID = "184"; */ "184.title" = "Spelling"; /* Class = "NSMenu"; title = "Spelling"; ObjectID = "185"; */ "185.title" = "Spelling"; /* Class = "NSMenuItem"; title = "Spelling…"; ObjectID = "187"; */ "187.title" = "Spelling…"; /* Class = "NSMenuItem"; title = "Check Spelling"; ObjectID = "189"; */ "189.title" = "Check Spelling"; /* Class = "NSMenuItem"; title = "Check Spelling as You Type"; ObjectID = "191"; */ "191.title" = "Check Spelling as You Type"; /* Class = "NSMenuItem"; title = "Zoom"; ObjectID = "197"; */ "197.title" = "Zoom"; /* Class = "NSMenuItem"; title = "Paste and Match Style"; ObjectID = "204"; */ "204.title" = "Paste and Match Style"; /* Class = "NSMenuItem"; title = "Check for Updates…"; ObjectID = "207"; */ "207.title" = "Check for Updates…"; ================================================ FILE: TestApplication/th.lproj/MainMenu.strings ================================================ /* Class = "NSMenuItem"; title = "Bring All to Front"; ObjectID = "5"; */ "5.title" = "Bring All to Front"; /* Class = "NSMenuItem"; title = "Window"; ObjectID = "19"; */ "19.title" = "Window"; /* Class = "NSMenuItem"; title = "Minimize"; ObjectID = "23"; */ "23.title" = "Minimize"; /* Class = "NSMenu"; title = "Window"; ObjectID = "24"; */ "24.title" = "Window"; /* Class = "NSMenu"; title = "MainMenu"; ObjectID = "29"; */ "29.title" = "MainMenu"; /* Class = "NSMenuItem"; title = "Sparkle Test App"; ObjectID = "56"; */ "56.title" = "Sparkle Test App"; /* Class = "NSMenu"; title = "Sparkle Test App"; ObjectID = "57"; */ "57.title" = "Sparkle Test App"; /* Class = "NSMenuItem"; title = "About Sparkle Test App"; ObjectID = "58"; */ "58.title" = "About Sparkle Test App"; /* Class = "NSMenuItem"; title = "Open..."; ObjectID = "72"; */ "72.title" = "Open..."; /* Class = "NSMenuItem"; title = "Close"; ObjectID = "73"; */ "73.title" = "Close"; /* Class = "NSMenuItem"; title = "Save"; ObjectID = "75"; */ "75.title" = "Save"; /* Class = "NSMenuItem"; title = "Page Setup…"; ObjectID = "77"; */ "77.title" = "Page Setup…"; /* Class = "NSMenuItem"; title = "Print…"; ObjectID = "78"; */ "78.title" = "Print…"; /* Class = "NSMenuItem"; title = "Save As…"; ObjectID = "80"; */ "80.title" = "Save As…"; /* Class = "NSMenu"; title = "File"; ObjectID = "81"; */ "81.title" = "File"; /* Class = "NSMenuItem"; title = "New"; ObjectID = "82"; */ "82.title" = "New"; /* Class = "NSMenuItem"; title = "File"; ObjectID = "83"; */ "83.title" = "File"; /* Class = "NSMenuItem"; title = "Help"; ObjectID = "103"; */ "103.title" = "Help"; /* Class = "NSMenu"; title = "Help"; ObjectID = "106"; */ "106.title" = "Help"; /* Class = "NSMenuItem"; title = "Sparkle Test App Help"; ObjectID = "111"; */ "111.title" = "Sparkle Test App Help"; /* Class = "NSMenuItem"; title = "Revert"; ObjectID = "112"; */ "112.title" = "Revert"; /* Class = "NSMenuItem"; title = "Open Recent"; ObjectID = "124"; */ "124.title" = "Open Recent"; /* Class = "NSMenu"; title = "Open Recent"; ObjectID = "125"; */ "125.title" = "Open Recent"; /* Class = "NSMenuItem"; title = "Clear Menu"; ObjectID = "126"; */ "126.title" = "Clear Menu"; /* Class = "NSMenu"; title = "Services"; ObjectID = "130"; */ "130.title" = "Services"; /* Class = "NSMenuItem"; title = "Services"; ObjectID = "131"; */ "131.title" = "Services"; /* Class = "NSMenuItem"; title = "Hide Sparkle Test App"; ObjectID = "134"; */ "134.title" = "Hide Sparkle Test App"; /* Class = "NSMenuItem"; title = "Quit Sparkle Test App"; ObjectID = "136"; */ "136.title" = "Quit Sparkle Test App"; /* Class = "NSMenuItem"; title = "Hide Others"; ObjectID = "145"; */ "145.title" = "Hide Others"; /* Class = "NSMenuItem"; title = "Show All"; ObjectID = "150"; */ "150.title" = "Show All"; /* Class = "NSMenuItem"; title = "Find…"; ObjectID = "154"; */ "154.title" = "Find…"; /* Class = "NSMenuItem"; title = "Jump to Selection"; ObjectID = "155"; */ "155.title" = "Jump to Selection"; /* Class = "NSMenuItem"; title = "Copy"; ObjectID = "157"; */ "157.title" = "Copy"; /* Class = "NSMenuItem"; title = "Undo"; ObjectID = "158"; */ "158.title" = "Undo"; /* Class = "NSMenu"; title = "Find"; ObjectID = "159"; */ "159.title" = "Find"; /* Class = "NSMenuItem"; title = "Cut"; ObjectID = "160"; */ "160.title" = "Cut"; /* Class = "NSMenuItem"; title = "Use Selection for Find"; ObjectID = "161"; */ "161.title" = "Use Selection for Find"; /* Class = "NSMenuItem"; title = "Find Previous"; ObjectID = "162"; */ "162.title" = "Find Previous"; /* Class = "NSMenuItem"; title = "Edit"; ObjectID = "163"; */ "163.title" = "Edit"; /* Class = "NSMenuItem"; title = "Delete"; ObjectID = "164"; */ "164.title" = "Delete"; /* Class = "NSMenuItem"; title = "Find Next"; ObjectID = "167"; */ "167.title" = "Find Next"; /* Class = "NSMenuItem"; title = "Find"; ObjectID = "168"; */ "168.title" = "Find"; /* Class = "NSMenu"; title = "Edit"; ObjectID = "169"; */ "169.title" = "Edit"; /* Class = "NSMenuItem"; title = "Paste"; ObjectID = "171"; */ "171.title" = "Paste"; /* Class = "NSMenuItem"; title = "Select All"; ObjectID = "172"; */ "172.title" = "Select All"; /* Class = "NSMenuItem"; title = "Redo"; ObjectID = "173"; */ "173.title" = "Redo"; /* Class = "NSMenuItem"; title = "Spelling"; ObjectID = "184"; */ "184.title" = "Spelling"; /* Class = "NSMenu"; title = "Spelling"; ObjectID = "185"; */ "185.title" = "Spelling"; /* Class = "NSMenuItem"; title = "Spelling…"; ObjectID = "187"; */ "187.title" = "Spelling…"; /* Class = "NSMenuItem"; title = "Check Spelling"; ObjectID = "189"; */ "189.title" = "Check Spelling"; /* Class = "NSMenuItem"; title = "Check Spelling as You Type"; ObjectID = "191"; */ "191.title" = "Check Spelling as You Type"; /* Class = "NSMenuItem"; title = "Zoom"; ObjectID = "197"; */ "197.title" = "Zoom"; /* Class = "NSMenuItem"; title = "Paste and Match Style"; ObjectID = "204"; */ "204.title" = "Paste and Match Style"; /* Class = "NSMenuItem"; title = "Check for Updates…"; ObjectID = "207"; */ "207.title" = "Check for Updates…"; ================================================ FILE: TestApplication/tr.lproj/MainMenu.strings ================================================ /* Class = "NSMenuItem"; title = "Bring All to Front"; ObjectID = "5"; */ "5.title" = "Bring All to Front"; /* Class = "NSMenuItem"; title = "Window"; ObjectID = "19"; */ "19.title" = "Window"; /* Class = "NSMenuItem"; title = "Minimize"; ObjectID = "23"; */ "23.title" = "Minimize"; /* Class = "NSMenu"; title = "Window"; ObjectID = "24"; */ "24.title" = "Window"; /* Class = "NSMenu"; title = "MainMenu"; ObjectID = "29"; */ "29.title" = "MainMenu"; /* Class = "NSMenuItem"; title = "Sparkle Test App"; ObjectID = "56"; */ "56.title" = "Sparkle Test App"; /* Class = "NSMenu"; title = "Sparkle Test App"; ObjectID = "57"; */ "57.title" = "Sparkle Test App"; /* Class = "NSMenuItem"; title = "About Sparkle Test App"; ObjectID = "58"; */ "58.title" = "About Sparkle Test App"; /* Class = "NSMenuItem"; title = "Open..."; ObjectID = "72"; */ "72.title" = "Open..."; /* Class = "NSMenuItem"; title = "Close"; ObjectID = "73"; */ "73.title" = "Close"; /* Class = "NSMenuItem"; title = "Save"; ObjectID = "75"; */ "75.title" = "Save"; /* Class = "NSMenuItem"; title = "Page Setup…"; ObjectID = "77"; */ "77.title" = "Page Setup…"; /* Class = "NSMenuItem"; title = "Print…"; ObjectID = "78"; */ "78.title" = "Print…"; /* Class = "NSMenuItem"; title = "Save As…"; ObjectID = "80"; */ "80.title" = "Save As…"; /* Class = "NSMenu"; title = "File"; ObjectID = "81"; */ "81.title" = "File"; /* Class = "NSMenuItem"; title = "New"; ObjectID = "82"; */ "82.title" = "New"; /* Class = "NSMenuItem"; title = "File"; ObjectID = "83"; */ "83.title" = "File"; /* Class = "NSMenuItem"; title = "Help"; ObjectID = "103"; */ "103.title" = "Help"; /* Class = "NSMenu"; title = "Help"; ObjectID = "106"; */ "106.title" = "Help"; /* Class = "NSMenuItem"; title = "Sparkle Test App Help"; ObjectID = "111"; */ "111.title" = "Sparkle Test App Help"; /* Class = "NSMenuItem"; title = "Revert"; ObjectID = "112"; */ "112.title" = "Revert"; /* Class = "NSMenuItem"; title = "Open Recent"; ObjectID = "124"; */ "124.title" = "Open Recent"; /* Class = "NSMenu"; title = "Open Recent"; ObjectID = "125"; */ "125.title" = "Open Recent"; /* Class = "NSMenuItem"; title = "Clear Menu"; ObjectID = "126"; */ "126.title" = "Clear Menu"; /* Class = "NSMenu"; title = "Services"; ObjectID = "130"; */ "130.title" = "Services"; /* Class = "NSMenuItem"; title = "Services"; ObjectID = "131"; */ "131.title" = "Services"; /* Class = "NSMenuItem"; title = "Hide Sparkle Test App"; ObjectID = "134"; */ "134.title" = "Hide Sparkle Test App"; /* Class = "NSMenuItem"; title = "Quit Sparkle Test App"; ObjectID = "136"; */ "136.title" = "Quit Sparkle Test App"; /* Class = "NSMenuItem"; title = "Hide Others"; ObjectID = "145"; */ "145.title" = "Hide Others"; /* Class = "NSMenuItem"; title = "Show All"; ObjectID = "150"; */ "150.title" = "Show All"; /* Class = "NSMenuItem"; title = "Find…"; ObjectID = "154"; */ "154.title" = "Find…"; /* Class = "NSMenuItem"; title = "Jump to Selection"; ObjectID = "155"; */ "155.title" = "Jump to Selection"; /* Class = "NSMenuItem"; title = "Copy"; ObjectID = "157"; */ "157.title" = "Copy"; /* Class = "NSMenuItem"; title = "Undo"; ObjectID = "158"; */ "158.title" = "Undo"; /* Class = "NSMenu"; title = "Find"; ObjectID = "159"; */ "159.title" = "Find"; /* Class = "NSMenuItem"; title = "Cut"; ObjectID = "160"; */ "160.title" = "Cut"; /* Class = "NSMenuItem"; title = "Use Selection for Find"; ObjectID = "161"; */ "161.title" = "Use Selection for Find"; /* Class = "NSMenuItem"; title = "Find Previous"; ObjectID = "162"; */ "162.title" = "Find Previous"; /* Class = "NSMenuItem"; title = "Edit"; ObjectID = "163"; */ "163.title" = "Edit"; /* Class = "NSMenuItem"; title = "Delete"; ObjectID = "164"; */ "164.title" = "Delete"; /* Class = "NSMenuItem"; title = "Find Next"; ObjectID = "167"; */ "167.title" = "Find Next"; /* Class = "NSMenuItem"; title = "Find"; ObjectID = "168"; */ "168.title" = "Find"; /* Class = "NSMenu"; title = "Edit"; ObjectID = "169"; */ "169.title" = "Edit"; /* Class = "NSMenuItem"; title = "Paste"; ObjectID = "171"; */ "171.title" = "Paste"; /* Class = "NSMenuItem"; title = "Select All"; ObjectID = "172"; */ "172.title" = "Select All"; /* Class = "NSMenuItem"; title = "Redo"; ObjectID = "173"; */ "173.title" = "Redo"; /* Class = "NSMenuItem"; title = "Spelling"; ObjectID = "184"; */ "184.title" = "Spelling"; /* Class = "NSMenu"; title = "Spelling"; ObjectID = "185"; */ "185.title" = "Spelling"; /* Class = "NSMenuItem"; title = "Spelling…"; ObjectID = "187"; */ "187.title" = "Spelling…"; /* Class = "NSMenuItem"; title = "Check Spelling"; ObjectID = "189"; */ "189.title" = "Check Spelling"; /* Class = "NSMenuItem"; title = "Check Spelling as You Type"; ObjectID = "191"; */ "191.title" = "Check Spelling as You Type"; /* Class = "NSMenuItem"; title = "Zoom"; ObjectID = "197"; */ "197.title" = "Zoom"; /* Class = "NSMenuItem"; title = "Paste and Match Style"; ObjectID = "204"; */ "204.title" = "Paste and Match Style"; /* Class = "NSMenuItem"; title = "Check for Updates…"; ObjectID = "207"; */ "207.title" = "Check for Updates…"; ================================================ FILE: TestApplication/uk.lproj/MainMenu.strings ================================================ /* Class = "NSMenuItem"; title = "Bring All to Front"; ObjectID = "5"; */ "5.title" = "Bring All to Front"; /* Class = "NSMenuItem"; title = "Window"; ObjectID = "19"; */ "19.title" = "Window"; /* Class = "NSMenuItem"; title = "Minimize"; ObjectID = "23"; */ "23.title" = "Minimize"; /* Class = "NSMenu"; title = "Window"; ObjectID = "24"; */ "24.title" = "Window"; /* Class = "NSMenu"; title = "MainMenu"; ObjectID = "29"; */ "29.title" = "MainMenu"; /* Class = "NSMenuItem"; title = "Sparkle Test App"; ObjectID = "56"; */ "56.title" = "Sparkle Test App"; /* Class = "NSMenu"; title = "Sparkle Test App"; ObjectID = "57"; */ "57.title" = "Sparkle Test App"; /* Class = "NSMenuItem"; title = "About Sparkle Test App"; ObjectID = "58"; */ "58.title" = "About Sparkle Test App"; /* Class = "NSMenuItem"; title = "Open..."; ObjectID = "72"; */ "72.title" = "Open..."; /* Class = "NSMenuItem"; title = "Close"; ObjectID = "73"; */ "73.title" = "Close"; /* Class = "NSMenuItem"; title = "Save"; ObjectID = "75"; */ "75.title" = "Save"; /* Class = "NSMenuItem"; title = "Page Setup…"; ObjectID = "77"; */ "77.title" = "Page Setup…"; /* Class = "NSMenuItem"; title = "Print…"; ObjectID = "78"; */ "78.title" = "Print…"; /* Class = "NSMenuItem"; title = "Save As…"; ObjectID = "80"; */ "80.title" = "Save As…"; /* Class = "NSMenu"; title = "File"; ObjectID = "81"; */ "81.title" = "File"; /* Class = "NSMenuItem"; title = "New"; ObjectID = "82"; */ "82.title" = "New"; /* Class = "NSMenuItem"; title = "File"; ObjectID = "83"; */ "83.title" = "File"; /* Class = "NSMenuItem"; title = "Help"; ObjectID = "103"; */ "103.title" = "Help"; /* Class = "NSMenu"; title = "Help"; ObjectID = "106"; */ "106.title" = "Help"; /* Class = "NSMenuItem"; title = "Sparkle Test App Help"; ObjectID = "111"; */ "111.title" = "Sparkle Test App Help"; /* Class = "NSMenuItem"; title = "Revert"; ObjectID = "112"; */ "112.title" = "Revert"; /* Class = "NSMenuItem"; title = "Open Recent"; ObjectID = "124"; */ "124.title" = "Open Recent"; /* Class = "NSMenu"; title = "Open Recent"; ObjectID = "125"; */ "125.title" = "Open Recent"; /* Class = "NSMenuItem"; title = "Clear Menu"; ObjectID = "126"; */ "126.title" = "Clear Menu"; /* Class = "NSMenu"; title = "Services"; ObjectID = "130"; */ "130.title" = "Services"; /* Class = "NSMenuItem"; title = "Services"; ObjectID = "131"; */ "131.title" = "Services"; /* Class = "NSMenuItem"; title = "Hide Sparkle Test App"; ObjectID = "134"; */ "134.title" = "Hide Sparkle Test App"; /* Class = "NSMenuItem"; title = "Quit Sparkle Test App"; ObjectID = "136"; */ "136.title" = "Quit Sparkle Test App"; /* Class = "NSMenuItem"; title = "Hide Others"; ObjectID = "145"; */ "145.title" = "Hide Others"; /* Class = "NSMenuItem"; title = "Show All"; ObjectID = "150"; */ "150.title" = "Show All"; /* Class = "NSMenuItem"; title = "Find…"; ObjectID = "154"; */ "154.title" = "Find…"; /* Class = "NSMenuItem"; title = "Jump to Selection"; ObjectID = "155"; */ "155.title" = "Jump to Selection"; /* Class = "NSMenuItem"; title = "Copy"; ObjectID = "157"; */ "157.title" = "Copy"; /* Class = "NSMenuItem"; title = "Undo"; ObjectID = "158"; */ "158.title" = "Undo"; /* Class = "NSMenu"; title = "Find"; ObjectID = "159"; */ "159.title" = "Find"; /* Class = "NSMenuItem"; title = "Cut"; ObjectID = "160"; */ "160.title" = "Cut"; /* Class = "NSMenuItem"; title = "Use Selection for Find"; ObjectID = "161"; */ "161.title" = "Use Selection for Find"; /* Class = "NSMenuItem"; title = "Find Previous"; ObjectID = "162"; */ "162.title" = "Find Previous"; /* Class = "NSMenuItem"; title = "Edit"; ObjectID = "163"; */ "163.title" = "Edit"; /* Class = "NSMenuItem"; title = "Delete"; ObjectID = "164"; */ "164.title" = "Delete"; /* Class = "NSMenuItem"; title = "Find Next"; ObjectID = "167"; */ "167.title" = "Find Next"; /* Class = "NSMenuItem"; title = "Find"; ObjectID = "168"; */ "168.title" = "Find"; /* Class = "NSMenu"; title = "Edit"; ObjectID = "169"; */ "169.title" = "Edit"; /* Class = "NSMenuItem"; title = "Paste"; ObjectID = "171"; */ "171.title" = "Paste"; /* Class = "NSMenuItem"; title = "Select All"; ObjectID = "172"; */ "172.title" = "Select All"; /* Class = "NSMenuItem"; title = "Redo"; ObjectID = "173"; */ "173.title" = "Redo"; /* Class = "NSMenuItem"; title = "Spelling"; ObjectID = "184"; */ "184.title" = "Spelling"; /* Class = "NSMenu"; title = "Spelling"; ObjectID = "185"; */ "185.title" = "Spelling"; /* Class = "NSMenuItem"; title = "Spelling…"; ObjectID = "187"; */ "187.title" = "Spelling…"; /* Class = "NSMenuItem"; title = "Check Spelling"; ObjectID = "189"; */ "189.title" = "Check Spelling"; /* Class = "NSMenuItem"; title = "Check Spelling as You Type"; ObjectID = "191"; */ "191.title" = "Check Spelling as You Type"; /* Class = "NSMenuItem"; title = "Zoom"; ObjectID = "197"; */ "197.title" = "Zoom"; /* Class = "NSMenuItem"; title = "Paste and Match Style"; ObjectID = "204"; */ "204.title" = "Paste and Match Style"; /* Class = "NSMenuItem"; title = "Check for Updates…"; ObjectID = "207"; */ "207.title" = "Check for Updates…"; ================================================ FILE: TestApplication/vi.lproj/MainMenu.strings ================================================ /* Class = "NSMenuItem"; title = "Bring All to Front"; ObjectID = "5"; */ "5.title" = "Đưa tất cả ra phía trước"; /* Class = "NSMenuItem"; title = "Window"; ObjectID = "19"; */ "19.title" = "Cửa sổ"; /* Class = "NSMenuItem"; title = "Minimize"; ObjectID = "23"; */ "23.title" = "Thu nhỏ"; /* Class = "NSMenu"; title = "Window"; ObjectID = "24"; */ "24.title" = "Cửa sổ"; /* Class = "NSMenu"; title = "MainMenu"; ObjectID = "29"; */ "29.title" = "Menu chính"; /* Class = "NSMenuItem"; title = "Sparkle Test App"; ObjectID = "56"; */ "56.title" = "Ứng dụng kiểm thử Sparkle"; /* Class = "NSMenu"; title = "Sparkle Test App"; ObjectID = "57"; */ "57.title" = "Ứng dụng kiểm thử Sparkle"; /* Class = "NSMenuItem"; title = "About Sparkle Test App"; ObjectID = "58"; */ "58.title" = "Giới thiệu về Ứng dụng kiểm thử Sparkle"; /* Class = "NSMenuItem"; title = "Open..."; ObjectID = "72"; */ "72.title" = "Mở..."; /* Class = "NSMenuItem"; title = "Close"; ObjectID = "73"; */ "73.title" = "Đóng"; /* Class = "NSMenuItem"; title = "Save"; ObjectID = "75"; */ "75.title" = "Lưu"; /* Class = "NSMenuItem"; title = "Page Setup…"; ObjectID = "77"; */ "77.title" = "Thiết lập trang…"; /* Class = "NSMenuItem"; title = "Print…"; ObjectID = "78"; */ "78.title" = "In…"; /* Class = "NSMenuItem"; title = "Save As…"; ObjectID = "80"; */ "80.title" = "Lưu dưới dạng…"; /* Class = "NSMenu"; title = "File"; ObjectID = "81"; */ "81.title" = "Tệp"; /* Class = "NSMenuItem"; title = "New"; ObjectID = "82"; */ "82.title" = "Mới"; /* Class = "NSMenuItem"; title = "File"; ObjectID = "83"; */ "83.title" = "Tệp"; /* Class = "NSMenuItem"; title = "Help"; ObjectID = "103"; */ "103.title" = "Trợ giúp"; /* Class = "NSMenu"; title = "Help"; ObjectID = "106"; */ "106.title" = "Trợ giúp"; /* Class = "NSMenuItem"; title = "Sparkle Test App Help"; ObjectID = "111"; */ "111.title" = "Trợ giúp Ứng dụng kiểm thử Sparkle"; /* Class = "NSMenuItem"; title = "Revert"; ObjectID = "112"; */ "112.title" = "Hoàn nguyên"; /* Class = "NSMenuItem"; title = "Open Recent"; ObjectID = "124"; */ "124.title" = "Mở gần đây"; /* Class = "NSMenu"; title = "Open Recent"; ObjectID = "125"; */ "125.title" = "Mở gần đây"; /* Class = "NSMenuItem"; title = "Clear Menu"; ObjectID = "126"; */ "126.title" = "Xóa menu"; /* Class = "NSMenu"; title = "Services"; ObjectID = "130"; */ "130.title" = "Dịch vụ"; /* Class = "NSMenuItem"; title = "Services"; ObjectID = "131"; */ "131.title" = "Dịch vụ"; /* Class = "NSMenuItem"; title = "Hide Sparkle Test App"; ObjectID = "134"; */ "134.title" = "Ẩn Ứng dụng kiểm thử Sparkle"; /* Class = "NSMenuItem"; title = "Quit Sparkle Test App"; ObjectID = "136"; */ "136.title" = "Thoát Ứng dụng kiểm thử Sparkle"; /* Class = "NSMenuItem"; title = "Hide Others"; ObjectID = "145"; */ "145.title" = "Ẩn các ứng dụng khác"; /* Class = "NSMenuItem"; title = "Show All"; ObjectID = "150"; */ "150.title" = "Hiện tất cả"; /* Class = "NSMenuItem"; title = "Find…"; ObjectID = "154"; */ "154.title" = "Tìm…"; /* Class = "NSMenuItem"; title = "Jump to Selection"; ObjectID = "155"; */ "155.title" = "Chuyển đến vùng chọn"; /* Class = "NSMenuItem"; title = "Copy"; ObjectID = "157"; */ "157.title" = "Sao chép"; /* Class = "NSMenuItem"; title = "Undo"; ObjectID = "158"; */ "158.title" = "Hoàn tác"; /* Class = "NSMenu"; title = "Find"; ObjectID = "159"; */ "159.title" = "Tìm"; /* Class = "NSMenuItem"; title = "Cut"; ObjectID = "160"; */ "160.title" = "Cắt"; /* Class = "NSMenuItem"; title = "Use Selection for Find"; ObjectID = "161"; */ "161.title" = "Dùng vùng chọn để tìm"; /* Class = "NSMenuItem"; title = "Find Previous"; ObjectID = "162"; */ "162.title" = "Tìm trước đó"; /* Class = "NSMenuItem"; title = "Edit"; ObjectID = "163"; */ "163.title" = "Chỉnh sửa"; /* Class = "NSMenuItem"; title = "Delete"; ObjectID = "164"; */ "164.title" = "Xóa"; /* Class = "NSMenuItem"; title = "Find Next"; ObjectID = "167"; */ "167.title" = "Tìm tiếp theo"; /* Class = "NSMenuItem"; title = "Find"; ObjectID = "168"; */ "168.title" = "Tìm"; /* Class = "NSMenu"; title = "Edit"; ObjectID = "169"; */ "169.title" = "Chỉnh sửa"; /* Class = "NSMenuItem"; title = "Paste"; ObjectID = "171"; */ "171.title" = "Dán"; /* Class = "NSMenuItem"; title = "Select All"; ObjectID = "172"; */ "172.title" = "Chọn tất cả"; /* Class = "NSMenuItem"; title = "Redo"; ObjectID = "173"; */ "173.title" = "Làm lại"; /* Class = "NSMenuItem"; title = "Spelling"; ObjectID = "184"; */ "184.title" = "Chính tả"; /* Class = "NSMenu"; title = "Spelling"; ObjectID = "185"; */ "185.title" = "Chính tả"; /* Class = "NSMenuItem"; title = "Spelling…"; ObjectID = "187"; */ "187.title" = "Chính tả…"; /* Class = "NSMenuItem"; title = "Check Spelling"; ObjectID = "189"; */ "189.title" = "Kiểm tra chính tả"; /* Class = "NSMenuItem"; title = "Check Spelling as You Type"; ObjectID = "191"; */ "191.title" = "Kiểm tra chính tả khi nhập"; /* Class = "NSMenuItem"; title = "Zoom"; ObjectID = "197"; */ "197.title" = "Thu phóng"; /* Class = "NSMenuItem"; title = "Paste and Match Style"; ObjectID = "204"; */ "204.title" = "Dán và khớp kiểu"; /* Class = "NSMenuItem"; title = "Check for Updates…"; ObjectID = "207"; */ "207.title" = "Kiểm tra cập nhật…"; ================================================ FILE: TestApplication/zh_CN.lproj/MainMenu.strings ================================================ /* Class = "NSMenuItem"; title = "Bring All to Front"; ObjectID = "5"; */ "5.title" = "Bring All to Front"; /* Class = "NSMenuItem"; title = "Window"; ObjectID = "19"; */ "19.title" = "Window"; /* Class = "NSMenuItem"; title = "Minimize"; ObjectID = "23"; */ "23.title" = "Minimize"; /* Class = "NSMenu"; title = "Window"; ObjectID = "24"; */ "24.title" = "Window"; /* Class = "NSMenu"; title = "MainMenu"; ObjectID = "29"; */ "29.title" = "MainMenu"; /* Class = "NSMenuItem"; title = "Sparkle Test App"; ObjectID = "56"; */ "56.title" = "Sparkle Test App"; /* Class = "NSMenu"; title = "Sparkle Test App"; ObjectID = "57"; */ "57.title" = "Sparkle Test App"; /* Class = "NSMenuItem"; title = "About Sparkle Test App"; ObjectID = "58"; */ "58.title" = "About Sparkle Test App"; /* Class = "NSMenuItem"; title = "Open..."; ObjectID = "72"; */ "72.title" = "Open..."; /* Class = "NSMenuItem"; title = "Close"; ObjectID = "73"; */ "73.title" = "Close"; /* Class = "NSMenuItem"; title = "Save"; ObjectID = "75"; */ "75.title" = "Save"; /* Class = "NSMenuItem"; title = "Page Setup…"; ObjectID = "77"; */ "77.title" = "Page Setup…"; /* Class = "NSMenuItem"; title = "Print…"; ObjectID = "78"; */ "78.title" = "Print…"; /* Class = "NSMenuItem"; title = "Save As…"; ObjectID = "80"; */ "80.title" = "Save As…"; /* Class = "NSMenu"; title = "File"; ObjectID = "81"; */ "81.title" = "File"; /* Class = "NSMenuItem"; title = "New"; ObjectID = "82"; */ "82.title" = "New"; /* Class = "NSMenuItem"; title = "File"; ObjectID = "83"; */ "83.title" = "File"; /* Class = "NSMenuItem"; title = "Help"; ObjectID = "103"; */ "103.title" = "Help"; /* Class = "NSMenu"; title = "Help"; ObjectID = "106"; */ "106.title" = "Help"; /* Class = "NSMenuItem"; title = "Sparkle Test App Help"; ObjectID = "111"; */ "111.title" = "Sparkle Test App Help"; /* Class = "NSMenuItem"; title = "Revert"; ObjectID = "112"; */ "112.title" = "Revert"; /* Class = "NSMenuItem"; title = "Open Recent"; ObjectID = "124"; */ "124.title" = "Open Recent"; /* Class = "NSMenu"; title = "Open Recent"; ObjectID = "125"; */ "125.title" = "Open Recent"; /* Class = "NSMenuItem"; title = "Clear Menu"; ObjectID = "126"; */ "126.title" = "Clear Menu"; /* Class = "NSMenu"; title = "Services"; ObjectID = "130"; */ "130.title" = "Services"; /* Class = "NSMenuItem"; title = "Services"; ObjectID = "131"; */ "131.title" = "Services"; /* Class = "NSMenuItem"; title = "Hide Sparkle Test App"; ObjectID = "134"; */ "134.title" = "Hide Sparkle Test App"; /* Class = "NSMenuItem"; title = "Quit Sparkle Test App"; ObjectID = "136"; */ "136.title" = "Quit Sparkle Test App"; /* Class = "NSMenuItem"; title = "Hide Others"; ObjectID = "145"; */ "145.title" = "Hide Others"; /* Class = "NSMenuItem"; title = "Show All"; ObjectID = "150"; */ "150.title" = "Show All"; /* Class = "NSMenuItem"; title = "Find…"; ObjectID = "154"; */ "154.title" = "Find…"; /* Class = "NSMenuItem"; title = "Jump to Selection"; ObjectID = "155"; */ "155.title" = "Jump to Selection"; /* Class = "NSMenuItem"; title = "Copy"; ObjectID = "157"; */ "157.title" = "Copy"; /* Class = "NSMenuItem"; title = "Undo"; ObjectID = "158"; */ "158.title" = "Undo"; /* Class = "NSMenu"; title = "Find"; ObjectID = "159"; */ "159.title" = "Find"; /* Class = "NSMenuItem"; title = "Cut"; ObjectID = "160"; */ "160.title" = "Cut"; /* Class = "NSMenuItem"; title = "Use Selection for Find"; ObjectID = "161"; */ "161.title" = "Use Selection for Find"; /* Class = "NSMenuItem"; title = "Find Previous"; ObjectID = "162"; */ "162.title" = "Find Previous"; /* Class = "NSMenuItem"; title = "Edit"; ObjectID = "163"; */ "163.title" = "Edit"; /* Class = "NSMenuItem"; title = "Delete"; ObjectID = "164"; */ "164.title" = "Delete"; /* Class = "NSMenuItem"; title = "Find Next"; ObjectID = "167"; */ "167.title" = "Find Next"; /* Class = "NSMenuItem"; title = "Find"; ObjectID = "168"; */ "168.title" = "Find"; /* Class = "NSMenu"; title = "Edit"; ObjectID = "169"; */ "169.title" = "Edit"; /* Class = "NSMenuItem"; title = "Paste"; ObjectID = "171"; */ "171.title" = "Paste"; /* Class = "NSMenuItem"; title = "Select All"; ObjectID = "172"; */ "172.title" = "Select All"; /* Class = "NSMenuItem"; title = "Redo"; ObjectID = "173"; */ "173.title" = "Redo"; /* Class = "NSMenuItem"; title = "Spelling"; ObjectID = "184"; */ "184.title" = "Spelling"; /* Class = "NSMenu"; title = "Spelling"; ObjectID = "185"; */ "185.title" = "Spelling"; /* Class = "NSMenuItem"; title = "Spelling…"; ObjectID = "187"; */ "187.title" = "Spelling…"; /* Class = "NSMenuItem"; title = "Check Spelling"; ObjectID = "189"; */ "189.title" = "Check Spelling"; /* Class = "NSMenuItem"; title = "Check Spelling as You Type"; ObjectID = "191"; */ "191.title" = "Check Spelling as You Type"; /* Class = "NSMenuItem"; title = "Zoom"; ObjectID = "197"; */ "197.title" = "Zoom"; /* Class = "NSMenuItem"; title = "Paste and Match Style"; ObjectID = "204"; */ "204.title" = "Paste and Match Style"; /* Class = "NSMenuItem"; title = "Check for Updates…"; ObjectID = "207"; */ "207.title" = "Check for Updates…"; ================================================ FILE: TestApplication/zh_HK.lproj/MainMenu.strings ================================================ /* Class = "NSMenuItem"; title = "Bring All to Front"; ObjectID = "5"; */ "5.title" = "Bring All to Front"; /* Class = "NSMenuItem"; title = "Window"; ObjectID = "19"; */ "19.title" = "Window"; /* Class = "NSMenuItem"; title = "Minimize"; ObjectID = "23"; */ "23.title" = "Minimize"; /* Class = "NSMenu"; title = "Window"; ObjectID = "24"; */ "24.title" = "Window"; /* Class = "NSMenu"; title = "MainMenu"; ObjectID = "29"; */ "29.title" = "MainMenu"; /* Class = "NSMenuItem"; title = "Sparkle Test App"; ObjectID = "56"; */ "56.title" = "Sparkle Test App"; /* Class = "NSMenu"; title = "Sparkle Test App"; ObjectID = "57"; */ "57.title" = "Sparkle Test App"; /* Class = "NSMenuItem"; title = "About Sparkle Test App"; ObjectID = "58"; */ "58.title" = "About Sparkle Test App"; /* Class = "NSMenuItem"; title = "Open..."; ObjectID = "72"; */ "72.title" = "Open..."; /* Class = "NSMenuItem"; title = "Close"; ObjectID = "73"; */ "73.title" = "Close"; /* Class = "NSMenuItem"; title = "Save"; ObjectID = "75"; */ "75.title" = "Save"; /* Class = "NSMenuItem"; title = "Page Setup…"; ObjectID = "77"; */ "77.title" = "Page Setup…"; /* Class = "NSMenuItem"; title = "Print…"; ObjectID = "78"; */ "78.title" = "Print…"; /* Class = "NSMenuItem"; title = "Save As…"; ObjectID = "80"; */ "80.title" = "Save As…"; /* Class = "NSMenu"; title = "File"; ObjectID = "81"; */ "81.title" = "File"; /* Class = "NSMenuItem"; title = "New"; ObjectID = "82"; */ "82.title" = "New"; /* Class = "NSMenuItem"; title = "File"; ObjectID = "83"; */ "83.title" = "File"; /* Class = "NSMenuItem"; title = "Help"; ObjectID = "103"; */ "103.title" = "Help"; /* Class = "NSMenu"; title = "Help"; ObjectID = "106"; */ "106.title" = "Help"; /* Class = "NSMenuItem"; title = "Sparkle Test App Help"; ObjectID = "111"; */ "111.title" = "Sparkle Test App Help"; /* Class = "NSMenuItem"; title = "Revert"; ObjectID = "112"; */ "112.title" = "Revert"; /* Class = "NSMenuItem"; title = "Open Recent"; ObjectID = "124"; */ "124.title" = "Open Recent"; /* Class = "NSMenu"; title = "Open Recent"; ObjectID = "125"; */ "125.title" = "Open Recent"; /* Class = "NSMenuItem"; title = "Clear Menu"; ObjectID = "126"; */ "126.title" = "Clear Menu"; /* Class = "NSMenu"; title = "Services"; ObjectID = "130"; */ "130.title" = "Services"; /* Class = "NSMenuItem"; title = "Services"; ObjectID = "131"; */ "131.title" = "Services"; /* Class = "NSMenuItem"; title = "Hide Sparkle Test App"; ObjectID = "134"; */ "134.title" = "Hide Sparkle Test App"; /* Class = "NSMenuItem"; title = "Quit Sparkle Test App"; ObjectID = "136"; */ "136.title" = "Quit Sparkle Test App"; /* Class = "NSMenuItem"; title = "Hide Others"; ObjectID = "145"; */ "145.title" = "Hide Others"; /* Class = "NSMenuItem"; title = "Show All"; ObjectID = "150"; */ "150.title" = "Show All"; /* Class = "NSMenuItem"; title = "Find…"; ObjectID = "154"; */ "154.title" = "Find…"; /* Class = "NSMenuItem"; title = "Jump to Selection"; ObjectID = "155"; */ "155.title" = "Jump to Selection"; /* Class = "NSMenuItem"; title = "Copy"; ObjectID = "157"; */ "157.title" = "Copy"; /* Class = "NSMenuItem"; title = "Undo"; ObjectID = "158"; */ "158.title" = "Undo"; /* Class = "NSMenu"; title = "Find"; ObjectID = "159"; */ "159.title" = "Find"; /* Class = "NSMenuItem"; title = "Cut"; ObjectID = "160"; */ "160.title" = "Cut"; /* Class = "NSMenuItem"; title = "Use Selection for Find"; ObjectID = "161"; */ "161.title" = "Use Selection for Find"; /* Class = "NSMenuItem"; title = "Find Previous"; ObjectID = "162"; */ "162.title" = "Find Previous"; /* Class = "NSMenuItem"; title = "Edit"; ObjectID = "163"; */ "163.title" = "Edit"; /* Class = "NSMenuItem"; title = "Delete"; ObjectID = "164"; */ "164.title" = "Delete"; /* Class = "NSMenuItem"; title = "Find Next"; ObjectID = "167"; */ "167.title" = "Find Next"; /* Class = "NSMenuItem"; title = "Find"; ObjectID = "168"; */ "168.title" = "Find"; /* Class = "NSMenu"; title = "Edit"; ObjectID = "169"; */ "169.title" = "Edit"; /* Class = "NSMenuItem"; title = "Paste"; ObjectID = "171"; */ "171.title" = "Paste"; /* Class = "NSMenuItem"; title = "Select All"; ObjectID = "172"; */ "172.title" = "Select All"; /* Class = "NSMenuItem"; title = "Redo"; ObjectID = "173"; */ "173.title" = "Redo"; /* Class = "NSMenuItem"; title = "Spelling"; ObjectID = "184"; */ "184.title" = "Spelling"; /* Class = "NSMenu"; title = "Spelling"; ObjectID = "185"; */ "185.title" = "Spelling"; /* Class = "NSMenuItem"; title = "Spelling…"; ObjectID = "187"; */ "187.title" = "Spelling…"; /* Class = "NSMenuItem"; title = "Check Spelling"; ObjectID = "189"; */ "189.title" = "Check Spelling"; /* Class = "NSMenuItem"; title = "Check Spelling as You Type"; ObjectID = "191"; */ "191.title" = "Check Spelling as You Type"; /* Class = "NSMenuItem"; title = "Zoom"; ObjectID = "197"; */ "197.title" = "Zoom"; /* Class = "NSMenuItem"; title = "Paste and Match Style"; ObjectID = "204"; */ "204.title" = "Paste and Match Style"; /* Class = "NSMenuItem"; title = "Check for Updates…"; ObjectID = "207"; */ "207.title" = "Check for Updates…"; ================================================ FILE: TestApplication/zh_TW.lproj/MainMenu.strings ================================================ /* Class = "NSMenuItem"; title = "Bring All to Front"; ObjectID = "5"; */ "5.title" = "Bring All to Front"; /* Class = "NSMenuItem"; title = "Window"; ObjectID = "19"; */ "19.title" = "Window"; /* Class = "NSMenuItem"; title = "Minimize"; ObjectID = "23"; */ "23.title" = "Minimize"; /* Class = "NSMenu"; title = "Window"; ObjectID = "24"; */ "24.title" = "Window"; /* Class = "NSMenu"; title = "MainMenu"; ObjectID = "29"; */ "29.title" = "MainMenu"; /* Class = "NSMenuItem"; title = "Sparkle Test App"; ObjectID = "56"; */ "56.title" = "Sparkle Test App"; /* Class = "NSMenu"; title = "Sparkle Test App"; ObjectID = "57"; */ "57.title" = "Sparkle Test App"; /* Class = "NSMenuItem"; title = "About Sparkle Test App"; ObjectID = "58"; */ "58.title" = "About Sparkle Test App"; /* Class = "NSMenuItem"; title = "Open..."; ObjectID = "72"; */ "72.title" = "Open..."; /* Class = "NSMenuItem"; title = "Close"; ObjectID = "73"; */ "73.title" = "Close"; /* Class = "NSMenuItem"; title = "Save"; ObjectID = "75"; */ "75.title" = "Save"; /* Class = "NSMenuItem"; title = "Page Setup…"; ObjectID = "77"; */ "77.title" = "Page Setup…"; /* Class = "NSMenuItem"; title = "Print…"; ObjectID = "78"; */ "78.title" = "Print…"; /* Class = "NSMenuItem"; title = "Save As…"; ObjectID = "80"; */ "80.title" = "Save As…"; /* Class = "NSMenu"; title = "File"; ObjectID = "81"; */ "81.title" = "File"; /* Class = "NSMenuItem"; title = "New"; ObjectID = "82"; */ "82.title" = "New"; /* Class = "NSMenuItem"; title = "File"; ObjectID = "83"; */ "83.title" = "File"; /* Class = "NSMenuItem"; title = "Help"; ObjectID = "103"; */ "103.title" = "Help"; /* Class = "NSMenu"; title = "Help"; ObjectID = "106"; */ "106.title" = "Help"; /* Class = "NSMenuItem"; title = "Sparkle Test App Help"; ObjectID = "111"; */ "111.title" = "Sparkle Test App Help"; /* Class = "NSMenuItem"; title = "Revert"; ObjectID = "112"; */ "112.title" = "Revert"; /* Class = "NSMenuItem"; title = "Open Recent"; ObjectID = "124"; */ "124.title" = "Open Recent"; /* Class = "NSMenu"; title = "Open Recent"; ObjectID = "125"; */ "125.title" = "Open Recent"; /* Class = "NSMenuItem"; title = "Clear Menu"; ObjectID = "126"; */ "126.title" = "Clear Menu"; /* Class = "NSMenu"; title = "Services"; ObjectID = "130"; */ "130.title" = "Services"; /* Class = "NSMenuItem"; title = "Services"; ObjectID = "131"; */ "131.title" = "Services"; /* Class = "NSMenuItem"; title = "Hide Sparkle Test App"; ObjectID = "134"; */ "134.title" = "Hide Sparkle Test App"; /* Class = "NSMenuItem"; title = "Quit Sparkle Test App"; ObjectID = "136"; */ "136.title" = "Quit Sparkle Test App"; /* Class = "NSMenuItem"; title = "Hide Others"; ObjectID = "145"; */ "145.title" = "Hide Others"; /* Class = "NSMenuItem"; title = "Show All"; ObjectID = "150"; */ "150.title" = "Show All"; /* Class = "NSMenuItem"; title = "Find…"; ObjectID = "154"; */ "154.title" = "Find…"; /* Class = "NSMenuItem"; title = "Jump to Selection"; ObjectID = "155"; */ "155.title" = "Jump to Selection"; /* Class = "NSMenuItem"; title = "Copy"; ObjectID = "157"; */ "157.title" = "Copy"; /* Class = "NSMenuItem"; title = "Undo"; ObjectID = "158"; */ "158.title" = "Undo"; /* Class = "NSMenu"; title = "Find"; ObjectID = "159"; */ "159.title" = "Find"; /* Class = "NSMenuItem"; title = "Cut"; ObjectID = "160"; */ "160.title" = "Cut"; /* Class = "NSMenuItem"; title = "Use Selection for Find"; ObjectID = "161"; */ "161.title" = "Use Selection for Find"; /* Class = "NSMenuItem"; title = "Find Previous"; ObjectID = "162"; */ "162.title" = "Find Previous"; /* Class = "NSMenuItem"; title = "Edit"; ObjectID = "163"; */ "163.title" = "Edit"; /* Class = "NSMenuItem"; title = "Delete"; ObjectID = "164"; */ "164.title" = "Delete"; /* Class = "NSMenuItem"; title = "Find Next"; ObjectID = "167"; */ "167.title" = "Find Next"; /* Class = "NSMenuItem"; title = "Find"; ObjectID = "168"; */ "168.title" = "Find"; /* Class = "NSMenu"; title = "Edit"; ObjectID = "169"; */ "169.title" = "Edit"; /* Class = "NSMenuItem"; title = "Paste"; ObjectID = "171"; */ "171.title" = "Paste"; /* Class = "NSMenuItem"; title = "Select All"; ObjectID = "172"; */ "172.title" = "Select All"; /* Class = "NSMenuItem"; title = "Redo"; ObjectID = "173"; */ "173.title" = "Redo"; /* Class = "NSMenuItem"; title = "Spelling"; ObjectID = "184"; */ "184.title" = "Spelling"; /* Class = "NSMenu"; title = "Spelling"; ObjectID = "185"; */ "185.title" = "Spelling"; /* Class = "NSMenuItem"; title = "Spelling…"; ObjectID = "187"; */ "187.title" = "Spelling…"; /* Class = "NSMenuItem"; title = "Check Spelling"; ObjectID = "189"; */ "189.title" = "Check Spelling"; /* Class = "NSMenuItem"; title = "Check Spelling as You Type"; ObjectID = "191"; */ "191.title" = "Check Spelling as You Type"; /* Class = "NSMenuItem"; title = "Zoom"; ObjectID = "197"; */ "197.title" = "Zoom"; /* Class = "NSMenuItem"; title = "Paste and Match Style"; ObjectID = "204"; */ "204.title" = "Paste and Match Style"; /* Class = "NSMenuItem"; title = "Check for Updates…"; ObjectID = "207"; */ "207.title" = "Check for Updates…"; ================================================ FILE: Tests/.swiftlint.yml ================================================ # Inherits from top level config disabled_rules: - force_try - force_cast ================================================ FILE: Tests/Resources/SUUpdateValidatorTest/Both.bundle/Contents/Info.plist ================================================ CFBundleInfoDictionaryVersion 6.0 CFBundlePackageType BNDL CFBundleSignature ???? CFBundleVersion 1.0 SUPublicEDKey rhHib+w769W2/6/t+oM1ZxgjBB93BfBKMLO0Qo1etQs= SUPublicDSAKeyFile test-pubkey.pem CFBundleIdentifier org.sparkle-project.Sparkle.SUUpdateValidatorTestBundle.Both ================================================ FILE: Tests/Resources/SUUpdateValidatorTest/Both.bundle/Contents/Resources/test-pubkey.pem ================================================ -----BEGIN PUBLIC KEY----- MIIGOjCCBC0GByqGSM44BAEwggQgAoICAQCHyAu13F9I72JZzN9KgVhrprKnRH7n dJJP5ZVHaSqm5PbRmHbnT06DGMxloZc8Nd5QWwG6N+5O1I9ZZjnIrc9Qzodho8hM KNInrfCEy/lTHjydvQvYG2LitbknapgGTf57BWEIYSiAQyP8Gs6lTVZmGrqSJdRC Kdt29OmFKLhWVn5hm2nx7PCbRT7FzrRyX8jkjCUKSeI/s1j3jpjFT5nGutAsSYNS Y/ifH1/IRejv9kRUEWM7qEU/ue3C18qCoDsmTSQVh+E1MIBWqeaWWAGrw7JO4cxR cgR6eeDYp8plTw//e9dSn1u/6ArnQ4QAoZP8Q6MfJMABgg07lltRIQYiXnUNPE4c xzMQ6kxbKLBi2VyeKm/mQ5oO6PH+59Lw0Q1YhchAXCO171fx2s4W2UGcIjdNbyhs My5iSEICwXQQgAvTEC08An8aYi5waEpQ1fnnkQcawTgRoBL2gMNsQOkZdERbJr2X zNJfLwGoDnNJTD8V4FCAeHieOAqmlGsqRc7mKPq2EVW7GH6vzWBJbZc71rmk7EwY 2zvpR9aFam71ivmFgLYwLrRef3vd+AGbIc2WOcIADJ9JIa6HF1gK8LSym/t+/2ws fQ1FHpjHmYZICJqac1SQ6KZhMz9q35yKqBSaNmiv+mXBg7W6Jfckjg8mGgxvTdL+ VDRDlK9K6bxClwIVAPd9bTMe17hl3ipR0X6O2UOmt799AoICABPfIvExHzdmJK+G kx+o28PNNw9ZAte2RxZWm8b0m9MW+jdeWDrhGbbk7nVJn6i6xaaRgJDWuZnKUQbC 0WwjOm9fXnjBmnQhacJu0RGyr/b+akzZ4Y/7N+SBxRjasLVTFd+msQ2NE1PkXps5 rhJWVMu9R9xp0qR/e+rh/Cax7nSq5UNAqSy9gzHkOhjS7s6UJ1yfCEO5Xfcvb7ND RpLIeBYbLT3OSkSd6kkFvMtb0Sl/WZ7z88flkdNGbutAcJVQzBcfQIwCuXyOYzUr 8TcIB5j4c19MN9yfkykgemieC0uz/xzgtZt6g6bFfQNONmJ0YGoEPkz1GftI6dx/ 324vh4KhLfBjKDKFB8Pt9JKI9nQA7P10GlwWeA+IpSTzjlJq3x35u220K7tc/5xr TDMEftBemn/dvb/bJ9h+iSciZ7EcnN2RXB7SfykUAohE9DdEUlmjip6DYLP+hSN2 oHiVmDVjv3XEGYQfATuxQmwcveHTYZmPId8XF2wkGiy6w0BY0NN+4oEuZga4YvEK 3MVF2VEzM0onFzNgszwb+KaUfcA+eycthidca+sJzmOGAMWCx/vydNryMcWVXABF IRxbZVaXHCWf1RhAf0jcsiz9+JIuScAB2J26Oy2shPGOmesvuUHeUTqzvEJ2G77j k9Mps+1fbzySKX3PQEwA/qQ11qT6A4ICBQACggIAbDXVjXtIxAM4TSt9FSep+H1j yRoEXgisf5Q6eU6I7Wf6kAoW6bHw76S/OHgcMBYX7Z42kbVbna/rIAVfHnfrAaZY ygYVqbxTKdhi0c8IxR2qyF9Z5UK68C/EP0SHjHJrFRAnYgwkqvJXbemHFB4c0Ds1 iIL47Kas8o/WvLpT1VzlHXFyFKvRxNMdeJsy8/LBSrQpRUiKcJFM2lg+O8cNWsRd kHTeWnjLrZT2rpPUIkSQZdbR16VBI8nS4pYpzdZ/N2zy+2S5dup7jMNtnLbXVT3X AjSjiYHRZPQUnX0pG3qA0BzDuA0U3MdBs8Wf7YhGd8XLbAfdo5zPSM9GimJrGH0s q4XKLHAEXUPfSuDGOdG8l990MujcRrewocwX5La1X/Nc4TDClCPUOVbf1aqy7LXY TB1M+nHvTb5HhtIrZYSHmsdpWlcLj5mYde/nvFXmNu3RNVCLhMzQDV9S/U1hqNcH m/BX2VTJ0xqhFIlA5UVrKZpnAEISKcFjwmZo7GuA1ENw2vhuvswjfYQ3OMfEr258 0b1tKNMWrkbCgHdALjT/VdRUtte7RyGvsMccUHu6MHPowFRxbT3lWUqj1LKdZYnT uERL4E1J2VIP1/x2upPPWl/wbQjxplsRQrSY+cQCfWM22Wtilouh+CdEc+1DNKs/ bKZWFUcty/GYmXtxTTk= -----END PUBLIC KEY----- ================================================ FILE: Tests/Resources/SUUpdateValidatorTest/CodeSignedBoth.bundle/Contents/Info.plist ================================================ CFBundleInfoDictionaryVersion 6.0 CFBundlePackageType BNDL CFBundleSignature ???? CFBundleVersion 1.0 SUPublicEDKey rhHib+w769W2/6/t+oM1ZxgjBB93BfBKMLO0Qo1etQs= SUPublicDSAKeyFile test-pubkey.pem CFBundleIdentifier org.sparkle-project.Sparkle.SUUpdateValidatorTestBundle.CodeSignedBoth ================================================ FILE: Tests/Resources/SUUpdateValidatorTest/CodeSignedBoth.bundle/Contents/Resources/test-pubkey.pem ================================================ -----BEGIN PUBLIC KEY----- MIIGOjCCBC0GByqGSM44BAEwggQgAoICAQCHyAu13F9I72JZzN9KgVhrprKnRH7n dJJP5ZVHaSqm5PbRmHbnT06DGMxloZc8Nd5QWwG6N+5O1I9ZZjnIrc9Qzodho8hM KNInrfCEy/lTHjydvQvYG2LitbknapgGTf57BWEIYSiAQyP8Gs6lTVZmGrqSJdRC Kdt29OmFKLhWVn5hm2nx7PCbRT7FzrRyX8jkjCUKSeI/s1j3jpjFT5nGutAsSYNS Y/ifH1/IRejv9kRUEWM7qEU/ue3C18qCoDsmTSQVh+E1MIBWqeaWWAGrw7JO4cxR cgR6eeDYp8plTw//e9dSn1u/6ArnQ4QAoZP8Q6MfJMABgg07lltRIQYiXnUNPE4c xzMQ6kxbKLBi2VyeKm/mQ5oO6PH+59Lw0Q1YhchAXCO171fx2s4W2UGcIjdNbyhs My5iSEICwXQQgAvTEC08An8aYi5waEpQ1fnnkQcawTgRoBL2gMNsQOkZdERbJr2X zNJfLwGoDnNJTD8V4FCAeHieOAqmlGsqRc7mKPq2EVW7GH6vzWBJbZc71rmk7EwY 2zvpR9aFam71ivmFgLYwLrRef3vd+AGbIc2WOcIADJ9JIa6HF1gK8LSym/t+/2ws fQ1FHpjHmYZICJqac1SQ6KZhMz9q35yKqBSaNmiv+mXBg7W6Jfckjg8mGgxvTdL+ VDRDlK9K6bxClwIVAPd9bTMe17hl3ipR0X6O2UOmt799AoICABPfIvExHzdmJK+G kx+o28PNNw9ZAte2RxZWm8b0m9MW+jdeWDrhGbbk7nVJn6i6xaaRgJDWuZnKUQbC 0WwjOm9fXnjBmnQhacJu0RGyr/b+akzZ4Y/7N+SBxRjasLVTFd+msQ2NE1PkXps5 rhJWVMu9R9xp0qR/e+rh/Cax7nSq5UNAqSy9gzHkOhjS7s6UJ1yfCEO5Xfcvb7ND RpLIeBYbLT3OSkSd6kkFvMtb0Sl/WZ7z88flkdNGbutAcJVQzBcfQIwCuXyOYzUr 8TcIB5j4c19MN9yfkykgemieC0uz/xzgtZt6g6bFfQNONmJ0YGoEPkz1GftI6dx/ 324vh4KhLfBjKDKFB8Pt9JKI9nQA7P10GlwWeA+IpSTzjlJq3x35u220K7tc/5xr TDMEftBemn/dvb/bJ9h+iSciZ7EcnN2RXB7SfykUAohE9DdEUlmjip6DYLP+hSN2 oHiVmDVjv3XEGYQfATuxQmwcveHTYZmPId8XF2wkGiy6w0BY0NN+4oEuZga4YvEK 3MVF2VEzM0onFzNgszwb+KaUfcA+eycthidca+sJzmOGAMWCx/vydNryMcWVXABF IRxbZVaXHCWf1RhAf0jcsiz9+JIuScAB2J26Oy2shPGOmesvuUHeUTqzvEJ2G77j k9Mps+1fbzySKX3PQEwA/qQ11qT6A4ICBQACggIAbDXVjXtIxAM4TSt9FSep+H1j yRoEXgisf5Q6eU6I7Wf6kAoW6bHw76S/OHgcMBYX7Z42kbVbna/rIAVfHnfrAaZY ygYVqbxTKdhi0c8IxR2qyF9Z5UK68C/EP0SHjHJrFRAnYgwkqvJXbemHFB4c0Ds1 iIL47Kas8o/WvLpT1VzlHXFyFKvRxNMdeJsy8/LBSrQpRUiKcJFM2lg+O8cNWsRd kHTeWnjLrZT2rpPUIkSQZdbR16VBI8nS4pYpzdZ/N2zy+2S5dup7jMNtnLbXVT3X AjSjiYHRZPQUnX0pG3qA0BzDuA0U3MdBs8Wf7YhGd8XLbAfdo5zPSM9GimJrGH0s q4XKLHAEXUPfSuDGOdG8l990MujcRrewocwX5La1X/Nc4TDClCPUOVbf1aqy7LXY TB1M+nHvTb5HhtIrZYSHmsdpWlcLj5mYde/nvFXmNu3RNVCLhMzQDV9S/U1hqNcH m/BX2VTJ0xqhFIlA5UVrKZpnAEISKcFjwmZo7GuA1ENw2vhuvswjfYQ3OMfEr258 0b1tKNMWrkbCgHdALjT/VdRUtte7RyGvsMccUHu6MHPowFRxbT3lWUqj1LKdZYnT uERL4E1J2VIP1/x2upPPWl/wbQjxplsRQrSY+cQCfWM22Wtilouh+CdEc+1DNKs/ bKZWFUcty/GYmXtxTTk= -----END PUBLIC KEY----- ================================================ FILE: Tests/Resources/SUUpdateValidatorTest/CodeSignedBoth.bundle/Contents/_CodeSignature/CodeResources ================================================ files Resources/test-pubkey.pem pR1EGsp5nhk1vr/xfQPT8es9ZBw= files2 Resources/test-pubkey.pem hash pR1EGsp5nhk1vr/xfQPT8es9ZBw= hash2 zD/XQJu4ElPWE6sfqtzoR2vEjdfgi98j/hiRoYZqv9w= rules ^Resources/ ^Resources/.*\.lproj/ optional weight 1000 ^Resources/.*\.lproj/locversion.plist$ omit weight 1100 ^Resources/Base\.lproj/ weight 1010 ^version.plist$ rules2 .*\.dSYM($|/) weight 11 ^(.*/)?\.DS_Store$ omit weight 2000 ^(Frameworks|SharedFrameworks|PlugIns|Plug-ins|XPCServices|Helpers|MacOS|Library/(Automator|Spotlight|LoginItems))/ nested weight 10 ^.* ^Info\.plist$ omit weight 20 ^PkgInfo$ omit weight 20 ^Resources/ weight 20 ^Resources/.*\.lproj/ optional weight 1000 ^Resources/.*\.lproj/locversion.plist$ omit weight 1100 ^Resources/Base\.lproj/ weight 1010 ^[^/]+$ nested weight 10 ^embedded\.provisionprofile$ weight 20 ^version\.plist$ weight 20 ================================================ FILE: Tests/Resources/SUUpdateValidatorTest/CodeSignedBoth.bundle/Contents/_CodeSignature/CodeSignature ================================================ ================================================ FILE: Tests/Resources/SUUpdateValidatorTest/CodeSignedBothNew.bundle/Contents/Info.plist ================================================ CFBundleInfoDictionaryVersion 6.0 CFBundlePackageType BNDL CFBundleSignature ???? CFBundleVersion 1.0 SUPublicEDKey rhHib+w769W2/6/t+oM1ZxgjBB93BfBKMLO0Qo1etQs= SUPublicDSAKeyFile test-pubkey.pem CFBundleIdentifier org.sparkle-project.Sparkle.SUUpdateValidatorTestBundle.CodeSignedBothNew ================================================ FILE: Tests/Resources/SUUpdateValidatorTest/CodeSignedBothNew.bundle/Contents/Resources/test-pubkey.pem ================================================ -----BEGIN PUBLIC KEY----- MIIGOjCCBC0GByqGSM44BAEwggQgAoICAQCHyAu13F9I72JZzN9KgVhrprKnRH7n dJJP5ZVHaSqm5PbRmHbnT06DGMxloZc8Nd5QWwG6N+5O1I9ZZjnIrc9Qzodho8hM KNInrfCEy/lTHjydvQvYG2LitbknapgGTf57BWEIYSiAQyP8Gs6lTVZmGrqSJdRC Kdt29OmFKLhWVn5hm2nx7PCbRT7FzrRyX8jkjCUKSeI/s1j3jpjFT5nGutAsSYNS Y/ifH1/IRejv9kRUEWM7qEU/ue3C18qCoDsmTSQVh+E1MIBWqeaWWAGrw7JO4cxR cgR6eeDYp8plTw//e9dSn1u/6ArnQ4QAoZP8Q6MfJMABgg07lltRIQYiXnUNPE4c xzMQ6kxbKLBi2VyeKm/mQ5oO6PH+59Lw0Q1YhchAXCO171fx2s4W2UGcIjdNbyhs My5iSEICwXQQgAvTEC08An8aYi5waEpQ1fnnkQcawTgRoBL2gMNsQOkZdERbJr2X zNJfLwGoDnNJTD8V4FCAeHieOAqmlGsqRc7mKPq2EVW7GH6vzWBJbZc71rmk7EwY 2zvpR9aFam71ivmFgLYwLrRef3vd+AGbIc2WOcIADJ9JIa6HF1gK8LSym/t+/2ws fQ1FHpjHmYZICJqac1SQ6KZhMz9q35yKqBSaNmiv+mXBg7W6Jfckjg8mGgxvTdL+ VDRDlK9K6bxClwIVAPd9bTMe17hl3ipR0X6O2UOmt799AoICABPfIvExHzdmJK+G kx+o28PNNw9ZAte2RxZWm8b0m9MW+jdeWDrhGbbk7nVJn6i6xaaRgJDWuZnKUQbC 0WwjOm9fXnjBmnQhacJu0RGyr/b+akzZ4Y/7N+SBxRjasLVTFd+msQ2NE1PkXps5 rhJWVMu9R9xp0qR/e+rh/Cax7nSq5UNAqSy9gzHkOhjS7s6UJ1yfCEO5Xfcvb7ND RpLIeBYbLT3OSkSd6kkFvMtb0Sl/WZ7z88flkdNGbutAcJVQzBcfQIwCuXyOYzUr 8TcIB5j4c19MN9yfkykgemieC0uz/xzgtZt6g6bFfQNONmJ0YGoEPkz1GftI6dx/ 324vh4KhLfBjKDKFB8Pt9JKI9nQA7P10GlwWeA+IpSTzjlJq3x35u220K7tc/5xr TDMEftBemn/dvb/bJ9h+iSciZ7EcnN2RXB7SfykUAohE9DdEUlmjip6DYLP+hSN2 oHiVmDVjv3XEGYQfATuxQmwcveHTYZmPId8XF2wkGiy6w0BY0NN+4oEuZga4YvEK 3MVF2VEzM0onFzNgszwb+KaUfcA+eycthidca+sJzmOGAMWCx/vydNryMcWVXABF IRxbZVaXHCWf1RhAf0jcsiz9+JIuScAB2J26Oy2shPGOmesvuUHeUTqzvEJ2G77j k9Mps+1fbzySKX3PQEwA/qQ11qT6A4ICBQACggIAbDXVjXtIxAM4TSt9FSep+H1j yRoEXgisf5Q6eU6I7Wf6kAoW6bHw76S/OHgcMBYX7Z42kbVbna/rIAVfHnfrAaZY ygYVqbxTKdhi0c8IxR2qyF9Z5UK68C/EP0SHjHJrFRAnYgwkqvJXbemHFB4c0Ds1 iIL47Kas8o/WvLpT1VzlHXFyFKvRxNMdeJsy8/LBSrQpRUiKcJFM2lg+O8cNWsRd kHTeWnjLrZT2rpPUIkSQZdbR16VBI8nS4pYpzdZ/N2zy+2S5dup7jMNtnLbXVT3X AjSjiYHRZPQUnX0pG3qA0BzDuA0U3MdBs8Wf7YhGd8XLbAfdo5zPSM9GimJrGH0s q4XKLHAEXUPfSuDGOdG8l990MujcRrewocwX5La1X/Nc4TDClCPUOVbf1aqy7LXY TB1M+nHvTb5HhtIrZYSHmsdpWlcLj5mYde/nvFXmNu3RNVCLhMzQDV9S/U1hqNcH m/BX2VTJ0xqhFIlA5UVrKZpnAEISKcFjwmZo7GuA1ENw2vhuvswjfYQ3OMfEr258 0b1tKNMWrkbCgHdALjT/VdRUtte7RyGvsMccUHu6MHPowFRxbT3lWUqj1LKdZYnT uERL4E1J2VIP1/x2upPPWl/wbQjxplsRQrSY+cQCfWM22Wtilouh+CdEc+1DNKs/ bKZWFUcty/GYmXtxTTk= -----END PUBLIC KEY----- ================================================ FILE: Tests/Resources/SUUpdateValidatorTest/CodeSignedBothNew.bundle/Contents/_CodeSignature/CodeResources ================================================ files Resources/test-pubkey.pem pR1EGsp5nhk1vr/xfQPT8es9ZBw= files2 Resources/test-pubkey.pem hash pR1EGsp5nhk1vr/xfQPT8es9ZBw= hash2 zD/XQJu4ElPWE6sfqtzoR2vEjdfgi98j/hiRoYZqv9w= rules ^Resources/ ^Resources/.*\.lproj/ optional weight 1000 ^Resources/.*\.lproj/locversion.plist$ omit weight 1100 ^Resources/Base\.lproj/ weight 1010 ^version.plist$ rules2 .*\.dSYM($|/) weight 11 ^(.*/)?\.DS_Store$ omit weight 2000 ^(Frameworks|SharedFrameworks|PlugIns|Plug-ins|XPCServices|Helpers|MacOS|Library/(Automator|Spotlight|LoginItems))/ nested weight 10 ^.* ^Info\.plist$ omit weight 20 ^PkgInfo$ omit weight 20 ^Resources/ weight 20 ^Resources/.*\.lproj/ optional weight 1000 ^Resources/.*\.lproj/locversion.plist$ omit weight 1100 ^Resources/Base\.lproj/ weight 1010 ^[^/]+$ nested weight 10 ^embedded\.provisionprofile$ weight 20 ^version\.plist$ weight 20 ================================================ FILE: Tests/Resources/SUUpdateValidatorTest/CodeSignedBothNew.bundle/Contents/_CodeSignature/CodeSignature ================================================ ================================================ FILE: Tests/Resources/SUUpdateValidatorTest/CodeSignedInvalid.bundle/Contents/Info.plist ================================================ CFBundleInfoDictionaryVersion 6.0 CFBundlePackageType BNDL CFBundleSignature ???? CFBundleVersion 1.0 SUPublicEDKey rhHib+w769W2/6/t+oM1ZxgjBB93BfBKMLO0Qo1etQs= SUPublicDSAKeyFile test-pubkey.pem CFBundleIdentifier org.sparkle-project.Sparkle.SUUpdateValidatorTestBundle.CodeSignedInvalid ================================================ FILE: Tests/Resources/SUUpdateValidatorTest/CodeSignedInvalid.bundle/Contents/Resources/test-pubkey.pem ================================================ -----BEGIN PUBLIC KEY----- MIIGOjCCBC0GByqGSM44BAEwggQgAoICAQCHyAu13F9I72JZzN9KgVhrprKnRH7n dJJP5ZVHaSqm5PbRmHbnT06DGMxloZc8Nd5QWwG6N+5O1I9ZZjnIrc9Qzodho8hM KNInrfCEy/lTHjydvQvYG2LitbknapgGTf57BWEIYSiAQyP8Gs6lTVZmGrqSJdRC Kdt29OmFKLhWVn5hm2nx7PCbRT7FzrRyX8jkjCUKSeI/s1j3jpjFT5nGutAsSYNS Y/ifH1/IRejv9kRUEWM7qEU/ue3C18qCoDsmTSQVh+E1MIBWqeaWWAGrw7JO4cxR cgR6eeDYp8plTw//e9dSn1u/6ArnQ4QAoZP8Q6MfJMABgg07lltRIQYiXnUNPE4c xzMQ6kxbKLBi2VyeKm/mQ5oO6PH+59Lw0Q1YhchAXCO171fx2s4W2UGcIjdNbyhs My5iSEICwXQQgAvTEC08An8aYi5waEpQ1fnnkQcawTgRoBL2gMNsQOkZdERbJr2X zNJfLwGoDnNJTD8V4FCAeHieOAqmlGsqRc7mKPq2EVW7GH6vzWBJbZc71rmk7EwY 2zvpR9aFam71ivmFgLYwLrRef3vd+AGbIc2WOcIADJ9JIa6HF1gK8LSym/t+/2ws fQ1FHpjHmYZICJqac1SQ6KZhMz9q35yKqBSaNmiv+mXBg7W6Jfckjg8mGgxvTdL+ VDRDlK9K6bxClwIVAPd9bTMe17hl3ipR0X6O2UOmt799AoICABPfIvExHzdmJK+G kx+o28PNNw9ZAte2RxZWm8b0m9MW+jdeWDrhGbbk7nVJn6i6xaaRgJDWuZnKUQbC 0WwjOm9fXnjBmnQhacJu0RGyr/b+akzZ4Y/7N+SBxRjasLVTFd+msQ2NE1PkXps5 rhJWVMu9R9xp0qR/e+rh/Cax7nSq5UNAqSy9gzHkOhjS7s6UJ1yfCEO5Xfcvb7ND RpLIeBYbLT3OSkSd6kkFvMtb0Sl/WZ7z88flkdNGbutAcJVQzBcfQIwCuXyOYzUr 8TcIB5j4c19MN9yfkykgemieC0uz/xzgtZt6g6bFfQNONmJ0YGoEPkz1GftI6dx/ 324vh4KhLfBjKDKFB8Pt9JKI9nQA7P10GlwWeA+IpSTzjlJq3x35u220K7tc/5xr TDMEftBemn/dvb/bJ9h+iSciZ7EcnN2RXB7SfykUAohE9DdEUlmjip6DYLP+hSN2 oHiVmDVjv3XEGYQfATuxQmwcveHTYZmPId8XF2wkGiy6w0BY0NN+4oEuZga4YvEK 3MVF2VEzM0onFzNgszwb+KaUfcA+eycthidca+sJzmOGAMWCx/vydNryMcWVXABF IRxbZVaXHCWf1RhAf0jcsiz9+JIuScAB2J26Oy2shPGOmesvuUHeUTqzvEJ2G77j k9Mps+1fbzySKX3PQEwA/qQ11qT6A4ICBQACggIAbDXVjXtIxAM4TSt9FSep+H1j yRoEXgisf5Q6eU6I7Wf6kAoW6bHw76S/OHgcMBYX7Z42kbVbna/rIAVfHnfrAaZY ygYVqbxTKdhi0c8IxR2qyF9Z5UK68C/EP0SHjHJrFRAnYgwkqvJXbemHFB4c0Ds1 iIL47Kas8o/WvLpT1VzlHXFyFKvRxNMdeJsy8/LBSrQpRUiKcJFM2lg+O8cNWsRd kHTeWnjLrZT2rpPUIkSQZdbR16VBI8nS4pYpzdZ/N2zy+2S5dup7jMNtnLbXVT3X AjSjiYHRZPQUnX0pG3qA0BzDuA0U3MdBs8Wf7YhGd8XLbAfdo5zPSM9GimJrGH0s q4XKLHAEXUPfSuDGOdG8l990MujcRrewocwX5La1X/Nc4TDClCPUOVbf1aqy7LXY TB1M+nHvTb5HhtIrZYSHmsdpWlcLj5mYde/nvFXmNu3RNVCLhMzQDV9S/U1hqNcH m/BX2VTJ0xqhFIlA5UVrKZpnAEISKcFjwmZo7GuA1ENw2vhuvswjfYQ3OMfEr258 0b1tKNMWrkbCgHdALjT/VdRUtte7RyGvsMccUHu6MHPowFRxbT3lWUqj1LKdZYnT uERL4E1J2VIP1/x2upPPWl/wbQjxplsRQrSY+cQCfWM22Wtilouh+CdEc+1DNKs/ bKZWFUcty/GYmXtxTTk= -----END PUBLIC KEY----- ================================================ FILE: Tests/Resources/SUUpdateValidatorTest/CodeSignedInvalid.bundle/Contents/_CodeSignature/CodeResources ================================================ files files2 rules ^Resources/ ^Resources/.*\.lproj/ optional weight 1000 ^Resources/.*\.lproj/locversion.plist$ omit weight 1100 ^Resources/Base\.lproj/ weight 1010 ^version.plist$ rules2 .*\.dSYM($|/) weight 11 ^(.*/)?\.DS_Store$ omit weight 2000 ^(Frameworks|SharedFrameworks|PlugIns|Plug-ins|XPCServices|Helpers|MacOS|Library/(Automator|Spotlight|LoginItems))/ nested weight 10 ^.* ^Info\.plist$ omit weight 20 ^PkgInfo$ omit weight 20 ^Resources/ weight 20 ^Resources/.*\.lproj/ optional weight 1000 ^Resources/.*\.lproj/locversion.plist$ omit weight 1100 ^Resources/Base\.lproj/ weight 1010 ^[^/]+$ nested weight 10 ^embedded\.provisionprofile$ weight 20 ^version\.plist$ weight 20 ================================================ FILE: Tests/Resources/SUUpdateValidatorTest/CodeSignedInvalid.bundle/Contents/_CodeSignature/CodeSignature ================================================ ================================================ FILE: Tests/Resources/SUUpdateValidatorTest/CodeSignedInvalidOnly.bundle/Contents/Info.plist ================================================ CFBundleInfoDictionaryVersion 6.0 CFBundlePackageType BNDL CFBundleSignature ???? CFBundleVersion 1.0 CFBundleIdentifier org.sparkle-project.Sparkle.SUUpdateValidatorTestBundle.CodeSignedInvalidOnly ================================================ FILE: Tests/Resources/SUUpdateValidatorTest/CodeSignedInvalidOnly.bundle/Contents/_CodeSignature/CodeResources ================================================ files files2 rules ^Resources/ ^Resources/.*\.lproj/ optional weight 1000 ^Resources/.*\.lproj/locversion.plist$ omit weight 1100 ^Resources/Base\.lproj/ weight 1010 ^version.plist$ rules2 .*\.dSYM($|/) weight 11 ^(.*/)?\.DS_Store$ omit weight 2000 ^(Frameworks|SharedFrameworks|PlugIns|Plug-ins|XPCServices|Helpers|MacOS|Library/(Automator|Spotlight|LoginItems))/ nested weight 10 ^.* ^Info\.plist$ omit weight 20 ^PkgInfo$ omit weight 20 ^Resources/ weight 20 ^Resources/.*\.lproj/ optional weight 1000 ^Resources/.*\.lproj/locversion.plist$ omit weight 1100 ^Resources/Base\.lproj/ weight 1010 ^[^/]+$ nested weight 10 ^embedded\.provisionprofile$ weight 20 ^version\.plist$ weight 20 ================================================ FILE: Tests/Resources/SUUpdateValidatorTest/CodeSignedInvalidOnly.bundle/Contents/_CodeSignature/CodeSignature ================================================ ================================================ FILE: Tests/Resources/SUUpdateValidatorTest/CodeSignedOldED.bundle/Contents/Info.plist ================================================ CFBundleInfoDictionaryVersion 6.0 CFBundlePackageType BNDL CFBundleSignature ???? CFBundleVersion 1.0 CFBundleIdentifier org.sparkle-project.Sparkle.SUUpdateValidatorTestBundle.CodeSignedOldED SUPublicEDKey OLDKEYw769W2/6/t+oM1ZxgjBB93BfBKMLO0Qo1etQs= ================================================ FILE: Tests/Resources/SUUpdateValidatorTest/CodeSignedOldED.bundle/Contents/_CodeSignature/CodeResources ================================================ files files2 rules ^Resources/ ^Resources/.*\.lproj/ optional weight 1000 ^Resources/.*\.lproj/locversion.plist$ omit weight 1100 ^Resources/Base\.lproj/ weight 1010 ^version.plist$ rules2 .*\.dSYM($|/) weight 11 ^(.*/)?\.DS_Store$ omit weight 2000 ^(Frameworks|SharedFrameworks|PlugIns|Plug-ins|XPCServices|Helpers|MacOS|Library/(Automator|Spotlight|LoginItems))/ nested weight 10 ^.* ^Info\.plist$ omit weight 20 ^PkgInfo$ omit weight 20 ^Resources/ weight 20 ^Resources/.*\.lproj/ optional weight 1000 ^Resources/.*\.lproj/locversion.plist$ omit weight 1100 ^Resources/Base\.lproj/ weight 1010 ^[^/]+$ nested weight 10 ^embedded\.provisionprofile$ weight 20 ^version\.plist$ weight 20 ================================================ FILE: Tests/Resources/SUUpdateValidatorTest/CodeSignedOldED.bundle/Contents/_CodeSignature/CodeSignature ================================================ ================================================ FILE: Tests/Resources/SUUpdateValidatorTest/CodeSignedOnly.bundle/Contents/Info.plist ================================================ CFBundleInfoDictionaryVersion 6.0 CFBundlePackageType BNDL CFBundleSignature ???? CFBundleVersion 1.0 CFBundleIdentifier org.sparkle-project.Sparkle.SUUpdateValidatorTestBundle.CodeSignedOnly ================================================ FILE: Tests/Resources/SUUpdateValidatorTest/CodeSignedOnly.bundle/Contents/_CodeSignature/CodeResources ================================================ files files2 rules ^Resources/ ^Resources/.*\.lproj/ optional weight 1000 ^Resources/.*\.lproj/locversion.plist$ omit weight 1100 ^Resources/Base\.lproj/ weight 1010 ^version.plist$ rules2 .*\.dSYM($|/) weight 11 ^(.*/)?\.DS_Store$ omit weight 2000 ^(Frameworks|SharedFrameworks|PlugIns|Plug-ins|XPCServices|Helpers|MacOS|Library/(Automator|Spotlight|LoginItems))/ nested weight 10 ^.* ^Info\.plist$ omit weight 20 ^PkgInfo$ omit weight 20 ^Resources/ weight 20 ^Resources/.*\.lproj/ optional weight 1000 ^Resources/.*\.lproj/locversion.plist$ omit weight 1100 ^Resources/Base\.lproj/ weight 1010 ^[^/]+$ nested weight 10 ^embedded\.provisionprofile$ weight 20 ^version\.plist$ weight 20 ================================================ FILE: Tests/Resources/SUUpdateValidatorTest/CodeSignedOnly.bundle/Contents/_CodeSignature/CodeSignature ================================================ ================================================ FILE: Tests/Resources/SUUpdateValidatorTest/CodeSignedOnlyNew.bundle/Contents/Info.plist ================================================ CFBundleInfoDictionaryVersion 6.0 CFBundlePackageType BNDL CFBundleSignature ???? CFBundleVersion 1.0 CFBundleIdentifier org.sparkle-project.Sparkle.SUUpdateValidatorTestBundle.CodeSignedOnlyNew ================================================ FILE: Tests/Resources/SUUpdateValidatorTest/CodeSignedOnlyNew.bundle/Contents/_CodeSignature/CodeResources ================================================ files files2 rules ^Resources/ ^Resources/.*\.lproj/ optional weight 1000 ^Resources/.*\.lproj/locversion.plist$ omit weight 1100 ^Resources/Base\.lproj/ weight 1010 ^version.plist$ rules2 .*\.dSYM($|/) weight 11 ^(.*/)?\.DS_Store$ omit weight 2000 ^(Frameworks|SharedFrameworks|PlugIns|Plug-ins|XPCServices|Helpers|MacOS|Library/(Automator|Spotlight|LoginItems))/ nested weight 10 ^.* ^Info\.plist$ omit weight 20 ^PkgInfo$ omit weight 20 ^Resources/ weight 20 ^Resources/.*\.lproj/ optional weight 1000 ^Resources/.*\.lproj/locversion.plist$ omit weight 1100 ^Resources/Base\.lproj/ weight 1010 ^[^/]+$ nested weight 10 ^embedded\.provisionprofile$ weight 20 ^version\.plist$ weight 20 ================================================ FILE: Tests/Resources/SUUpdateValidatorTest/CodeSignedOnlyNew.bundle/Contents/_CodeSignature/CodeSignature ================================================ ================================================ FILE: Tests/Resources/SUUpdateValidatorTest/DSAOnly.bundle/Contents/Info.plist ================================================ CFBundleInfoDictionaryVersion 6.0 CFBundlePackageType BNDL CFBundleSignature ???? CFBundleVersion 1.0 SUPublicDSAKeyFile test-pubkey.pem CFBundleIdentifier org.sparkle-project.Sparkle.SUUpdateValidatorTestBundle.DSAOnly ================================================ FILE: Tests/Resources/SUUpdateValidatorTest/DSAOnly.bundle/Contents/Resources/test-pubkey.pem ================================================ -----BEGIN PUBLIC KEY----- MIIGOjCCBC0GByqGSM44BAEwggQgAoICAQCHyAu13F9I72JZzN9KgVhrprKnRH7n dJJP5ZVHaSqm5PbRmHbnT06DGMxloZc8Nd5QWwG6N+5O1I9ZZjnIrc9Qzodho8hM KNInrfCEy/lTHjydvQvYG2LitbknapgGTf57BWEIYSiAQyP8Gs6lTVZmGrqSJdRC Kdt29OmFKLhWVn5hm2nx7PCbRT7FzrRyX8jkjCUKSeI/s1j3jpjFT5nGutAsSYNS Y/ifH1/IRejv9kRUEWM7qEU/ue3C18qCoDsmTSQVh+E1MIBWqeaWWAGrw7JO4cxR cgR6eeDYp8plTw//e9dSn1u/6ArnQ4QAoZP8Q6MfJMABgg07lltRIQYiXnUNPE4c xzMQ6kxbKLBi2VyeKm/mQ5oO6PH+59Lw0Q1YhchAXCO171fx2s4W2UGcIjdNbyhs My5iSEICwXQQgAvTEC08An8aYi5waEpQ1fnnkQcawTgRoBL2gMNsQOkZdERbJr2X zNJfLwGoDnNJTD8V4FCAeHieOAqmlGsqRc7mKPq2EVW7GH6vzWBJbZc71rmk7EwY 2zvpR9aFam71ivmFgLYwLrRef3vd+AGbIc2WOcIADJ9JIa6HF1gK8LSym/t+/2ws fQ1FHpjHmYZICJqac1SQ6KZhMz9q35yKqBSaNmiv+mXBg7W6Jfckjg8mGgxvTdL+ VDRDlK9K6bxClwIVAPd9bTMe17hl3ipR0X6O2UOmt799AoICABPfIvExHzdmJK+G kx+o28PNNw9ZAte2RxZWm8b0m9MW+jdeWDrhGbbk7nVJn6i6xaaRgJDWuZnKUQbC 0WwjOm9fXnjBmnQhacJu0RGyr/b+akzZ4Y/7N+SBxRjasLVTFd+msQ2NE1PkXps5 rhJWVMu9R9xp0qR/e+rh/Cax7nSq5UNAqSy9gzHkOhjS7s6UJ1yfCEO5Xfcvb7ND RpLIeBYbLT3OSkSd6kkFvMtb0Sl/WZ7z88flkdNGbutAcJVQzBcfQIwCuXyOYzUr 8TcIB5j4c19MN9yfkykgemieC0uz/xzgtZt6g6bFfQNONmJ0YGoEPkz1GftI6dx/ 324vh4KhLfBjKDKFB8Pt9JKI9nQA7P10GlwWeA+IpSTzjlJq3x35u220K7tc/5xr TDMEftBemn/dvb/bJ9h+iSciZ7EcnN2RXB7SfykUAohE9DdEUlmjip6DYLP+hSN2 oHiVmDVjv3XEGYQfATuxQmwcveHTYZmPId8XF2wkGiy6w0BY0NN+4oEuZga4YvEK 3MVF2VEzM0onFzNgszwb+KaUfcA+eycthidca+sJzmOGAMWCx/vydNryMcWVXABF IRxbZVaXHCWf1RhAf0jcsiz9+JIuScAB2J26Oy2shPGOmesvuUHeUTqzvEJ2G77j k9Mps+1fbzySKX3PQEwA/qQ11qT6A4ICBQACggIAbDXVjXtIxAM4TSt9FSep+H1j yRoEXgisf5Q6eU6I7Wf6kAoW6bHw76S/OHgcMBYX7Z42kbVbna/rIAVfHnfrAaZY ygYVqbxTKdhi0c8IxR2qyF9Z5UK68C/EP0SHjHJrFRAnYgwkqvJXbemHFB4c0Ds1 iIL47Kas8o/WvLpT1VzlHXFyFKvRxNMdeJsy8/LBSrQpRUiKcJFM2lg+O8cNWsRd kHTeWnjLrZT2rpPUIkSQZdbR16VBI8nS4pYpzdZ/N2zy+2S5dup7jMNtnLbXVT3X AjSjiYHRZPQUnX0pG3qA0BzDuA0U3MdBs8Wf7YhGd8XLbAfdo5zPSM9GimJrGH0s q4XKLHAEXUPfSuDGOdG8l990MujcRrewocwX5La1X/Nc4TDClCPUOVbf1aqy7LXY TB1M+nHvTb5HhtIrZYSHmsdpWlcLj5mYde/nvFXmNu3RNVCLhMzQDV9S/U1hqNcH m/BX2VTJ0xqhFIlA5UVrKZpnAEISKcFjwmZo7GuA1ENw2vhuvswjfYQ3OMfEr258 0b1tKNMWrkbCgHdALjT/VdRUtte7RyGvsMccUHu6MHPowFRxbT3lWUqj1LKdZYnT uERL4E1J2VIP1/x2upPPWl/wbQjxplsRQrSY+cQCfWM22Wtilouh+CdEc+1DNKs/ bKZWFUcty/GYmXtxTTk= -----END PUBLIC KEY----- ================================================ FILE: Tests/Resources/SUUpdateValidatorTest/EDOnly.bundle/Contents/Info.plist ================================================ CFBundleInfoDictionaryVersion 6.0 CFBundlePackageType BNDL CFBundleSignature ???? CFBundleVersion 1.0 SUPublicEDKey rhHib+w769W2/6/t+oM1ZxgjBB93BfBKMLO0Qo1etQs= CFBundleIdentifier org.sparkle-project.Sparkle.SUUpdateValidatorTestBundle.EDOnly ================================================ FILE: Tests/Resources/SUUpdateValidatorTest/None.bundle/Contents/Info.plist ================================================ CFBundleInfoDictionaryVersion 6.0 CFBundlePackageType BNDL CFBundleSignature ???? CFBundleVersion 1.0 CFBundleIdentifier org.sparkle-project.Sparkle.SUUpdateValidatorTestBundle.None ================================================ FILE: Tests/Resources/SUUpdateValidatorTest/resign-all.sh ================================================ #!/bin/sh here=$(dirname "$0") cd "$here" codesign -f -s - -i org.sparkle-project.Sparkle.SUUpdateValidatorTestBundle.CodeSigned -r='designated => identifier "org.sparkle-project.Sparkle.SUUpdateValidatorTestBundle.CodeSigned"' CodeSignedOnly.bundle CodeSignedBoth.bundle CodeSignedOldED.bundle codesign -f -s - -i org.sparkle-project.Sparkle.SUUpdateValidatorTestBundle.CodeSignedNew -r='designated => identifier "org.sparkle-project.Sparkle.SUUpdateValidatorTestBundle.CodeSignedNew"' CodeSignedOnlyNew.bundle CodeSignedBothNew.bundle for invalidBundle in CodeSignedInvalid.bundle CodeSignedInvalidOnly.bundle; do cp -rf CodeSignedOnly.bundle/Contents/_CodeSignature ${invalidBundle}/Contents echo ${invalidBundle}: copied code signature from CodeSignedOnly.bundle done ================================================ FILE: Tests/Resources/signed-test-file.txt ================================================ Hello World! ================================================ FILE: Tests/Resources/test-dangerous-link.xml ================================================ For unit test only Version 4.0 Sat, 26 Jul 2014 15:20:12 +0000 https://sparkle-project.org/notes/relnote-4.0.txt https://sparkle-project.org/fullnotes.txt https://sparkle-project.org Version 3.0 Sat, 26 Jul 2014 15:20:12 +0000 https://sparkle-project.org/notes/relnote-3.0.txt https://sparkle-project.org/fullnotes.txt https://sparkle-project.org Version 2.0 Sat, 26 Jul 2014 15:20:12 +0000 http://sparkle-project.org/notes/relnote-2.0.txt http://sparkle-project.org/fullnotes.txt http://sparkle-project.org 2.0 2.0 (Visit abc.com for updates) Version 1.0 Sat, 26 Jul 2014 15:20:12 +0000 file://sparkle-project.org/notes/relnote-1.0.txt file://sparkle-project.org/fullnotes.txt file://sparkle-project.org ================================================ FILE: Tests/Resources/test-links.xml ================================================ For unit test only Version 3.0 Sat, 26 Jul 2014 15:20:12 +0000 https://sparkle-project.org/notes/relnote-3.0.txt https://sparkle-project.org/fullnotes.txt https://sparkle-project.org Version 2.0 Sat, 26 Jul 2014 15:20:12 +0000 http://sparkle-project.org/notes/relnote-2.0.txt http://sparkle-project.org/fullnotes.txt http://sparkle-project.org Version 1.0 Sat, 26 Jul 2014 15:20:12 +0000 file://sparkle-project.org/notes/relnote-1.0.txt file://sparkle-project.org/fullnotes.txt file://sparkle-project.org ================================================ FILE: Tests/Resources/test-pubkey.pem ================================================ -----BEGIN PUBLIC KEY----- MIIGOjCCBC0GByqGSM44BAEwggQgAoICAQCHyAu13F9I72JZzN9KgVhrprKnRH7n dJJP5ZVHaSqm5PbRmHbnT06DGMxloZc8Nd5QWwG6N+5O1I9ZZjnIrc9Qzodho8hM KNInrfCEy/lTHjydvQvYG2LitbknapgGTf57BWEIYSiAQyP8Gs6lTVZmGrqSJdRC Kdt29OmFKLhWVn5hm2nx7PCbRT7FzrRyX8jkjCUKSeI/s1j3jpjFT5nGutAsSYNS Y/ifH1/IRejv9kRUEWM7qEU/ue3C18qCoDsmTSQVh+E1MIBWqeaWWAGrw7JO4cxR cgR6eeDYp8plTw//e9dSn1u/6ArnQ4QAoZP8Q6MfJMABgg07lltRIQYiXnUNPE4c xzMQ6kxbKLBi2VyeKm/mQ5oO6PH+59Lw0Q1YhchAXCO171fx2s4W2UGcIjdNbyhs My5iSEICwXQQgAvTEC08An8aYi5waEpQ1fnnkQcawTgRoBL2gMNsQOkZdERbJr2X zNJfLwGoDnNJTD8V4FCAeHieOAqmlGsqRc7mKPq2EVW7GH6vzWBJbZc71rmk7EwY 2zvpR9aFam71ivmFgLYwLrRef3vd+AGbIc2WOcIADJ9JIa6HF1gK8LSym/t+/2ws fQ1FHpjHmYZICJqac1SQ6KZhMz9q35yKqBSaNmiv+mXBg7W6Jfckjg8mGgxvTdL+ VDRDlK9K6bxClwIVAPd9bTMe17hl3ipR0X6O2UOmt799AoICABPfIvExHzdmJK+G kx+o28PNNw9ZAte2RxZWm8b0m9MW+jdeWDrhGbbk7nVJn6i6xaaRgJDWuZnKUQbC 0WwjOm9fXnjBmnQhacJu0RGyr/b+akzZ4Y/7N+SBxRjasLVTFd+msQ2NE1PkXps5 rhJWVMu9R9xp0qR/e+rh/Cax7nSq5UNAqSy9gzHkOhjS7s6UJ1yfCEO5Xfcvb7ND RpLIeBYbLT3OSkSd6kkFvMtb0Sl/WZ7z88flkdNGbutAcJVQzBcfQIwCuXyOYzUr 8TcIB5j4c19MN9yfkykgemieC0uz/xzgtZt6g6bFfQNONmJ0YGoEPkz1GftI6dx/ 324vh4KhLfBjKDKFB8Pt9JKI9nQA7P10GlwWeA+IpSTzjlJq3x35u220K7tc/5xr TDMEftBemn/dvb/bJ9h+iSciZ7EcnN2RXB7SfykUAohE9DdEUlmjip6DYLP+hSN2 oHiVmDVjv3XEGYQfATuxQmwcveHTYZmPId8XF2wkGiy6w0BY0NN+4oEuZga4YvEK 3MVF2VEzM0onFzNgszwb+KaUfcA+eycthidca+sJzmOGAMWCx/vydNryMcWVXABF IRxbZVaXHCWf1RhAf0jcsiz9+JIuScAB2J26Oy2shPGOmesvuUHeUTqzvEJ2G77j k9Mps+1fbzySKX3PQEwA/qQ11qT6A4ICBQACggIAbDXVjXtIxAM4TSt9FSep+H1j yRoEXgisf5Q6eU6I7Wf6kAoW6bHw76S/OHgcMBYX7Z42kbVbna/rIAVfHnfrAaZY ygYVqbxTKdhi0c8IxR2qyF9Z5UK68C/EP0SHjHJrFRAnYgwkqvJXbemHFB4c0Ds1 iIL47Kas8o/WvLpT1VzlHXFyFKvRxNMdeJsy8/LBSrQpRUiKcJFM2lg+O8cNWsRd kHTeWnjLrZT2rpPUIkSQZdbR16VBI8nS4pYpzdZ/N2zy+2S5dup7jMNtnLbXVT3X AjSjiYHRZPQUnX0pG3qA0BzDuA0U3MdBs8Wf7YhGd8XLbAfdo5zPSM9GimJrGH0s q4XKLHAEXUPfSuDGOdG8l990MujcRrewocwX5La1X/Nc4TDClCPUOVbf1aqy7LXY TB1M+nHvTb5HhtIrZYSHmsdpWlcLj5mYde/nvFXmNu3RNVCLhMzQDV9S/U1hqNcH m/BX2VTJ0xqhFIlA5UVrKZpnAEISKcFjwmZo7GuA1ENw2vhuvswjfYQ3OMfEr258 0b1tKNMWrkbCgHdALjT/VdRUtte7RyGvsMccUHu6MHPowFRxbT3lWUqj1LKdZYnT uERL4E1J2VIP1/x2upPPWl/wbQjxplsRQrSY+cQCfWM22Wtilouh+CdEc+1DNKs/ bKZWFUcty/GYmXtxTTk= -----END PUBLIC KEY----- ================================================ FILE: Tests/Resources/test-relative-urls.xml ================================================ For unit test only Version 3.0 Sat, 26 Jul 2014 15:20:12 +0000 notes/relnote-3.0.txt Version 2.0 desc Sat, 26 Jul 2014 15:20:11 +0000 2.0 /info/info-2.0.txt Version 1.0 desc Sat, 26 Jul 2014 15:20:11 +0000 1.0 notes/fullnotes.txt Version 0.9 desc Sat, 26 Jul 2014 15:20:11 +0000 0.9 https://sparkle-project.org/releasenotes.html ================================================ FILE: Tests/Resources/testappcast.xml ================================================ For unit test only Version 2.0 desc Sat, 26 Jul 2014 15:20:11 +0000 Version 3.0 3.0 86400 desc3 Version 4.0 4.0 Sat, 26 Jul 2014 15:20:13 +0000 9999.0.0 Version 5.0 5.0 2.0.0 ================================================ FILE: Tests/Resources/testappcast_arm64HardwareRequirement.xml ================================================ For unit test only Version 6.0 6.0 27.0 Version 5.0 5.0 arm64,fakerequirement Version 4.0 4.0 ARM64 Version 3.0 3.0 arm64 Version 2.1 2.1 Version 2.0 2.0 ================================================ FILE: Tests/Resources/testappcast_channels.xml ================================================ For unit test only Version 2.0 desc Sat, 26 Jul 2014 15:20:11 +0000 Version 3.0 Version 4.0 4.0 Sat, 26 Jul 2014 15:20:13 +0000 beta Version 5.0 5.0 nightly Version 6.0 6.0 Version 7.0 7.0 ================================================ FILE: Tests/Resources/testappcast_info_updates.xml ================================================ For unit test only Version 2.0 desc Sat, 26 Jul 2014 15:20:11 +0000 Version 3.0 2.5 2.4 0.5 http://sparkle-project.org Version 4.0 Sat, 26 Jul 2014 15:20:13 +0000 4.0 http://sparkle-project.org Version 5.0 5.0 http://sparkle-project.org ================================================ FILE: Tests/Resources/testappcast_minimumAutoupdateVersion.xml ================================================ For unit test only Version 3.0 3.0 2.0 Version 2.0 2.0 ================================================ FILE: Tests/Resources/testappcast_minimumAutoupdateVersionSkipping.xml ================================================ For unit test only Version 4.1 4.1 4.0 Version 4.0 4.0 4.0 Version 3.9 3.9 3.0 Version 3.0 3.0 3.0 Version 2.0 2.0 ================================================ FILE: Tests/Resources/testappcast_minimumAutoupdateVersionSkipping2.xml ================================================ For unit test only Version 4.1 4.1 4.0 Version 4.0 4.0 4.0 Version 3.9 3.9 3.0 3.5 Version 3.0 3.0 3.0 Version 2.0 2.0 2.0 ================================================ FILE: Tests/Resources/testappcast_minimumUpdateVersion.xml ================================================ For unit test only Version 2.0 desc Sat, 26 Jul 2014 15:20:11 +0000 Version 3.0 0.9.1 Version 4.0 4.0 Sat, 26 Jul 2014 15:20:13 +0000 1.0.0 Version 5.0 5.0 Sat, 26 Jul 2014 15:20:13 +0000 4.0 ================================================ FILE: Tests/Resources/testappcast_phasedRollout.xml ================================================ For unit test only Version 3.0 desc Sat, 26 Jul 2014 15:20:11 +0000 3.0 2.0 86400 Version 2.0 desc Sat, 26 Jul 2014 15:20:11 +0000 2.0 86400 ================================================ FILE: Tests/Resources/testlocalizedreleasenotesappcast.xml ================================================ For unit test only Version 6.3 6.3 english-later-implicit Sat, 26 Jul 2019 15:20:13 +0000 https://sparkle-project.org/notes.es.html https://sparkle-project.org/notes.en.html 2.0.0 Version 6.2 6.2 english-first-implicit Sat, 26 Jul 2019 15:20:13 +0000 https://sparkle-project.org/notes.en.html https://sparkle-project.org/notes.es.html 2.0.0 Version 6.1 6.1 english-first Sat, 26 Jul 2019 15:20:13 +0000 https://sparkle-project.org/notes.en.html https://sparkle-project.org/notes.es.html 2.0.0 Version 6.0 6.0 english-later Sat, 26 Jul 2019 15:20:13 +0000 https://sparkle-project.org/notes.es.html https://sparkle-project.org/notes.en.html 2.0.0 ================================================ FILE: Tests/Resources/testnamespaces.xml ================================================ For unit test only Version 3.0 Sat, 26 Jul 2014 15:20:12 +0000 https://sparkle-project.org/#works Version 2.0 desc Sat, 26 Jul 2014 15:20:11 +0000 ================================================ FILE: Tests/Resources/testreleasenotes.html ================================================

    1.8 Changes

    • Add some stuff
    • Fix some stuff
    • Update some stuff
    ================================================ FILE: Tests/SUAppcastTest.swift ================================================ // // SUAppcastTest.swift // Sparkle // // Created by Kornel on 17/02/2016. // Copyright © 2016 Sparkle Project. All rights reserved. // import XCTest class SUAppcastTest: XCTestCase { func testParseAppcast() { let testURL = Bundle(for: SUAppcastTest.self).url(forResource: "testappcast", withExtension: "xml")! do { let testData = try Data(contentsOf: testURL) let versionComparator = SUStandardVersionComparator.default let hostVersion = "1.0" let stateResolver = SPUAppcastItemStateResolver(hostVersion: hostVersion, applicationVersionComparator: versionComparator, standardVersionComparator: versionComparator) let appcast = try SUAppcast(xmlData: testData, relativeTo: nil, stateResolver: stateResolver, signingValidationStatus: .skipped) let items = appcast.items XCTAssertEqual(4, items.count) XCTAssertEqual("Version 2.0", items[0].title) XCTAssertEqual("desc", items[0].itemDescription) XCTAssertEqual("plain-text", items[0].itemDescriptionFormat) XCTAssertEqual("Sat, 26 Jul 2014 15:20:11 +0000", items[0].dateString) XCTAssertTrue(items[0].isCriticalUpdate) XCTAssertEqual(items[0].versionString, "2.0") // This is the best release matching our system version XCTAssertEqual("Version 3.0", items[1].title) XCTAssertEqual("desc3", items[1].itemDescription) XCTAssertEqual("html", items[1].itemDescriptionFormat) XCTAssertNil(items[1].dateString) XCTAssertTrue(items[1].isCriticalUpdate) XCTAssertEqual(items[1].phasedRolloutInterval, 86400) XCTAssertEqual(items[1].versionString, "3.0") XCTAssertEqual("Version 4.0", items[2].title) XCTAssertNil(items[2].itemDescription) XCTAssertEqual("Sat, 26 Jul 2014 15:20:13 +0000", items[2].dateString) XCTAssertFalse(items[2].isCriticalUpdate) XCTAssertEqual("Version 5.0", items[3].title) XCTAssertNil(items[3].itemDescription) XCTAssertNil(items[3].dateString) XCTAssertFalse(items[3].isCriticalUpdate) // Test best appcast item & a delta update item let currentDate = Date() let supportedAppcast = SUAppcastDriver.filterSupportedAppcast(appcast, phasedUpdateGroup: nil, skippedUpdate: nil, currentDate: currentDate, hostVersion: hostVersion, versionComparator: versionComparator, testMinimumSystemRequirements: true, testMinimumAutoupdateVersion: false) let supportedAppcastItems = supportedAppcast.items var deltaItem: SUAppcastItem? let bestAppcastItem = SUAppcastDriver.bestItem(fromAppcastItems: supportedAppcastItems, getDeltaItem: &deltaItem, withHostVersion: "1.0", comparator: SUStandardVersionComparator()) XCTAssertEqual(bestAppcastItem, items[1]) XCTAssertEqual(deltaItem!.fileURL!.lastPathComponent, "3.0_from_1.0.patch") XCTAssertEqual(deltaItem!.versionString, "3.0") // Test latest delta update item available var latestDeltaItem: SUAppcastItem? SUAppcastDriver.bestItem(fromAppcastItems: supportedAppcastItems, getDeltaItem: &latestDeltaItem, withHostVersion: "2.0", comparator: SUStandardVersionComparator()) XCTAssertEqual(latestDeltaItem!.fileURL!.lastPathComponent, "3.0_from_2.0.patch") // Test a delta item that does not exist var nonexistentDeltaItem: SUAppcastItem? SUAppcastDriver.bestItem(fromAppcastItems: supportedAppcastItems, getDeltaItem: &nonexistentDeltaItem, withHostVersion: "2.1", comparator: SUStandardVersionComparator()) XCTAssertNil(nonexistentDeltaItem) } catch let err as NSError { NSLog("%@", err) XCTFail(err.localizedDescription) } } func testChannelsAndMacOSReleases() { let testURL = Bundle(for: SUAppcastTest.self).url(forResource: "testappcast_channels", withExtension: "xml")! do { let testData = try Data(contentsOf: testURL) let versionComparator = SUStandardVersionComparator.default let hostVersion = "1.0" let stateResolver = SPUAppcastItemStateResolver(hostVersion: hostVersion, applicationVersionComparator: versionComparator, standardVersionComparator: versionComparator) let appcast = try SUAppcast(xmlData: testData, relativeTo: nil, stateResolver: stateResolver, signingValidationStatus: .skipped) XCTAssertEqual(6, appcast.items.count) do { let filteredAppcast = SUAppcastDriver.filterAppcast(appcast, forMacOSAndAllowedChannels: ["beta", "nightly"]) XCTAssertEqual(4, filteredAppcast.items.count) XCTAssertEqual("2.0", filteredAppcast.items[0].versionString) XCTAssertEqual("3.0", filteredAppcast.items[1].versionString) XCTAssertEqual("4.0", filteredAppcast.items[2].versionString) XCTAssertEqual("5.0", filteredAppcast.items[3].versionString) } do { let filteredAppcast = SUAppcastDriver.filterAppcast(appcast, forMacOSAndAllowedChannels: []) XCTAssertEqual(2, filteredAppcast.items.count) XCTAssertEqual("2.0", filteredAppcast.items[0].versionString) XCTAssertEqual("3.0", filteredAppcast.items[1].versionString) } do { let filteredAppcast = SUAppcastDriver.filterAppcast(appcast, forMacOSAndAllowedChannels: ["beta"]) XCTAssertEqual(3, filteredAppcast.items.count) XCTAssertEqual("2.0", filteredAppcast.items[0].versionString) XCTAssertEqual("3.0", filteredAppcast.items[1].versionString) XCTAssertEqual("4.0", filteredAppcast.items[2].versionString) } do { let filteredAppcast = SUAppcastDriver.filterAppcast(appcast, forMacOSAndAllowedChannels: ["nightly"]) XCTAssertEqual(3, filteredAppcast.items.count) XCTAssertEqual("2.0", filteredAppcast.items[0].versionString) XCTAssertEqual("3.0", filteredAppcast.items[1].versionString) XCTAssertEqual("5.0", filteredAppcast.items[2].versionString) } do { let filteredAppcast = SUAppcastDriver.filterAppcast(appcast, forMacOSAndAllowedChannels: ["madeup"]) XCTAssertEqual("2.0", filteredAppcast.items[0].versionString) XCTAssertEqual("3.0", filteredAppcast.items[1].versionString) XCTAssertEqual(2, filteredAppcast.items.count) } } catch let err as NSError { NSLog("%@", err) XCTFail(err.localizedDescription) } } func testMinimumUpdateVersion() { let testURL = Bundle(for: SUAppcastTest.self).url(forResource: "testappcast_minimumUpdateVersion", withExtension: "xml")! do { let testData = try Data(contentsOf: testURL) let versionComparator = SUStandardVersionComparator.default let hostVersion = "1.0" let stateResolver = SPUAppcastItemStateResolver(hostVersion: hostVersion, applicationVersionComparator: versionComparator, standardVersionComparator: versionComparator) let appcast = try SUAppcast(xmlData: testData, relativeTo: nil, stateResolver: stateResolver, signingValidationStatus: .skipped) XCTAssertEqual(4, appcast.items.count) do { let filteredAppcast = SUAppcastDriver.filterAppcast(appcast, forMacOSAndAllowedChannels: []) XCTAssertEqual(3, filteredAppcast.items.count) XCTAssertEqual("2.0", filteredAppcast.items[0].versionString) XCTAssertEqual("3.0", filteredAppcast.items[1].versionString) XCTAssertEqual("4.0", filteredAppcast.items[2].versionString) } } catch let err as NSError { NSLog("%@", err) XCTFail(err.localizedDescription) } } func testCriticalUpdateVersion() { let testURL = Bundle(for: SUAppcastTest.self).url(forResource: "testappcast", withExtension: "xml")! do { let testData = try Data(contentsOf: testURL) let versionComparator = SUStandardVersionComparator.default // If critical update version is 1.5 and host version is 1.0, update should be marked critical do { let hostVersion = "1.0" let stateResolver = SPUAppcastItemStateResolver(hostVersion: hostVersion, applicationVersionComparator: versionComparator, standardVersionComparator: versionComparator) let appcast = try SUAppcast(xmlData: testData, relativeTo: nil, stateResolver: stateResolver, signingValidationStatus: .skipped) XCTAssertTrue(appcast.items[0].isCriticalUpdate) } // If critical update version is 1.5 and host version is 1.5, update should not be marked critical do { let hostVersion = "1.5" let stateResolver = SPUAppcastItemStateResolver(hostVersion: hostVersion, applicationVersionComparator: versionComparator, standardVersionComparator: versionComparator) let appcast = try SUAppcast(xmlData: testData, relativeTo: nil, stateResolver: stateResolver, signingValidationStatus: .skipped) XCTAssertFalse(appcast.items[0].isCriticalUpdate) } // If critical update version is 1.5 and host version is 1.6, update should not be marked critical do { let hostVersion = "1.6" let stateResolver = SPUAppcastItemStateResolver(hostVersion: hostVersion, applicationVersionComparator: versionComparator, standardVersionComparator: versionComparator) let appcast = try SUAppcast(xmlData: testData, relativeTo: nil, stateResolver: stateResolver, signingValidationStatus: .skipped) XCTAssertFalse(appcast.items[0].isCriticalUpdate) } } catch let err as NSError { NSLog("%@", err) XCTFail(err.localizedDescription) } } func testInformationalUpdateVersions() { let testURL = Bundle(for: SUAppcastTest.self).url(forResource: "testappcast_info_updates", withExtension: "xml")! do { let testData = try Data(contentsOf: testURL) let versionComparator = SUStandardVersionComparator.default // Test informational updates from version 1.0 do { let hostVersion = "1.0" let stateResolver = SPUAppcastItemStateResolver(hostVersion: hostVersion, applicationVersionComparator: versionComparator, standardVersionComparator: versionComparator) let appcast = try SUAppcast(xmlData: testData, relativeTo: nil, stateResolver: stateResolver, signingValidationStatus: .skipped) XCTAssertFalse(appcast.items[0].isInformationOnlyUpdate) XCTAssertFalse(appcast.items[1].isInformationOnlyUpdate) XCTAssertTrue(appcast.items[2].isInformationOnlyUpdate) XCTAssertTrue(appcast.items[3].isInformationOnlyUpdate) // Test delta updates inheriting informational only updates do { let deltaUpdate = appcast.items[2].deltaUpdates!["2.0"]! XCTAssertTrue(deltaUpdate.isInformationOnlyUpdate) } } // Test informational updates from version 2.3 do { let hostVersion = "2.3" let stateResolver = SPUAppcastItemStateResolver(hostVersion: hostVersion, applicationVersionComparator: versionComparator, standardVersionComparator: versionComparator) let appcast = try SUAppcast(xmlData: testData, relativeTo: nil, stateResolver: stateResolver, signingValidationStatus: .skipped) XCTAssertFalse(appcast.items[1].isInformationOnlyUpdate) } // Test informational updates from version 2.4 do { let hostVersion = "2.4" let stateResolver = SPUAppcastItemStateResolver(hostVersion: hostVersion, applicationVersionComparator: versionComparator, standardVersionComparator: versionComparator) let appcast = try SUAppcast(xmlData: testData, relativeTo: nil, stateResolver: stateResolver, signingValidationStatus: .skipped) XCTAssertTrue(appcast.items[1].isInformationOnlyUpdate) } // Test informational updates from version 2.5 do { let hostVersion = "2.5" let stateResolver = SPUAppcastItemStateResolver(hostVersion: hostVersion, applicationVersionComparator: versionComparator, standardVersionComparator: versionComparator) let appcast = try SUAppcast(xmlData: testData, relativeTo: nil, stateResolver: stateResolver, signingValidationStatus: .skipped) XCTAssertTrue(appcast.items[1].isInformationOnlyUpdate) } // Test informational updates from version 2.6 do { let hostVersion = "2.6" let stateResolver = SPUAppcastItemStateResolver(hostVersion: hostVersion, applicationVersionComparator: versionComparator, standardVersionComparator: versionComparator) let appcast = try SUAppcast(xmlData: testData, relativeTo: nil, stateResolver: stateResolver, signingValidationStatus: .skipped) XCTAssertFalse(appcast.items[1].isInformationOnlyUpdate) } // Test informational updates from version 0.5 do { let hostVersion = "0.5" let stateResolver = SPUAppcastItemStateResolver(hostVersion: hostVersion, applicationVersionComparator: versionComparator, standardVersionComparator: versionComparator) let appcast = try SUAppcast(xmlData: testData, relativeTo: nil, stateResolver: stateResolver, signingValidationStatus: .skipped) XCTAssertFalse(appcast.items[1].isInformationOnlyUpdate) } // Test informational updates from version 0.4 do { let hostVersion = "0.4" let stateResolver = SPUAppcastItemStateResolver(hostVersion: hostVersion, applicationVersionComparator: versionComparator, standardVersionComparator: versionComparator) let appcast = try SUAppcast(xmlData: testData, relativeTo: nil, stateResolver: stateResolver, signingValidationStatus: .skipped) XCTAssertTrue(appcast.items[1].isInformationOnlyUpdate) } // Test informational updates from version 0.0 do { let hostVersion = "0.0" let stateResolver = SPUAppcastItemStateResolver(hostVersion: hostVersion, applicationVersionComparator: versionComparator, standardVersionComparator: versionComparator) let appcast = try SUAppcast(xmlData: testData, relativeTo: nil, stateResolver: stateResolver, signingValidationStatus: .skipped) XCTAssertTrue(appcast.items[1].isInformationOnlyUpdate) } } catch let err as NSError { NSLog("%@", err) XCTFail(err.localizedDescription) } } func testMinimumAutoupdateVersion() { let testURL = Bundle(for: SUAppcastTest.self).url(forResource: "testappcast_minimumAutoupdateVersion", withExtension: "xml")! do { let testData = try Data(contentsOf: testURL) let versionComparator = SUStandardVersionComparator() do { // Test appcast without a filter let hostVersion = "1.0" let stateResolver = SPUAppcastItemStateResolver(hostVersion: hostVersion, applicationVersionComparator: versionComparator, standardVersionComparator: versionComparator) let appcast = try SUAppcast(xmlData: testData, relativeTo: nil, stateResolver: stateResolver, signingValidationStatus: .skipped) XCTAssertEqual(2, appcast.items.count) } let currentDate = Date() // Because 3.0 has minimum autoupdate version of 2.0, we should be offered 2.0 do { let hostVersion = "1.0" let stateResolver = SPUAppcastItemStateResolver(hostVersion: hostVersion, applicationVersionComparator: versionComparator, standardVersionComparator: versionComparator) let appcast = try SUAppcast(xmlData: testData, relativeTo: nil, stateResolver: stateResolver, signingValidationStatus: .skipped) let supportedAppcast = SUAppcastDriver.filterSupportedAppcast(appcast, phasedUpdateGroup: nil, skippedUpdate: nil, currentDate: currentDate, hostVersion: hostVersion, versionComparator: versionComparator, testMinimumSystemRequirements: true, testMinimumAutoupdateVersion: true) XCTAssertEqual(1, supportedAppcast.items.count) let bestAppcastItem = SUAppcastDriver.bestItem(fromAppcastItems: supportedAppcast.items, getDeltaItem: nil, withHostVersion: hostVersion, comparator: versionComparator) XCTAssertEqual(bestAppcastItem.versionString, "2.0") } // We should be offered 3.0 if host version is 2.0 do { let hostVersion = "2.0" let stateResolver = SPUAppcastItemStateResolver(hostVersion: hostVersion, applicationVersionComparator: versionComparator, standardVersionComparator: versionComparator) let appcast = try SUAppcast(xmlData: testData, relativeTo: nil, stateResolver: stateResolver, signingValidationStatus: .skipped) let supportedAppcast = SUAppcastDriver.filterSupportedAppcast(appcast, phasedUpdateGroup: nil, skippedUpdate: nil, currentDate: currentDate, hostVersion: hostVersion, versionComparator: versionComparator, testMinimumSystemRequirements: true, testMinimumAutoupdateVersion: true) XCTAssertEqual(2, supportedAppcast.items.count) let bestAppcastItem = SUAppcastDriver.bestItem(fromAppcastItems: supportedAppcast.items, getDeltaItem: nil, withHostVersion: hostVersion, comparator: versionComparator) XCTAssertEqual(bestAppcastItem.versionString, "3.0") } // We should be offered 3.0 if host version is 2.5 do { let hostVersion = "2.5" let stateResolver = SPUAppcastItemStateResolver(hostVersion: hostVersion, applicationVersionComparator: versionComparator, standardVersionComparator: versionComparator) let appcast = try SUAppcast(xmlData: testData, relativeTo: nil, stateResolver: stateResolver, signingValidationStatus: .skipped) let supportedAppcast = SUAppcastDriver.filterSupportedAppcast(appcast, phasedUpdateGroup: nil, skippedUpdate: nil, currentDate: currentDate, hostVersion: hostVersion, versionComparator: versionComparator, testMinimumSystemRequirements: true, testMinimumAutoupdateVersion: true) XCTAssertEqual(2, supportedAppcast.items.count) let bestAppcastItem = SUAppcastDriver.bestItem(fromAppcastItems: supportedAppcast.items, getDeltaItem: nil, withHostVersion: hostVersion, comparator: versionComparator) XCTAssertEqual(bestAppcastItem.versionString, "3.0") } // Because 3.0 has minimum autoupdate version of 2.0, we would be be offered 2.0, but not if it has been skipped do { let hostVersion = "1.0" let stateResolver = SPUAppcastItemStateResolver(hostVersion: hostVersion, applicationVersionComparator: versionComparator, standardVersionComparator: versionComparator) let appcast = try SUAppcast(xmlData: testData, relativeTo: nil, stateResolver: stateResolver, signingValidationStatus: .skipped) // There should be no items if 2.0 is skipped from 1.0 and 3.0 fails minimum autoupdate version do { let skippedUpdate = SPUSkippedUpdate(minorVersion: "2.0", majorVersion: nil, majorSubreleaseVersion: nil) let supportedAppcast = SUAppcastDriver.filterSupportedAppcast(appcast, phasedUpdateGroup: nil, skippedUpdate: skippedUpdate, currentDate: currentDate, hostVersion: hostVersion, versionComparator: versionComparator, testMinimumSystemRequirements: true, testMinimumAutoupdateVersion: true) XCTAssertEqual(0, supportedAppcast.items.count) } // Try again but allowing minimum autoupdate version to fail do { let skippedUpdate = SPUSkippedUpdate(minorVersion: "2.0", majorVersion: nil, majorSubreleaseVersion: nil) let supportedAppcast = SUAppcastDriver.filterSupportedAppcast(appcast, phasedUpdateGroup: nil, skippedUpdate: skippedUpdate, currentDate: currentDate, hostVersion: hostVersion, versionComparator: versionComparator, testMinimumSystemRequirements: true, testMinimumAutoupdateVersion: false) XCTAssertEqual(1, supportedAppcast.items.count) let bestAppcastItem = SUAppcastDriver.bestItem(fromAppcastItems: supportedAppcast.items, getDeltaItem: nil, withHostVersion: hostVersion, comparator: versionComparator) XCTAssertEqual(bestAppcastItem.versionString, "3.0") } // Allow minimum autoupdate version to fail and only skip 3.0 do { let skippedUpdate = SPUSkippedUpdate(minorVersion: nil, majorVersion: "3.0", majorSubreleaseVersion: nil) let supportedAppcast = SUAppcastDriver.filterSupportedAppcast(appcast, phasedUpdateGroup: nil, skippedUpdate: skippedUpdate, currentDate: currentDate, hostVersion: hostVersion, versionComparator: versionComparator, testMinimumSystemRequirements: true, testMinimumAutoupdateVersion: false) XCTAssertEqual(1, supportedAppcast.items.count) let bestAppcastItem = SUAppcastDriver.bestItem(fromAppcastItems: supportedAppcast.items, getDeltaItem: nil, withHostVersion: hostVersion, comparator: versionComparator) XCTAssertEqual(bestAppcastItem.versionString, "2.0") } // Allow minimum autoupdate version to fail skipping both 2.0 and 3.0 do { let skippedUpdate = SPUSkippedUpdate(minorVersion: "2.0", majorVersion: "3.0", majorSubreleaseVersion: nil) let supportedAppcast = SUAppcastDriver.filterSupportedAppcast(appcast, phasedUpdateGroup: nil, skippedUpdate: skippedUpdate, currentDate: currentDate, hostVersion: hostVersion, versionComparator: versionComparator, testMinimumSystemRequirements: true, testMinimumAutoupdateVersion: false) XCTAssertEqual(0, supportedAppcast.items.count) } // Allow minimum autoupdate version to fail and only skip "2.5" // This should implicitly only skip 2.0 do { let skippedUpdate = SPUSkippedUpdate(minorVersion: "2.5", majorVersion: nil, majorSubreleaseVersion: nil) let supportedAppcast = SUAppcastDriver.filterSupportedAppcast(appcast, phasedUpdateGroup: nil, skippedUpdate: skippedUpdate, currentDate: currentDate, hostVersion: hostVersion, versionComparator: versionComparator, testMinimumSystemRequirements: true, testMinimumAutoupdateVersion: false) XCTAssertEqual(1, supportedAppcast.items.count) let bestAppcastItem = SUAppcastDriver.bestItem(fromAppcastItems: supportedAppcast.items, getDeltaItem: nil, withHostVersion: hostVersion, comparator: versionComparator) XCTAssertEqual(bestAppcastItem.versionString, "3.0") } // This should not skip anything but require passing minimum autoupdate version do { let skippedUpdate = SPUSkippedUpdate(minorVersion: "1.5", majorVersion: nil, majorSubreleaseVersion: nil) let supportedAppcast = SUAppcastDriver.filterSupportedAppcast(appcast, phasedUpdateGroup: nil, skippedUpdate: skippedUpdate, currentDate: currentDate, hostVersion: hostVersion, versionComparator: versionComparator, testMinimumSystemRequirements: true, testMinimumAutoupdateVersion: true) XCTAssertEqual(1, supportedAppcast.items.count) let bestAppcastItem = SUAppcastDriver.bestItem(fromAppcastItems: supportedAppcast.items, getDeltaItem: nil, withHostVersion: hostVersion, comparator: versionComparator) XCTAssertEqual(bestAppcastItem.versionString, "2.0") } // This should not skip anything but allow failing minimum autoupdate version do { let skippedUpdate = SPUSkippedUpdate(minorVersion: "1.5", majorVersion: nil, majorSubreleaseVersion: nil) let supportedAppcast = SUAppcastDriver.filterSupportedAppcast(appcast, phasedUpdateGroup: nil, skippedUpdate: skippedUpdate, currentDate: currentDate, hostVersion: hostVersion, versionComparator: versionComparator, testMinimumSystemRequirements: true, testMinimumAutoupdateVersion: false) XCTAssertEqual(2, supportedAppcast.items.count) let bestAppcastItem = SUAppcastDriver.bestItem(fromAppcastItems: supportedAppcast.items, getDeltaItem: nil, withHostVersion: hostVersion, comparator: versionComparator) XCTAssertEqual(bestAppcastItem.versionString, "3.0") } // This should not skip anything but require passing minimum autoupdate version do { let skippedUpdate = SPUSkippedUpdate(minorVersion: "1.5", majorVersion: "1.0", majorSubreleaseVersion: nil) let supportedAppcast = SUAppcastDriver.filterSupportedAppcast(appcast, phasedUpdateGroup: nil, skippedUpdate: skippedUpdate, currentDate: currentDate, hostVersion: hostVersion, versionComparator: versionComparator, testMinimumSystemRequirements: true, testMinimumAutoupdateVersion: true) XCTAssertEqual(1, supportedAppcast.items.count) let bestAppcastItem = SUAppcastDriver.bestItem(fromAppcastItems: supportedAppcast.items, getDeltaItem: nil, withHostVersion: hostVersion, comparator: versionComparator) XCTAssertEqual(bestAppcastItem.versionString, "2.0") } // This should not skip anything but allow failing minimum autoupdate version do { let skippedUpdate = SPUSkippedUpdate(minorVersion: "1.5", majorVersion: "1.0", majorSubreleaseVersion: nil) let supportedAppcast = SUAppcastDriver.filterSupportedAppcast(appcast, phasedUpdateGroup: nil, skippedUpdate: skippedUpdate, currentDate: currentDate, hostVersion: hostVersion, versionComparator: versionComparator, testMinimumSystemRequirements: true, testMinimumAutoupdateVersion: false) XCTAssertEqual(2, supportedAppcast.items.count) let bestAppcastItem = SUAppcastDriver.bestItem(fromAppcastItems: supportedAppcast.items, getDeltaItem: nil, withHostVersion: hostVersion, comparator: versionComparator) XCTAssertEqual(bestAppcastItem.versionString, "3.0") } } } catch let err as NSError { NSLog("%@", err) XCTFail(err.localizedDescription) } } func testMinimumAutoupdateVersionAdvancedSkipping() { let testURL = Bundle(for: SUAppcastTest.self).url(forResource: "testappcast_minimumAutoupdateVersionSkipping", withExtension: "xml")! do { let testData = try Data(contentsOf: testURL) let versionComparator = SUStandardVersionComparator() do { // Test appcast without a filter let hostVersion = "1.0" let stateResolver = SPUAppcastItemStateResolver(hostVersion: hostVersion, applicationVersionComparator: versionComparator, standardVersionComparator: versionComparator) let appcast = try SUAppcast(xmlData: testData, relativeTo: nil, stateResolver: stateResolver, signingValidationStatus: .skipped) XCTAssertEqual(5, appcast.items.count) } let currentDate = Date() // Because 3.0 has minimum autoupdate version of 3.0, and 4.0 has minimum autoupdate version of 4.0, we should be offered 2.0 do { let hostVersion = "1.0" let stateResolver = SPUAppcastItemStateResolver(hostVersion: hostVersion, applicationVersionComparator: versionComparator, standardVersionComparator: versionComparator) let appcast = try SUAppcast(xmlData: testData, relativeTo: nil, stateResolver: stateResolver, signingValidationStatus: .skipped) let supportedAppcast = SUAppcastDriver.filterSupportedAppcast(appcast, phasedUpdateGroup: nil, skippedUpdate: nil, currentDate: currentDate, hostVersion: hostVersion, versionComparator: versionComparator, testMinimumSystemRequirements: true, testMinimumAutoupdateVersion: true) XCTAssertEqual(1, supportedAppcast.items.count) let bestAppcastItem = SUAppcastDriver.bestItem(fromAppcastItems: supportedAppcast.items, getDeltaItem: nil, withHostVersion: hostVersion, comparator: versionComparator) XCTAssertEqual(bestAppcastItem.versionString, "2.0") } // Allow minimum autoupdate version to fail and only skip major version "3.0" // This should skip all 3.x versions, but not 4.x versions nor 2.x versions do { let hostVersion = "1.0" let stateResolver = SPUAppcastItemStateResolver(hostVersion: hostVersion, applicationVersionComparator: versionComparator, standardVersionComparator: versionComparator) let appcast = try SUAppcast(xmlData: testData, relativeTo: nil, stateResolver: stateResolver, signingValidationStatus: .skipped) let skippedUpdate = SPUSkippedUpdate(minorVersion: nil, majorVersion: "3.0", majorSubreleaseVersion: nil) let supportedAppcast = SUAppcastDriver.filterSupportedAppcast(appcast, phasedUpdateGroup: nil, skippedUpdate: skippedUpdate, currentDate: currentDate, hostVersion: hostVersion, versionComparator: versionComparator, testMinimumSystemRequirements: true, testMinimumAutoupdateVersion: false) XCTAssertEqual(3, supportedAppcast.items.count) let bestAppcastItem = SUAppcastDriver.bestItem(fromAppcastItems: supportedAppcast.items, getDeltaItem: nil, withHostVersion: hostVersion, comparator: versionComparator) XCTAssertEqual(bestAppcastItem.versionString, "4.1") } // Allow minimum autoupdate version to pass and only skip major version "3.0" // This should only return back the latest minor version available do { let hostVersion = "1.0" let stateResolver = SPUAppcastItemStateResolver(hostVersion: hostVersion, applicationVersionComparator: versionComparator, standardVersionComparator: versionComparator) let appcast = try SUAppcast(xmlData: testData, relativeTo: nil, stateResolver: stateResolver, signingValidationStatus: .skipped) let skippedUpdate = SPUSkippedUpdate(minorVersion: nil, majorVersion: "3.0", majorSubreleaseVersion: nil) let supportedAppcast = SUAppcastDriver.filterSupportedAppcast(appcast, phasedUpdateGroup: nil, skippedUpdate: skippedUpdate, currentDate: currentDate, hostVersion: hostVersion, versionComparator: versionComparator, testMinimumSystemRequirements: true, testMinimumAutoupdateVersion: true) XCTAssertEqual(1, supportedAppcast.items.count) let bestAppcastItem = SUAppcastDriver.bestItem(fromAppcastItems: supportedAppcast.items, getDeltaItem: nil, withHostVersion: hostVersion, comparator: versionComparator) XCTAssertEqual(bestAppcastItem.versionString, "2.0") } // Allow minimum autoupdate version to fail and only skip major version "4.0" // This should skip all 3.x versions and 4.x versions but not 2.x versions do { let hostVersion = "1.0" let stateResolver = SPUAppcastItemStateResolver(hostVersion: hostVersion, applicationVersionComparator: versionComparator, standardVersionComparator: versionComparator) let appcast = try SUAppcast(xmlData: testData, relativeTo: nil, stateResolver: stateResolver, signingValidationStatus: .skipped) let skippedUpdate = SPUSkippedUpdate(minorVersion: nil, majorVersion: "4.0", majorSubreleaseVersion: nil) let supportedAppcast = SUAppcastDriver.filterSupportedAppcast(appcast, phasedUpdateGroup: nil, skippedUpdate: skippedUpdate, currentDate: currentDate, hostVersion: hostVersion, versionComparator: versionComparator, testMinimumSystemRequirements: true, testMinimumAutoupdateVersion: false) XCTAssertEqual(1, supportedAppcast.items.count) let bestAppcastItem = SUAppcastDriver.bestItem(fromAppcastItems: supportedAppcast.items, getDeltaItem: nil, withHostVersion: hostVersion, comparator: versionComparator) XCTAssertEqual(bestAppcastItem.versionString, "2.0") } } catch let err as NSError { NSLog("%@", err) XCTFail(err.localizedDescription) } } func testMinimumAutoupdateVersionIgnoringSkipping() { let testURL = Bundle(for: SUAppcastTest.self).url(forResource: "testappcast_minimumAutoupdateVersionSkipping2", withExtension: "xml")! do { let testData = try Data(contentsOf: testURL) let versionComparator = SUStandardVersionComparator() let currentDate = Date() do { let hostVersion = "1.0" let stateResolver = SPUAppcastItemStateResolver(hostVersion: hostVersion, applicationVersionComparator: versionComparator, standardVersionComparator: versionComparator) let appcast = try SUAppcast(xmlData: testData, relativeTo: nil, stateResolver: stateResolver, signingValidationStatus: .skipped) // Allow minimum autoupdate version to fail and only skip major version "3.0" with no subrelease version // This should skip all 3.x versions except for 3.9 which ignores skipped upgrades below 3.5, but not 4.x versions nor 2.x versions do { let skippedUpdate = SPUSkippedUpdate(minorVersion: nil, majorVersion: "3.0", majorSubreleaseVersion: nil) let supportedAppcast = SUAppcastDriver.filterSupportedAppcast(appcast, phasedUpdateGroup: nil, skippedUpdate: skippedUpdate, currentDate: currentDate, hostVersion: hostVersion, versionComparator: versionComparator, testMinimumSystemRequirements: true, testMinimumAutoupdateVersion: false) XCTAssertEqual(4, supportedAppcast.items.count) XCTAssertEqual(supportedAppcast.items[0].versionString, "4.1") XCTAssertEqual(supportedAppcast.items[1].versionString, "4.0") XCTAssertEqual(supportedAppcast.items[2].versionString, "3.9") XCTAssertEqual(supportedAppcast.items[3].versionString, "2.0") let bestAppcastItem = SUAppcastDriver.bestItem(fromAppcastItems: supportedAppcast.items, getDeltaItem: nil, withHostVersion: hostVersion, comparator: versionComparator) XCTAssertEqual(bestAppcastItem.versionString, "4.1") } // Allow minimum autoupdate version to fail and only skip major version "3.0" with subrelease version 3.4 // This should skip all 3.x versions except for 3.9 which ignores skipped upgrades below 3.5, but not 4.x versions nor 2.x versions do { let skippedUpdate = SPUSkippedUpdate(minorVersion: nil, majorVersion: "3.4", majorSubreleaseVersion: nil) let supportedAppcast = SUAppcastDriver.filterSupportedAppcast(appcast, phasedUpdateGroup: nil, skippedUpdate: skippedUpdate, currentDate: currentDate, hostVersion: hostVersion, versionComparator: versionComparator, testMinimumSystemRequirements: true, testMinimumAutoupdateVersion: false) XCTAssertEqual(4, supportedAppcast.items.count) XCTAssertEqual(supportedAppcast.items[0].versionString, "4.1") XCTAssertEqual(supportedAppcast.items[1].versionString, "4.0") XCTAssertEqual(supportedAppcast.items[2].versionString, "3.9") XCTAssertEqual(supportedAppcast.items[3].versionString, "2.0") let bestAppcastItem = SUAppcastDriver.bestItem(fromAppcastItems: supportedAppcast.items, getDeltaItem: nil, withHostVersion: hostVersion, comparator: versionComparator) XCTAssertEqual(bestAppcastItem.versionString, "4.1") } // Allow minimum autoupdate version to fail and only skip major version "3.0" with subrelease version 3.5 // This should skip all 3.x versions, but not 4.x versions nor 2.x versions do { let skippedUpdate = SPUSkippedUpdate(minorVersion: nil, majorVersion: "3.0", majorSubreleaseVersion: "3.5") let supportedAppcast = SUAppcastDriver.filterSupportedAppcast(appcast, phasedUpdateGroup: nil, skippedUpdate: skippedUpdate, currentDate: currentDate, hostVersion: hostVersion, versionComparator: versionComparator, testMinimumSystemRequirements: true, testMinimumAutoupdateVersion: false) XCTAssertEqual(3, supportedAppcast.items.count) XCTAssertEqual(supportedAppcast.items[0].versionString, "4.1") XCTAssertEqual(supportedAppcast.items[1].versionString, "4.0") XCTAssertEqual(supportedAppcast.items[2].versionString, "2.0") let bestAppcastItem = SUAppcastDriver.bestItem(fromAppcastItems: supportedAppcast.items, getDeltaItem: nil, withHostVersion: hostVersion, comparator: versionComparator) XCTAssertEqual(bestAppcastItem.versionString, "4.1") } // Allow minimum autoupdate version to fail and only skip major version "3.0" with subrelease version 3.5.1 // This should skip all 3.x versions, but not 4.x versions nor 2.x versions do { let skippedUpdate = SPUSkippedUpdate(minorVersion: nil, majorVersion: "3.0", majorSubreleaseVersion: "3.5.1") let supportedAppcast = SUAppcastDriver.filterSupportedAppcast(appcast, phasedUpdateGroup: nil, skippedUpdate: skippedUpdate, currentDate: currentDate, hostVersion: hostVersion, versionComparator: versionComparator, testMinimumSystemRequirements: true, testMinimumAutoupdateVersion: false) XCTAssertEqual(3, supportedAppcast.items.count) XCTAssertEqual(supportedAppcast.items[0].versionString, "4.1") XCTAssertEqual(supportedAppcast.items[1].versionString, "4.0") XCTAssertEqual(supportedAppcast.items[2].versionString, "2.0") let bestAppcastItem = SUAppcastDriver.bestItem(fromAppcastItems: supportedAppcast.items, getDeltaItem: nil, withHostVersion: hostVersion, comparator: versionComparator) XCTAssertEqual(bestAppcastItem.versionString, "4.1") } // Allow minimum autoupdate version to fail and only skip major version "4.0" with subrelease version 4.0 // This should skip all 3.x versions and 4.x versions, but not 2.x versions do { let skippedUpdate = SPUSkippedUpdate(minorVersion: nil, majorVersion: "4.0", majorSubreleaseVersion: "4.0") let supportedAppcast = SUAppcastDriver.filterSupportedAppcast(appcast, phasedUpdateGroup: nil, skippedUpdate: skippedUpdate, currentDate: currentDate, hostVersion: hostVersion, versionComparator: versionComparator, testMinimumSystemRequirements: true, testMinimumAutoupdateVersion: false) XCTAssertEqual(1, supportedAppcast.items.count) XCTAssertEqual(supportedAppcast.items[0].versionString, "2.0") let bestAppcastItem = SUAppcastDriver.bestItem(fromAppcastItems: supportedAppcast.items, getDeltaItem: nil, withHostVersion: hostVersion, comparator: versionComparator) XCTAssertEqual(bestAppcastItem.versionString, "2.0") } // Allow minimum autoupdate version to fail and only skip major version "4.0" with subrelease version 4.0, and skip minor version 2.1 // This should skip everything do { let skippedUpdate = SPUSkippedUpdate(minorVersion: "2.1", majorVersion: "4.0", majorSubreleaseVersion: "4.0") let supportedAppcast = SUAppcastDriver.filterSupportedAppcast(appcast, phasedUpdateGroup: nil, skippedUpdate: skippedUpdate, currentDate: currentDate, hostVersion: hostVersion, versionComparator: versionComparator, testMinimumSystemRequirements: true, testMinimumAutoupdateVersion: false) XCTAssertEqual(0, supportedAppcast.items.count) } } } catch let err as NSError { NSLog("%@", err) XCTFail(err.localizedDescription) } } func testARM64Requirement() throws { let testURL = Bundle(for: SUAppcastTest.self).url(forResource: "testappcast_arm64HardwareRequirement", withExtension: "xml")! do { let testData = try Data(contentsOf: testURL) let versionComparator = SUStandardVersionComparator() do { // Test appcast without a filter let hostVersion = "1.0" let stateResolver = SPUAppcastItemStateResolver(hostVersion: hostVersion, applicationVersionComparator: versionComparator, standardVersionComparator: versionComparator) let appcast = try SUAppcast(xmlData: testData, relativeTo: nil, stateResolver: stateResolver, signingValidationStatus: .skipped) XCTAssertEqual(6, appcast.items.count) } let currentDate = Date() do { let hostVersion = "1.0" let stateResolver = SPUAppcastItemStateResolver(hostVersion: hostVersion, applicationVersionComparator: versionComparator, standardVersionComparator: versionComparator) let appcast = try SUAppcast(xmlData: testData, relativeTo: nil, stateResolver: stateResolver, signingValidationStatus: .skipped) let supportedAppcast = SUAppcastDriver.filterSupportedAppcast(appcast, phasedUpdateGroup: nil, skippedUpdate: nil, currentDate: currentDate, hostVersion: hostVersion, versionComparator: versionComparator, testMinimumSystemRequirements: true, testMinimumAutoupdateVersion: false) #if arch(x86_64) // We're assuming this test is not run through Rosetta XCTAssertEqual(2, supportedAppcast.items.count) let bestAppcastItem = SUAppcastDriver.bestItem(fromAppcastItems: supportedAppcast.items, getDeltaItem: nil, withHostVersion: hostVersion, comparator: versionComparator) XCTAssertEqual(bestAppcastItem.versionString, "2.1") #else if #available(macOS 27, *) { XCTAssertEqual(6, supportedAppcast.items.count) let bestAppcastItem = SUAppcastDriver.bestItem(fromAppcastItems: supportedAppcast.items, getDeltaItem: nil, withHostVersion: hostVersion, comparator: versionComparator) XCTAssertEqual(bestAppcastItem.versionString, "6.0") } else { XCTAssertEqual(5, supportedAppcast.items.count) let bestAppcastItem = SUAppcastDriver.bestItem(fromAppcastItems: supportedAppcast.items, getDeltaItem: nil, withHostVersion: hostVersion, comparator: versionComparator) XCTAssertEqual(bestAppcastItem.versionString, "5.0") } #endif } } } func testPhasedGroupRollouts() { let testURL = Bundle(for: SUAppcastTest.self).url(forResource: "testappcast_phasedRollout", withExtension: "xml")! let dateFormatter = DateFormatter() dateFormatter.dateFormat = "E, dd MMM yyyy HH:mm:ss Z" dateFormatter.locale = Locale(identifier: "en_US") do { let testData = try Data(contentsOf: testURL) let versionComparator = SUStandardVersionComparator() // Because 3.0 has minimum autoupdate version of 2.0, we should be offered 2.0 do { let hostVersion = "1.0" let stateResolver = SPUAppcastItemStateResolver(hostVersion: hostVersion, applicationVersionComparator: versionComparator, standardVersionComparator: versionComparator) let appcast = try SUAppcast(xmlData: testData, relativeTo: nil, stateResolver: stateResolver, signingValidationStatus: .skipped) do { // Test no group let group: NSNumber? = nil let currentDate = Date() let supportedAppcast = SUAppcastDriver.filterSupportedAppcast(appcast, phasedUpdateGroup: group, skippedUpdate: nil, currentDate: currentDate, hostVersion: hostVersion, versionComparator: versionComparator, testMinimumSystemRequirements: true, testMinimumAutoupdateVersion: true) XCTAssertEqual(1, supportedAppcast.items.count) XCTAssertEqual("2.0", supportedAppcast.items[0].versionString) } do { // Test 0 group with current date (way ahead of pubDate) let group: NSNumber? = nil let currentDate = Date() let supportedAppcast = SUAppcastDriver.filterSupportedAppcast(appcast, phasedUpdateGroup: group, skippedUpdate: nil, currentDate: currentDate, hostVersion: hostVersion, versionComparator: versionComparator, testMinimumSystemRequirements: true, testMinimumAutoupdateVersion: true) XCTAssertEqual(1, supportedAppcast.items.count) } do { // Test 6th group with current date (way ahead of pubDate) let group = 6 as NSNumber let currentDate = Date() let supportedAppcast = SUAppcastDriver.filterSupportedAppcast(appcast, phasedUpdateGroup: group, skippedUpdate: nil, currentDate: currentDate, hostVersion: hostVersion, versionComparator: versionComparator, testMinimumSystemRequirements: true, testMinimumAutoupdateVersion: true) XCTAssertEqual(1, supportedAppcast.items.count) } do { let currentDate = dateFormatter.date(from: "Wed, 23 Jul 2014 15:20:11 +0000")! do { // Test group 0 with current date 3 days before rollout // No update should be found let group = 0 as NSNumber let supportedAppcast = SUAppcastDriver.filterSupportedAppcast(appcast, phasedUpdateGroup: group, skippedUpdate: nil, currentDate: currentDate, hostVersion: hostVersion, versionComparator: versionComparator, testMinimumSystemRequirements: true, testMinimumAutoupdateVersion: true) XCTAssertEqual(0, supportedAppcast.items.count) } do { // Test group 6 with current date 3 days before rollout // No update should be found still let group = 6 as NSNumber let supportedAppcast = SUAppcastDriver.filterSupportedAppcast(appcast, phasedUpdateGroup: group, skippedUpdate: nil, currentDate: currentDate, hostVersion: hostVersion, versionComparator: versionComparator, testMinimumSystemRequirements: true, testMinimumAutoupdateVersion: true) XCTAssertEqual(0, supportedAppcast.items.count) } } do { let currentDate = dateFormatter.date(from: "Mon, 28 Jul 2014 15:20:11 +0000")! do { // Test group 0 with current date 2 days after rollout let group = 0 as NSNumber let supportedAppcast = SUAppcastDriver.filterSupportedAppcast(appcast, phasedUpdateGroup: group, skippedUpdate: nil, currentDate: currentDate, hostVersion: hostVersion, versionComparator: versionComparator, testMinimumSystemRequirements: true, testMinimumAutoupdateVersion: true) XCTAssertEqual(1, supportedAppcast.items.count) } do { // Test group 1 with current date 3 days after rollout let group = 1 as NSNumber let supportedAppcast = SUAppcastDriver.filterSupportedAppcast(appcast, phasedUpdateGroup: group, skippedUpdate: nil, currentDate: currentDate, hostVersion: hostVersion, versionComparator: versionComparator, testMinimumSystemRequirements: true, testMinimumAutoupdateVersion: true) XCTAssertEqual(1, supportedAppcast.items.count) } do { // Test group 2 with current date 3 days after rollout let group = 2 as NSNumber let supportedAppcast = SUAppcastDriver.filterSupportedAppcast(appcast, phasedUpdateGroup: group, skippedUpdate: nil, currentDate: currentDate, hostVersion: hostVersion, versionComparator: versionComparator, testMinimumSystemRequirements: true, testMinimumAutoupdateVersion: true) XCTAssertEqual(1, supportedAppcast.items.count) } do { // Test group 3 with current date 3 days after rollout let group = 3 as NSNumber let supportedAppcast = SUAppcastDriver.filterSupportedAppcast(appcast, phasedUpdateGroup: group, skippedUpdate: nil, currentDate: currentDate, hostVersion: hostVersion, versionComparator: versionComparator, testMinimumSystemRequirements: true, testMinimumAutoupdateVersion: true) XCTAssertEqual(0, supportedAppcast.items.count) } do { // Test group 6 with current date 3 days after rollout let group = 6 as NSNumber let supportedAppcast = SUAppcastDriver.filterSupportedAppcast(appcast, phasedUpdateGroup: group, skippedUpdate: nil, currentDate: currentDate, hostVersion: hostVersion, versionComparator: versionComparator, testMinimumSystemRequirements: true, testMinimumAutoupdateVersion: true) XCTAssertEqual(0, supportedAppcast.items.count) } } // Test critical updates which ignore phased rollouts do { let hostVersion = "2.0" let stateResolver = SPUAppcastItemStateResolver(hostVersion: hostVersion, applicationVersionComparator: versionComparator, standardVersionComparator: versionComparator) let appcast = try SUAppcast(xmlData: testData, relativeTo: nil, stateResolver: stateResolver, signingValidationStatus: .skipped) do { // Test no group let group: NSNumber? = nil let currentDate = Date() let supportedAppcast = SUAppcastDriver.filterSupportedAppcast(appcast, phasedUpdateGroup: group, skippedUpdate: nil, currentDate: currentDate, hostVersion: hostVersion, versionComparator: versionComparator, testMinimumSystemRequirements: true, testMinimumAutoupdateVersion: true) XCTAssertEqual(2, supportedAppcast.items.count) XCTAssertEqual("3.0", supportedAppcast.items[0].versionString) } do { let currentDate = dateFormatter.date(from: "Wed, 23 Jul 2014 15:20:11 +0000")! do { // Test group 0 with current date 3 days before rollout let group = 0 as NSNumber let supportedAppcast = SUAppcastDriver.filterSupportedAppcast(appcast, phasedUpdateGroup: group, skippedUpdate: nil, currentDate: currentDate, hostVersion: hostVersion, versionComparator: versionComparator, testMinimumSystemRequirements: true, testMinimumAutoupdateVersion: true) XCTAssertEqual(1, supportedAppcast.items.count) XCTAssertEqual("3.0", supportedAppcast.items[0].versionString) } } do { let currentDate = dateFormatter.date(from: "Mon, 28 Jul 2014 15:20:11 +0000")! do { // Test group 6 with current date 3 days after rollout let group = 6 as NSNumber let supportedAppcast = SUAppcastDriver.filterSupportedAppcast(appcast, phasedUpdateGroup: group, skippedUpdate: nil, currentDate: currentDate, hostVersion: hostVersion, versionComparator: versionComparator, testMinimumSystemRequirements: true, testMinimumAutoupdateVersion: true) XCTAssertEqual(1, supportedAppcast.items.count) XCTAssertEqual("3.0", supportedAppcast.items[0].versionString) } } } } } catch let err as NSError { NSLog("%@", err) XCTFail(err.localizedDescription) } } func testParseAppcastWithLocalizedReleaseNotes() { let testFile = Bundle(for: SUAppcastTest.self).path(forResource: "testlocalizedreleasenotesappcast", ofType: "xml")! let testFileUrl = URL(fileURLWithPath: testFile) XCTAssertNotNil(testFileUrl) let preferredLanguage = Bundle.preferredLocalizations(from: ["en", "es"])[0] NSLog("Using preferred locale %@", preferredLanguage) let expectedReleaseNotesLink = (preferredLanguage == "es") ? "https://sparkle-project.org/notes.es.html" : "https://sparkle-project.org/notes.en.html" do { let testFileData = try Data(contentsOf: testFileUrl) let stateResolver = SPUAppcastItemStateResolver(hostVersion: "1.0", applicationVersionComparator: SUStandardVersionComparator.default, standardVersionComparator: SUStandardVersionComparator.default) let fullAppcast = try SUAppcast(xmlData: testFileData, relativeTo: testFileUrl, stateResolver: stateResolver, signingValidationStatus: .skipped) do { let appcast = SUAppcastDriver.filterAppcast(fullAppcast, forMacOSAndAllowedChannels: ["english-later"]) let items = appcast.items XCTAssertEqual(items.count, 1) XCTAssertEqual(items[0].versionString, "6.0") XCTAssertEqual(expectedReleaseNotesLink, items[0].releaseNotesURL!.absoluteString) } do { let appcast = SUAppcastDriver.filterAppcast(fullAppcast, forMacOSAndAllowedChannels: ["english-first"]) let items = appcast.items XCTAssertEqual(items.count, 1) XCTAssertEqual(items[0].versionString, "6.1") XCTAssertEqual(expectedReleaseNotesLink, items[0].releaseNotesURL!.absoluteString) } do { let appcast = SUAppcastDriver.filterAppcast(fullAppcast, forMacOSAndAllowedChannels: ["english-first-implicit"]) let items = appcast.items XCTAssertEqual(items.count, 1) XCTAssertEqual(items[0].versionString, "6.2") XCTAssertEqual(expectedReleaseNotesLink, items[0].releaseNotesURL!.absoluteString) } do { let appcast = SUAppcastDriver.filterAppcast(fullAppcast, forMacOSAndAllowedChannels: ["english-later-implicit"]) let items = appcast.items XCTAssertEqual(items.count, 1) XCTAssertEqual(items[0].versionString, "6.3") XCTAssertEqual(expectedReleaseNotesLink, items[0].releaseNotesURL!.absoluteString) } } catch let err as NSError { NSLog("%@", err) XCTFail(err.localizedDescription) } } func testNamespaces() { let testFile = Bundle(for: SUAppcastTest.self).path(forResource: "testnamespaces", ofType: "xml")! let testData = NSData(contentsOfFile: testFile)! do { let stateResolver = SPUAppcastItemStateResolver(hostVersion: "1.0", applicationVersionComparator: SUStandardVersionComparator.default, standardVersionComparator: SUStandardVersionComparator.default) let appcast = try SUAppcast(xmlData: testData as Data, relativeTo: nil, stateResolver: stateResolver, signingValidationStatus: .skipped) let items = appcast.items XCTAssertEqual(2, items.count) XCTAssertEqual("Version 2.0", items[1].title) XCTAssertEqual("desc", items[1].itemDescription) XCTAssertNotNil(items[0].releaseNotesURL) XCTAssertEqual("https://sparkle-project.org/#works", items[0].releaseNotesURL!.absoluteString) } catch let err as NSError { NSLog("%@", err) XCTFail(err.localizedDescription) } } func testLinks() { let testFile = Bundle(for: SUAppcastTest.self).path(forResource: "test-links", ofType: "xml")! let testData = NSData(contentsOfFile: testFile)! do { let baseURL: URL? = nil let stateResolver = SPUAppcastItemStateResolver(hostVersion: "1.0", applicationVersionComparator: SUStandardVersionComparator.default, standardVersionComparator: SUStandardVersionComparator.default) let appcast = try SUAppcast(xmlData: testData as Data, relativeTo: baseURL, stateResolver: stateResolver, signingValidationStatus: .skipped) let items = appcast.items XCTAssertEqual(3, items.count) // Test https XCTAssertEqual("https://sparkle-project.org/notes/relnote-3.0.txt", items[0].releaseNotesURL?.absoluteString) XCTAssertEqual("https://sparkle-project.org/fullnotes.txt", items[0].fullReleaseNotesURL?.absoluteString) XCTAssertEqual("https://sparkle-project.org", items[0].infoURL?.absoluteString) XCTAssertEqual("https://sparkle-project.org/release-3.0.zip", items[0].fileURL?.absoluteString) // Test http XCTAssertEqual("http://sparkle-project.org/notes/relnote-2.0.txt", items[1].releaseNotesURL?.absoluteString) XCTAssertEqual("http://sparkle-project.org/fullnotes.txt", items[1].fullReleaseNotesURL?.absoluteString) XCTAssertEqual("http://sparkle-project.org", items[1].infoURL?.absoluteString) XCTAssertEqual("http://sparkle-project.org/release-2.0.zip", items[1].fileURL?.absoluteString) // Test bad file URLs XCTAssertEqual(nil, items[2].releaseNotesURL?.absoluteString) XCTAssertEqual(nil, items[2].fullReleaseNotesURL?.absoluteString) XCTAssertEqual(nil, items[2].infoURL?.absoluteString) XCTAssertEqual("https://sparkle-project.org/release-1.0.zip", items[2].fileURL?.absoluteString) } catch let err as NSError { NSLog("%@", err) XCTFail(err.localizedDescription) } } func testRelativeURLs() { let testFile = Bundle(for: SUAppcastTest.self).path(forResource: "test-relative-urls", ofType: "xml")! let testData = NSData(contentsOfFile: testFile)! do { let baseURL = URL(string: "https://fake.sparkle-project.org/updates/index.xml")! let stateResolver = SPUAppcastItemStateResolver(hostVersion: "1.0", applicationVersionComparator: SUStandardVersionComparator.default, standardVersionComparator: SUStandardVersionComparator.default) let appcast = try SUAppcast(xmlData: testData as Data, relativeTo: baseURL, stateResolver: stateResolver, signingValidationStatus: .skipped) let items = appcast.items XCTAssertEqual(4, items.count) XCTAssertEqual("https://fake.sparkle-project.org/updates/release-3.0.zip", items[0].fileURL?.absoluteString) XCTAssertEqual("https://fake.sparkle-project.org/updates/notes/relnote-3.0.txt", items[0].releaseNotesURL?.absoluteString) XCTAssertEqual(2, items[0].deltaUpdates!.count) XCTAssertEqual("https://fake.sparkle-project.org/updates/3.0_from_2.0.delta", items[0].deltaUpdates!["2.0"]!.fileURL?.absoluteString) XCTAssertEqual("https://fake.sparkle-project.org/updates/3.0_from_1.0.delta", items[0].deltaUpdates!["1.0"]!.fileURL?.absoluteString) XCTAssertEqual("https://fake.sparkle-project.org/info/info-2.0.txt", items[1].infoURL?.absoluteString) XCTAssertEqual("https://fake.sparkle-project.org/updates/notes/fullnotes.txt", items[2].fullReleaseNotesURL?.absoluteString) // If a different base URL is in the feed, we should respect the base URL in the feed XCTAssertEqual("https://sparkle-project.org/releasenotes.html", items[3].fullReleaseNotesURL?.absoluteString) } catch let err as NSError { NSLog("%@", err) XCTFail(err.localizedDescription) } } func testDangerousLink() { let testFile = Bundle(for: SUAppcastTest.self).path(forResource: "test-dangerous-link", ofType: "xml")! let testData = NSData(contentsOfFile: testFile)! do { let baseURL: URL? = nil let stateResolver = SPUAppcastItemStateResolver(hostVersion: "1.0", applicationVersionComparator: SUStandardVersionComparator.default, standardVersionComparator: SUStandardVersionComparator.default) let _ = try SUAppcast(xmlData: testData as Data, relativeTo: baseURL, stateResolver: stateResolver, signingValidationStatus: .skipped) XCTFail("Appcast creation should fail when encountering dangerous link") } catch let err as NSError { NSLog("Expected error: %@", err) XCTAssertNotNil(err) } } func testDangerousLinkWithSucceededSigningValidation() { let testFile = Bundle(for: SUAppcastTest.self).path(forResource: "test-dangerous-link", ofType: "xml")! let testData = NSData(contentsOfFile: testFile)! do { let baseURL: URL? = nil let stateResolver = SPUAppcastItemStateResolver(hostVersion: "1.0", applicationVersionComparator: SUStandardVersionComparator.default, standardVersionComparator: SUStandardVersionComparator.default) let _ = try SUAppcast(xmlData: testData as Data, relativeTo: baseURL, stateResolver: stateResolver, signingValidationStatus: .skipped) XCTFail("Appcast creation should fail when encountering dangerous link") } catch let err as NSError { NSLog("Expected error: %@", err) XCTAssertNotNil(err) } } func testDangerousLinkWithFailedSigningValidation() { let testFile = Bundle(for: SUAppcastTest.self).path(forResource: "test-dangerous-link", ofType: "xml")! let testData = NSData(contentsOfFile: testFile)! do { let baseURL: URL? = nil let stateResolver = SPUAppcastItemStateResolver(hostVersion: "1.0", applicationVersionComparator: SUStandardVersionComparator.default, standardVersionComparator: SUStandardVersionComparator.default) let appcast = try SUAppcast(xmlData: testData as Data, relativeTo: baseURL, stateResolver: stateResolver, signingValidationStatus: .failed) // Links and info only and critical updates should be stripped/removed appropriately XCTAssertEqual(appcast.items.count, 2) XCTAssertEqual(appcast.signingValidationStatus, .failed) for (appcastItemIndex, appcastItem) in appcast.items.enumerated() { XCTAssertNil(appcastItem.infoURL) XCTAssertNil(appcastItem.releaseNotesURL) XCTAssertNil(appcastItem.fullReleaseNotesURL) XCTAssertFalse(appcastItem.isCriticalUpdate) if appcastItemIndex == 0 { XCTAssertEqual(appcastItem.versionString, "3.0 (Visit abc.") } else if appcastItemIndex == 1 { XCTAssertEqual(appcastItem.displayVersionString, "2.0 (Visit abc.") } } } catch let err as NSError { NSLog("Expected error: %@", err) XCTFail("Appcast creation should have passed when encountering dangerous link of one item") } } } ================================================ FILE: Tests/SUBinaryDeltaTest.m ================================================ // // SUBinaryDeltaTest.m // Sparkle // // Created by Jake Petroules on 2014-08-22. // Copyright (c) 2014 Sparkle Project. All rights reserved. // #import #import #import "SUBinaryDeltaCommon.h" #import "SUBinaryDeltaCreate.h" #import "SUBinaryDeltaApply.h" #import #include @interface SUBinaryDeltaTest : XCTestCase @end typedef void (^SUDeltaHandler)(NSFileManager *fileManager, NSString *sourceDirectory, NSString *destinationDirectory); @implementation SUBinaryDeltaTest - (void)testTemporaryDirectory { NSString *tmp1 = temporaryDirectory(@"Sparklęエンジン"); NSString *tmp2 = temporaryDirectory(@"Sparklęエンジン"); NSLog(@"Temporary directories: %@, %@", tmp1, tmp2); #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wobjc-messaging-id" XCTAssertNotEqualObjects(tmp1, tmp2); #pragma clang diagnostic pop XCTAssert(YES, @"Pass"); } - (void)testTemporaryFile { NSString *tmp1 = temporaryFilename(@"Sparklęエンジン"); NSString *tmp2 = temporaryFilename(@"Sparklęエンジン"); NSLog(@"Temporary files: %@, %@", tmp1, tmp2); #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wobjc-messaging-id" XCTAssertNotEqualObjects(tmp1, tmp2); #pragma clang diagnostic pop XCTAssert(YES, @"Pass"); } - (BOOL)createAndApplyPatchUsingVersion:(SUBinaryDeltaMajorVersion)majorVersion compressionMode:(SPUDeltaCompressionMode)compressionMode beforeDiffHandler:(SUDeltaHandler)beforeDiffHandler afterDiffHandler:(SUDeltaHandler)afterDiffHandler afterPatchHandler:(SUDeltaHandler)afterPatchHandler { NSString *sourceDirectory = temporaryDirectory(@"Spąrkle_temp1エンジン"); NSString *destinationDirectory = temporaryDirectory(@"Spąrkle_temp2エンジン"); NSString *diffFile = temporaryFilename(@"Spąrkle_diffエンジン"); NSString *patchDirectory = temporaryDirectory(@"Spąrkle_patchエンジン"); XCTAssertNotNil(sourceDirectory); XCTAssertNotNil(destinationDirectory); XCTAssertNotNil(diffFile); NSFileManager *fileManager = [[NSFileManager alloc] init]; if (beforeDiffHandler != nil) { beforeDiffHandler(fileManager, sourceDirectory, destinationDirectory); } NSError *createDiffError = nil; BOOL createdDiff = createBinaryDelta(sourceDirectory, destinationDirectory, diffFile, majorVersion, compressionMode, 0, NO, &createDiffError); if (!createdDiff) { NSLog(@"Creating binary diff failed with error: %@", createDiffError); } else if (afterDiffHandler != nil) { afterDiffHandler(fileManager, sourceDirectory, destinationDirectory); } NSError *applyDiffError = nil; BOOL appliedDiff = NO; if (createdDiff) { if (applyBinaryDelta(sourceDirectory, patchDirectory, diffFile, NO, ^(__unused double progress){}, &applyDiffError)) { appliedDiff = YES; if (afterPatchHandler != nil) { afterPatchHandler(fileManager, destinationDirectory, patchDirectory); } } else { NSLog(@"Applying binary diff failed with error: %@", applyDiffError); } } XCTAssertTrue([fileManager removeItemAtPath:sourceDirectory error:nil]); XCTAssertTrue([fileManager removeItemAtPath:destinationDirectory error:nil]); XCTAssertTrue([fileManager removeItemAtPath:patchDirectory error:nil]); XCTAssertTrue([fileManager removeItemAtPath:diffFile error:nil]); return appliedDiff; } - (BOOL)createAndApplyPatchWithBeforeDiffHandler:(SUDeltaHandler)beforeDiffHandler afterDiffHandler:(SUDeltaHandler)afterDiffHandler afterPatchHandler:(SUDeltaHandler)afterPatchHandler { #if SPARKLE_BUILD_LEGACY_DELTA_SUPPORT BOOL testingVersion2Delta = YES; #else BOOL testingVersion2Delta = NO; #endif return [self createAndApplyPatchWithBeforeDiffHandler:beforeDiffHandler afterDiffHandler:afterDiffHandler afterPatchHandler:afterPatchHandler testingVersion3Delta:YES testingVersion2Delta:testingVersion2Delta]; } - (BOOL)createAndApplyPatchWithBeforeDiffHandler:(SUDeltaHandler)beforeDiffHandler afterDiffHandler:(SUDeltaHandler)afterDiffHandler afterPatchHandler:(SUDeltaHandler)afterPatchHandler testingVersion3Delta:(BOOL)testingVersion3Delta testingVersion2Delta:(BOOL)testingVersion2Delta { XCTAssertEqual(SUBinaryDeltaMajorVersion4, SUBinaryDeltaMajorVersionLatest); BOOL version4DeltaFormatWithLZMASuccess = [self createAndApplyPatchUsingVersion:SUBinaryDeltaMajorVersion4 compressionMode:SPUDeltaCompressionModeLZMA beforeDiffHandler:beforeDiffHandler afterDiffHandler:afterDiffHandler afterPatchHandler:afterPatchHandler]; #if SPARKLE_BUILD_BZIP2_DELTA_SUPPORT BOOL version4DeltaFormatWithBZIP2Success = [self createAndApplyPatchUsingVersion:SUBinaryDeltaMajorVersion4 compressionMode:SPUDeltaCompressionModeBzip2 beforeDiffHandler:beforeDiffHandler afterDiffHandler:afterDiffHandler afterPatchHandler:afterPatchHandler]; #endif BOOL version4DeltaFormatWithZLIBSuccess = [self createAndApplyPatchUsingVersion:SUBinaryDeltaMajorVersion4 compressionMode:SPUDeltaCompressionModeZLIB beforeDiffHandler:beforeDiffHandler afterDiffHandler:afterDiffHandler afterPatchHandler:afterPatchHandler]; BOOL version3DeltaFormatWithLZMASuccess = !testingVersion3Delta || [self createAndApplyPatchUsingVersion:SUBinaryDeltaMajorVersion3 compressionMode:SPUDeltaCompressionModeLZMA beforeDiffHandler:beforeDiffHandler afterDiffHandler:afterDiffHandler afterPatchHandler:afterPatchHandler]; BOOL version2FormatSuccess = !testingVersion2Delta || [self createAndApplyPatchUsingVersion:SUBinaryDeltaMajorVersion2 compressionMode:SPUDeltaCompressionModeDefault beforeDiffHandler:beforeDiffHandler afterDiffHandler:afterDiffHandler afterPatchHandler:afterPatchHandler]; return ( version4DeltaFormatWithLZMASuccess && #if SPARKLE_BUILD_BZIP2_DELTA_SUPPORT version4DeltaFormatWithBZIP2Success && #endif version4DeltaFormatWithZLIBSuccess && version3DeltaFormatWithLZMASuccess && version2FormatSuccess ); } - (void)createAndApplyPatchWithHandler:(SUDeltaHandler)handler { BOOL success = [self createAndApplyPatchWithBeforeDiffHandler:handler afterDiffHandler:nil afterPatchHandler:nil]; XCTAssertTrue(success); } - (BOOL)testDirectoryHashEqualityWithSource:(NSString *)source destination:(NSString *)destination { XCTAssertNotNil(source); XCTAssertNotNil(destination); NSString *beforeHash = hashOfTree(source); NSString *afterHash = hashOfTree(destination); XCTAssertNotNil(beforeHash); XCTAssertNotNil(afterHash); return [beforeHash isEqualToString:afterHash]; } - (void)testNoFilesDiff { [self createAndApplyPatchWithHandler:^(NSFileManager *__unused fileManager, NSString *sourceDirectory, NSString *destinationDirectory) { XCTAssertTrue([self testDirectoryHashEqualityWithSource:sourceDirectory destination:destinationDirectory]); }]; } - (void)testEmptyDataDiff { [self createAndApplyPatchWithHandler:^(NSFileManager *__unused fileManager, NSString *sourceDirectory, NSString *destinationDirectory) { NSData *emptyData = [NSData data]; NSString *sourceFile = [sourceDirectory stringByAppendingPathComponent:@"AĄエンジン"]; NSString *destinationFile = [destinationDirectory stringByAppendingPathComponent:@"AĄエンジン"]; XCTAssertTrue([emptyData writeToFile:sourceFile atomically:YES]); XCTAssertTrue([emptyData writeToFile:destinationFile atomically:YES]); XCTAssertTrue([self testDirectoryHashEqualityWithSource:sourceDirectory destination:destinationDirectory]); }]; } - (void)testDifferentlyNamedEmptyDataDiff { [self createAndApplyPatchWithHandler:^(NSFileManager *__unused fileManager, NSString *sourceDirectory, NSString *destinationDirectory) { NSData *emptyData = [NSData data]; NSString *sourceFile = [sourceDirectory stringByAppendingPathComponent:@"A"]; NSString *destinationFile = [destinationDirectory stringByAppendingPathComponent:@"B"]; XCTAssertTrue([emptyData writeToFile:sourceFile atomically:YES]); XCTAssertTrue([emptyData writeToFile:destinationFile atomically:YES]); XCTAssertFalse([self testDirectoryHashEqualityWithSource:sourceDirectory destination:destinationDirectory]); }]; } - (void)testEmptyDirectoryDiff { [self createAndApplyPatchWithHandler:^(NSFileManager *fileManager, NSString *sourceDirectory, NSString *destinationDirectory) { NSString *sourceFile = [sourceDirectory stringByAppendingPathComponent:@"A"]; NSString *destinationFile = [destinationDirectory stringByAppendingPathComponent:@"A"]; NSError *error = nil; if (![fileManager createDirectoryAtPath:sourceFile withIntermediateDirectories:NO attributes:nil error:&error]) { NSLog(@"Failed creating directory with error: %@", error); XCTFail("Failed to create directory"); } if (![fileManager createDirectoryAtPath:destinationFile withIntermediateDirectories:NO attributes:nil error:&error]) { NSLog(@"Failed creating directory with error: %@", error); XCTFail("Failed to create directory"); } XCTAssertTrue([self testDirectoryHashEqualityWithSource:sourceDirectory destination:destinationDirectory]); }]; } - (void)testDifferentlyNamedEmptyDirectoryDiff { [self createAndApplyPatchWithHandler:^(NSFileManager *fileManager, NSString *sourceDirectory, NSString *destinationDirectory) { NSString *sourceFile = [sourceDirectory stringByAppendingPathComponent:@"A"]; NSString *destinationFile = [destinationDirectory stringByAppendingPathComponent:@"B"]; NSError *error = nil; if (![fileManager createDirectoryAtPath:sourceFile withIntermediateDirectories:NO attributes:nil error:&error]) { NSLog(@"Failed creating directory with error: %@", error); XCTFail("Failed to create directory"); } if (![fileManager createDirectoryAtPath:destinationFile withIntermediateDirectories:NO attributes:nil error:&error]) { NSLog(@"Failed creating directory with error: %@", error); XCTFail("Failed to create directory"); } XCTAssertFalse([self testDirectoryHashEqualityWithSource:sourceDirectory destination:destinationDirectory]); }]; } - (void)testSameNonexistentSymlinkDiff { [self createAndApplyPatchWithHandler:^(NSFileManager *fileManager, NSString *sourceDirectory, NSString *destinationDirectory) { NSString *sourceFile = [sourceDirectory stringByAppendingPathComponent:@"A"]; NSString *destinationFile = [destinationDirectory stringByAppendingPathComponent:@"A"]; NSError *error = nil; if (![fileManager createSymbolicLinkAtPath:sourceFile withDestinationPath:@"B" error:&error]) { NSLog(@"Failed creating empty symlink with error: %@", error); XCTFail("Failed to create empty symlink"); } if (![fileManager createSymbolicLinkAtPath:destinationFile withDestinationPath:@"B" error:&error]) { NSLog(@"Failed creating empty symlink with error: %@", error); XCTFail("Failed to create empty symlink"); } XCTAssertTrue([self testDirectoryHashEqualityWithSource:sourceDirectory destination:destinationDirectory]); }]; } - (void)testDifferentNonexistentSymlinkDiff { [self createAndApplyPatchWithHandler:^(NSFileManager *fileManager, NSString *sourceDirectory, NSString *destinationDirectory) { NSString *sourceFile = [sourceDirectory stringByAppendingPathComponent:@"A"]; NSString *destinationFile = [destinationDirectory stringByAppendingPathComponent:@"A"]; NSError *error = nil; if (![fileManager createSymbolicLinkAtPath:sourceFile withDestinationPath:@"B" error:&error]) { NSLog(@"Failed creating empty symlink with error: %@", error); XCTFail("Failed to create empty symlink"); } if (![fileManager createSymbolicLinkAtPath:destinationFile withDestinationPath:@"C" error:&error]) { NSLog(@"Failed creating empty symlink with error: %@", error); XCTFail("Failed to create empty symlink"); } XCTAssertFalse([self testDirectoryHashEqualityWithSource:sourceDirectory destination:destinationDirectory]); }]; } - (void)testNonexistentSymlinkPermissionDiff { [self createAndApplyPatchWithBeforeDiffHandler:^(NSFileManager *fileManager, NSString *sourceDirectory, NSString *destinationDirectory) { NSString *sourceFile = [sourceDirectory stringByAppendingPathComponent:@"A"]; NSString *destinationFile = [destinationDirectory stringByAppendingPathComponent:@"A"]; NSError *error = nil; if (![fileManager createSymbolicLinkAtPath:sourceFile withDestinationPath:@"B" error:&error]) { NSLog(@"Failed creating empty symlink to source with error: %@", error); XCTFail("Failed to create empty symlink"); } if (![fileManager createSymbolicLinkAtPath:destinationFile withDestinationPath:@"B" error:&error]) { NSLog(@"Failed creating empty symlink to destination with error: %@", error); XCTFail("Failed to create empty symlink"); } if (lchmod([sourceFile fileSystemRepresentation], 0777) != 0) { NSLog(@"Change Permission Error.."); XCTFail(@"Failed setting file permissions"); } // 0755 and 0777 should result in the same hash XCTAssertTrue([self testDirectoryHashEqualityWithSource:sourceDirectory destination:destinationDirectory]); } afterDiffHandler:nil afterPatchHandler:^(NSFileManager *fileManager, NSString * __unused sourceDirectory, NSString *destinationDirectory) { NSString *destinationFile = [destinationDirectory stringByAppendingPathComponent:@"A"]; NSError *error = nil; NSDictionary *attributes = [fileManager attributesOfItemAtPath:destinationFile error:&error]; if (attributes == nil) { NSLog(@"Failed to retrieve attributes with error: %@", error); XCTFail("Failed to retrieve symlink attributes"); } NSNumber *permissionAttribute = attributes[NSFilePosixPermissions]; XCTAssertNotNil(permissionAttribute); unsigned short permissions = permissionAttribute.unsignedShortValue & PERMISSION_FLAGS; XCTAssertEqual(permissions, VALID_SYMBOLIC_LINK_PERMISSIONS); }]; } - (void)testNonexistentSymlinkPermissionBadDiff { // Even though destination has a 0777 symlink permission, we only respect 0755 for symlinks [self createAndApplyPatchWithBeforeDiffHandler:^(NSFileManager *fileManager, NSString *sourceDirectory, NSString *destinationDirectory) { NSString *sourceFile = [sourceDirectory stringByAppendingPathComponent:@"A"]; NSString *destinationFile = [destinationDirectory stringByAppendingPathComponent:@"A"]; NSError *error = nil; if (![fileManager createSymbolicLinkAtPath:sourceFile withDestinationPath:@"B" error:&error]) { NSLog(@"Failed creating empty symlink to source with error: %@", error); XCTFail("Failed to create empty symlink"); } if (![fileManager createSymbolicLinkAtPath:destinationFile withDestinationPath:@"B" error:&error]) { NSLog(@"Failed creating empty symlink to destination with error: %@", error); XCTFail("Failed to create empty symlink"); } if (lchmod([destinationFile fileSystemRepresentation], 0777) != 0) { NSLog(@"Change Permission Error.."); XCTFail(@"Failed setting file permissions"); } XCTAssertTrue([self testDirectoryHashEqualityWithSource:sourceDirectory destination:destinationDirectory]); } afterDiffHandler:nil afterPatchHandler:^(NSFileManager *fileManager, NSString * __unused sourceDirectory, NSString *destinationDirectory) { NSString *destinationFile = [destinationDirectory stringByAppendingPathComponent:@"A"]; NSError *error = nil; NSDictionary *attributes = [fileManager attributesOfItemAtPath:destinationFile error:&error]; if (attributes == nil) { NSLog(@"Failed to retrieve attributes with error: %@", error); XCTFail("Failed to retrieve symlink attributes"); } NSNumber *permissionAttribute = attributes[NSFilePosixPermissions]; XCTAssertNotNil(permissionAttribute); unsigned short permissions = permissionAttribute.unsignedShortValue & PERMISSION_FLAGS; XCTAssertEqual(permissions, VALID_SYMBOLIC_LINK_PERMISSIONS); }]; } - (void)testSmallDataDiff { [self createAndApplyPatchWithHandler:^(NSFileManager *__unused fileManager, NSString *sourceDirectory, NSString *destinationDirectory) { NSString *sourceFile = [sourceDirectory stringByAppendingPathComponent:@"A"]; NSString *destinationFile = [destinationDirectory stringByAppendingPathComponent:@"A"]; XCTAssertTrue([[NSData data] writeToFile:sourceFile atomically:YES]); XCTAssertTrue([[NSData dataWithBytes:"loltest" length:7] writeToFile:destinationFile atomically:YES]); XCTAssertFalse([self testDirectoryHashEqualityWithSource:sourceDirectory destination:destinationDirectory]); }]; } - (void)testInvalidSource { BOOL success = [self createAndApplyPatchWithBeforeDiffHandler:^(NSFileManager *__unused fileManager, NSString *sourceDirectory, NSString *destinationDirectory) { NSString *sourceFile = [sourceDirectory stringByAppendingPathComponent:@"A"]; NSString *destinationFile = [destinationDirectory stringByAppendingPathComponent:@"A"]; XCTAssertTrue([[NSData data] writeToFile:sourceFile atomically:YES]); XCTAssertTrue([[NSData dataWithBytes:"loltest" length:7] writeToFile:destinationFile atomically:YES]); XCTAssertFalse([self testDirectoryHashEqualityWithSource:sourceDirectory destination:destinationDirectory]); } afterDiffHandler:^(NSFileManager *__unused fileManager, NSString *sourceDirectory, NSString *__unused destinationDirectory) { NSString *sourceFile = [sourceDirectory stringByAppendingPathComponent:@"A"]; XCTAssertTrue([[NSData dataWithBytes:"testt" length:5] writeToFile:sourceFile atomically:YES]); } afterPatchHandler:nil]; XCTAssertFalse(success); } - (NSData *)bigData1 { const size_t bufferSize = 4096*32; uint8_t *buffer = (uint8_t *)calloc(1, bufferSize); XCTAssertTrue(buffer != NULL); return [NSData dataWithBytesNoCopy:buffer length:bufferSize]; } - (NSData *)bigData2 { const size_t bufferSize = 4096*32; uint8_t *buffer = (uint8_t *)calloc(1, bufferSize); XCTAssertTrue(buffer != NULL); for (size_t bufferIndex = 0; bufferIndex < bufferSize; ++bufferIndex) { buffer[bufferIndex] = 1; } return [NSData dataWithBytesNoCopy:buffer length:bufferSize]; } - (void)testBigDataSameDiff { [self createAndApplyPatchWithHandler:^(NSFileManager *__unused fileManager, NSString *sourceDirectory, NSString *destinationDirectory) { NSString *sourceFile = [sourceDirectory stringByAppendingPathComponent:@"A"]; NSString *destinationFile = [destinationDirectory stringByAppendingPathComponent:@"A"]; XCTAssertTrue([[self bigData1] writeToFile:sourceFile atomically:YES]); XCTAssertTrue([[self bigData1] writeToFile:destinationFile atomically:YES]); XCTAssertTrue([self testDirectoryHashEqualityWithSource:sourceDirectory destination:destinationDirectory]); }]; } - (void)testBigDataDifferentDiff { [self createAndApplyPatchWithHandler:^(NSFileManager *__unused fileManager, NSString *sourceDirectory, NSString *destinationDirectory) { NSString *sourceFile = [sourceDirectory stringByAppendingPathComponent:@"A"]; NSString *destinationFile = [destinationDirectory stringByAppendingPathComponent:@"A"]; XCTAssertTrue([[self bigData1] writeToFile:sourceFile atomically:YES]); XCTAssertTrue([[self bigData2] writeToFile:destinationFile atomically:YES]); XCTAssertFalse([self testDirectoryHashEqualityWithSource:sourceDirectory destination:destinationDirectory]); }]; } - (void)testRegularFileAdded { [self createAndApplyPatchWithHandler:^(NSFileManager *__unused fileManager, NSString *sourceDirectory, NSString *destinationDirectory) { NSString *sourceFile = [sourceDirectory stringByAppendingPathComponent:@"A"]; NSString *destinationFile = [destinationDirectory stringByAppendingPathComponent:@"A"]; NSString *destinationFile2 = [destinationDirectory stringByAppendingPathComponent:@"B"]; XCTAssertTrue([[NSData data] writeToFile:sourceFile atomically:YES]); XCTAssertTrue([[NSData dataWithBytes:"loltest" length:7] writeToFile:destinationFile atomically:YES]); XCTAssertTrue([[NSData dataWithBytes:"lol" length:3] writeToFile:destinationFile2 atomically:YES]); XCTAssertFalse([self testDirectoryHashEqualityWithSource:sourceDirectory destination:destinationDirectory]); }]; } // Make sure old version patches are no longer supported - (void)testRegularFileAddedWithVersion1Delta { XCTAssertFalse([self createAndApplyPatchUsingVersion:SUBinaryDeltaMajorVersion1 compressionMode:SPUDeltaCompressionModeDefault beforeDiffHandler:nil afterDiffHandler:nil afterPatchHandler:nil]); } - (void)testDirectoryAdded { [self createAndApplyPatchWithHandler:^(NSFileManager *fileManager, NSString *sourceDirectory, NSString *destinationDirectory) { NSString *sourceFile = [sourceDirectory stringByAppendingPathComponent:@"A"]; NSString *destinationFile1 = [destinationDirectory stringByAppendingPathComponent:@"A"]; NSString *destinationFile2 = [destinationDirectory stringByAppendingPathComponent:@"B"]; NSString *destinationFile3 = [destinationFile2 stringByAppendingPathComponent:@"C"]; XCTAssertTrue([[NSData data] writeToFile:sourceFile atomically:YES]); XCTAssertTrue([[NSData dataWithBytes:"loltest" length:7] writeToFile:destinationFile1 atomically:YES]); NSError *error = nil; if (![fileManager createDirectoryAtPath:destinationFile2 withIntermediateDirectories:NO attributes:nil error:&error]) { NSLog(@"Failed creating directory with error: %@", error); XCTFail("Failed to create directory"); } XCTAssertTrue([[self bigData2] writeToFile:destinationFile3 atomically:YES]); XCTAssertFalse([self testDirectoryHashEqualityWithSource:sourceDirectory destination:destinationDirectory]); }]; } - (void)testDirectoryAddedWithOddPermissions { [self createAndApplyPatchWithHandler:^(NSFileManager *fileManager, NSString *sourceDirectory, NSString *destinationDirectory) { NSString *sourceFile = [sourceDirectory stringByAppendingPathComponent:@"A"]; NSString *destinationFile1 = [destinationDirectory stringByAppendingPathComponent:@"A"]; NSString *destinationFile2 = [destinationDirectory stringByAppendingPathComponent:@"B"]; NSString *destinationFile3 = [destinationFile2 stringByAppendingPathComponent:@"C"]; XCTAssertTrue([[NSData data] writeToFile:sourceFile atomically:YES]); XCTAssertTrue([[NSData dataWithBytes:"loltest" length:7] writeToFile:destinationFile1 atomically:YES]); NSError *error = nil; if (![fileManager createDirectoryAtPath:destinationFile2 withIntermediateDirectories:NO attributes:nil error:&error]) { NSLog(@"Failed creating directory with error: %@", error); XCTFail("Failed to create directory"); } XCTAssertTrue([[self bigData2] writeToFile:destinationFile3 atomically:YES]); if (![fileManager setAttributes:@{NSFilePosixPermissions : @0777} ofItemAtPath:destinationFile2 error:&error]) { NSLog(@"Change Permission Error: %@", error); XCTFail(@"Failed setting file permissions"); } XCTAssertFalse([self testDirectoryHashEqualityWithSource:sourceDirectory destination:destinationDirectory]); }]; } - (void)testDirectoryPermissionsChanged { [self createAndApplyPatchWithHandler:^(NSFileManager *fileManager, NSString *sourceDirectory, NSString *destinationDirectory) { NSString *sourceDir = [sourceDirectory stringByAppendingPathComponent:@"A"]; NSString *destDir = [destinationDirectory stringByAppendingPathComponent:@"A"]; NSError *error = nil; if (![fileManager createDirectoryAtPath:sourceDir withIntermediateDirectories:NO attributes:nil error:&error]) { NSLog(@"Failed creating directory with error: %@", error); XCTFail("Failed to create directory"); } if (![fileManager createDirectoryAtPath:destDir withIntermediateDirectories:NO attributes:nil error:&error]) { NSLog(@"Failed creating directory with error: %@", error); XCTFail("Failed to create directory"); } if (![fileManager setAttributes:@{NSFilePosixPermissions : @0777} ofItemAtPath:destDir error:&error]) { NSLog(@"Change Permission Error: %@", error); XCTFail(@"Failed setting file permissions"); } XCTAssertFalse([self testDirectoryHashEqualityWithSource:sourceDirectory destination:destinationDirectory]); }]; } - (void)testRegularFileRemoved { [self createAndApplyPatchWithHandler:^(NSFileManager *__unused fileManager, NSString *sourceDirectory, NSString *destinationDirectory) { NSString *sourceFile = [sourceDirectory stringByAppendingPathComponent:@"A"]; NSString *sourceFile2 = [destinationDirectory stringByAppendingPathComponent:@"B"]; NSString *destinationFile = [destinationDirectory stringByAppendingPathComponent:@"A"]; XCTAssertTrue([[NSData data] writeToFile:sourceFile atomically:YES]); XCTAssertTrue([[NSData dataWithBytes:"lol" length:3] writeToFile:sourceFile2 atomically:YES]); XCTAssertTrue([[NSData dataWithBytes:"loltest" length:7] writeToFile:destinationFile atomically:YES]); XCTAssertFalse([self testDirectoryHashEqualityWithSource:sourceDirectory destination:destinationDirectory]); }]; } - (void)testDirectoryRemoved { [self createAndApplyPatchWithHandler:^(NSFileManager *fileManager, NSString *sourceDirectory, NSString *destinationDirectory) { NSString *destinationFile = [destinationDirectory stringByAppendingPathComponent:@"A"]; NSString *sourceFile1 = [sourceDirectory stringByAppendingPathComponent:@"A"]; NSString *sourceFile2 = [sourceDirectory stringByAppendingPathComponent:@"B"]; NSString *sourceFile3 = [sourceFile2 stringByAppendingPathComponent:@"C"]; XCTAssertTrue([[NSData data] writeToFile:destinationFile atomically:YES]); XCTAssertTrue([[NSData dataWithBytes:"loltest" length:7] writeToFile:sourceFile1 atomically:YES]); NSError *error = nil; if (![fileManager createDirectoryAtPath:sourceFile2 withIntermediateDirectories:NO attributes:nil error:&error]) { NSLog(@"Failed creating directory with error: %@", error); XCTFail("Failed to create directory"); } XCTAssertTrue([[self bigData2] writeToFile:sourceFile3 atomically:YES]); XCTAssertFalse([self testDirectoryHashEqualityWithSource:sourceDirectory destination:destinationDirectory]); }]; } - (void)testRegularFileMove { [self createAndApplyPatchWithHandler:^(NSFileManager *fileManager, NSString *sourceDirectory, NSString *destinationDirectory) { NSString *sourceFile = [sourceDirectory stringByAppendingPathComponent:@"A/B/C/D/E/F/G/H/I/J/K/L/M/N/O/P/Q/R"]; NSString *destinationFile = [destinationDirectory stringByAppendingPathComponent:@"A/B/C/D/E/F/G/H/I/J/K/L/M/N/O/P/Q/R2"]; [fileManager createDirectoryAtURL:[NSURL fileURLWithPath:sourceFile.stringByDeletingLastPathComponent] withIntermediateDirectories:YES attributes:nil error:NULL]; [fileManager createDirectoryAtURL:[NSURL fileURLWithPath:destinationFile.stringByDeletingLastPathComponent] withIntermediateDirectories:YES attributes:nil error:NULL]; NSData *data = [NSData dataWithBytes:"loltes" length:6]; XCTAssertTrue([data writeToFile:sourceFile atomically:YES]); XCTAssertTrue([data writeToFile:destinationFile atomically:YES]); XCTAssertFalse([self testDirectoryHashEqualityWithSource:sourceDirectory destination:destinationDirectory]); }]; } - (void)testLargerRegularFileMoveWithFileInPlace { [self createAndApplyPatchWithHandler:^(NSFileManager *fileManager, NSString *sourceDirectory, NSString *destinationDirectory) { NSString *sourceFile = [sourceDirectory stringByAppendingPathComponent:@"A/B/C/D/E/F/G/H/I/J/K/L/M/N/O/P/Q/R"]; NSString *destinationFile = [destinationDirectory stringByAppendingPathComponent:@"A/B/C/D/E/F/G/H/I/J/K/L/M/N/O/P/Q/R2"]; NSString *destinationFile2 = [destinationDirectory stringByAppendingPathComponent:@"A/B/C/D/E/F/G/H/I/J/K/L/M/N/O/P/Q/R"]; NSString *destinationFile3 = [destinationDirectory stringByAppendingPathComponent:@"A/B/C/D/E/F/G/H/I/J/K/L/M/N/O/P/Q/X"]; [fileManager createDirectoryAtURL:[NSURL fileURLWithPath:sourceFile.stringByDeletingLastPathComponent] withIntermediateDirectories:YES attributes:nil error:NULL]; [fileManager createDirectoryAtURL:[NSURL fileURLWithPath:destinationFile.stringByDeletingLastPathComponent] withIntermediateDirectories:YES attributes:nil error:NULL]; NSData *data = [self bigData2]; XCTAssertTrue([data writeToFile:sourceFile atomically:YES]); XCTAssertTrue([data writeToFile:destinationFile atomically:YES]); XCTAssertTrue([data writeToFile:destinationFile2 atomically:YES]); XCTAssertTrue([data writeToFile:destinationFile3 atomically:YES]); XCTAssertFalse([self testDirectoryHashEqualityWithSource:sourceDirectory destination:destinationDirectory]); }]; } - (void)testRegularLargerFileMove { [self createAndApplyPatchWithHandler:^(NSFileManager *__unused fileManager, NSString *sourceDirectory, NSString *destinationDirectory) { NSString *sourceFile = [sourceDirectory stringByAppendingPathComponent:@"A"]; NSString *destinationFile = [destinationDirectory stringByAppendingPathComponent:@"B"]; NSData *data = [self bigData2]; XCTAssertTrue([data writeToFile:sourceFile atomically:YES]); XCTAssertTrue([data writeToFile:destinationFile atomically:YES]); XCTAssertFalse([self testDirectoryHashEqualityWithSource:sourceDirectory destination:destinationDirectory]); }]; } - (void)testRegularFileMoveWithPermissionChange { [self createAndApplyPatchWithHandler:^(NSFileManager *fileManager, NSString *sourceDirectory, NSString *destinationDirectory) { NSString *sourceFile = [sourceDirectory stringByAppendingPathComponent:@"A/B/C/D/E/F/G/H/I/J/K/L/M/N/O/P/Q/R"]; NSString *destinationFile = [destinationDirectory stringByAppendingPathComponent:@"A/B/C/D/E/F/G/H/I/J/K/L/M/N/O/P/Q/R2"]; [fileManager createDirectoryAtURL:[NSURL fileURLWithPath:sourceFile.stringByDeletingLastPathComponent] withIntermediateDirectories:YES attributes:nil error:NULL]; [fileManager createDirectoryAtURL:[NSURL fileURLWithPath:destinationFile.stringByDeletingLastPathComponent] withIntermediateDirectories:YES attributes:nil error:NULL]; NSData *data = [NSData dataWithBytes:"loltes" length:6]; XCTAssertTrue([data writeToFile:sourceFile atomically:YES]); XCTAssertTrue([data writeToFile:destinationFile atomically:YES]); if (chmod([destinationFile fileSystemRepresentation], 0777) != 0) { NSLog(@"Change Permission Error.."); XCTFail(@"Failed setting file permissions"); } XCTAssertFalse([self testDirectoryHashEqualityWithSource:sourceDirectory destination:destinationDirectory]); }]; } - (void)testRegularLargerFileMoveWithPermissionChange { [self createAndApplyPatchWithHandler:^(NSFileManager *__unused fileManager, NSString *sourceDirectory, NSString *destinationDirectory) { NSString *sourceFile = [sourceDirectory stringByAppendingPathComponent:@"A"]; NSString *destinationFile = [destinationDirectory stringByAppendingPathComponent:@"B"]; NSData *data = [self bigData2]; XCTAssertTrue([data writeToFile:sourceFile atomically:YES]); XCTAssertTrue([data writeToFile:destinationFile atomically:YES]); if (chmod([destinationFile fileSystemRepresentation], 0777) != 0) { NSLog(@"Change Permission Error.."); XCTFail(@"Failed setting file permissions"); } XCTAssertFalse([self testDirectoryHashEqualityWithSource:sourceDirectory destination:destinationDirectory]); }]; } - (void)testRegularLargerFileMoveWithNoWritablePermissionInSource { [self createAndApplyPatchWithHandler:^(NSFileManager *__unused fileManager, NSString *sourceDirectory, NSString *destinationDirectory) { NSString *sourceFile = [sourceDirectory stringByAppendingPathComponent:@"A"]; NSString *destinationFile = [destinationDirectory stringByAppendingPathComponent:@"B"]; NSData *data = [self bigData2]; XCTAssertTrue([data writeToFile:sourceFile atomically:YES]); XCTAssertTrue([data writeToFile:destinationFile atomically:YES]); if (chmod([sourceFile fileSystemRepresentation], 0444) != 0) { NSLog(@"Change Permission Error.."); XCTFail(@"Failed setting file permissions"); } XCTAssertFalse([self testDirectoryHashEqualityWithSource:sourceDirectory destination:destinationDirectory]); }]; } - (void)testRegularLargerFileMoveWithNoWritablePermissionInDestination { [self createAndApplyPatchWithHandler:^(NSFileManager *__unused fileManager, NSString *sourceDirectory, NSString *destinationDirectory) { NSString *sourceFile = [sourceDirectory stringByAppendingPathComponent:@"A"]; NSString *destinationFile = [destinationDirectory stringByAppendingPathComponent:@"B"]; NSData *data = [self bigData2]; XCTAssertTrue([data writeToFile:sourceFile atomically:YES]); XCTAssertTrue([data writeToFile:destinationFile atomically:YES]); if (chmod([destinationFile fileSystemRepresentation], 0444) != 0) { NSLog(@"Change Permission Error.."); XCTFail(@"Failed setting file permissions"); } XCTAssertFalse([self testDirectoryHashEqualityWithSource:sourceDirectory destination:destinationDirectory]); }]; } - (void)testRegularLargerFileMoveWithNoWritablePermissionInSourceAndOtherFileAtDestinationPresent { [self createAndApplyPatchWithHandler:^(NSFileManager *__unused fileManager, NSString *sourceDirectory, NSString *destinationDirectory) { NSString *sourceFile = [sourceDirectory stringByAppendingPathComponent:@"A"]; NSString *destinationFile = [destinationDirectory stringByAppendingPathComponent:@"B"]; NSString *destinationFile2 = [destinationDirectory stringByAppendingPathComponent:@"A"]; NSData *data = [self bigData2]; XCTAssertTrue([data writeToFile:sourceFile atomically:YES]); XCTAssertTrue([data writeToFile:destinationFile atomically:YES]); XCTAssertTrue([[self bigData1] writeToFile:destinationFile2 atomically:YES]); if (chmod([sourceFile fileSystemRepresentation], 0444) != 0) { NSLog(@"Change Permission Error.."); XCTFail(@"Failed setting file permissions"); } XCTAssertFalse([self testDirectoryHashEqualityWithSource:sourceDirectory destination:destinationDirectory]); }]; } - (void)testRegularLargerFileMoveWithNoWritablePermissionInDestinationAndOtherFileAtDestinationPresent { [self createAndApplyPatchWithHandler:^(NSFileManager *__unused fileManager, NSString *sourceDirectory, NSString *destinationDirectory) { NSString *sourceFile = [sourceDirectory stringByAppendingPathComponent:@"A"]; NSString *destinationFile = [destinationDirectory stringByAppendingPathComponent:@"B"]; NSString *destinationFile2 = [destinationDirectory stringByAppendingPathComponent:@"A"]; NSData *data = [self bigData2]; XCTAssertTrue([data writeToFile:sourceFile atomically:YES]); XCTAssertTrue([data writeToFile:destinationFile atomically:YES]); XCTAssertTrue([[self bigData1] writeToFile:destinationFile2 atomically:YES]); if (chmod([destinationFile fileSystemRepresentation], 0444) != 0) { NSLog(@"Change Permission Error.."); XCTFail(@"Failed setting file permissions"); } XCTAssertFalse([self testDirectoryHashEqualityWithSource:sourceDirectory destination:destinationDirectory]); }]; } - (void)testRegularLargerFileMoveWithNoWritablePermissionInSourceAndOtherFileAtSourcePresent { [self createAndApplyPatchWithHandler:^(NSFileManager *__unused fileManager, NSString *sourceDirectory, NSString *destinationDirectory) { NSString *sourceFile = [sourceDirectory stringByAppendingPathComponent:@"A"]; NSString *destinationFile = [destinationDirectory stringByAppendingPathComponent:@"B"]; NSString *sourceFile2 = [destinationDirectory stringByAppendingPathComponent:@"B"]; NSData *data = [self bigData2]; XCTAssertTrue([data writeToFile:sourceFile atomically:YES]); XCTAssertTrue([data writeToFile:destinationFile atomically:YES]); XCTAssertTrue([[self bigData1] writeToFile:sourceFile2 atomically:YES]); if (chmod([sourceFile fileSystemRepresentation], 0444) != 0) { NSLog(@"Change Permission Error.."); XCTFail(@"Failed setting file permissions"); } if (chmod([sourceFile2 fileSystemRepresentation], 0444) != 0) { NSLog(@"Change Permission Error.."); XCTFail(@"Failed setting file permissions"); } XCTAssertFalse([self testDirectoryHashEqualityWithSource:sourceDirectory destination:destinationDirectory]); }]; } - (void)testRegularLargerFileMoveWithNoWritablePermissionInDestinationAndOtherFileAtSourcePresent { [self createAndApplyPatchWithHandler:^(NSFileManager *__unused fileManager, NSString *sourceDirectory, NSString *destinationDirectory) { NSString *sourceFile = [sourceDirectory stringByAppendingPathComponent:@"A"]; NSString *destinationFile = [destinationDirectory stringByAppendingPathComponent:@"B"]; NSString *sourceFile2 = [destinationDirectory stringByAppendingPathComponent:@"B"]; NSData *data = [self bigData2]; XCTAssertTrue([data writeToFile:sourceFile atomically:YES]); XCTAssertTrue([data writeToFile:destinationFile atomically:YES]); XCTAssertTrue([[self bigData1] writeToFile:sourceFile2 atomically:YES]); if (chmod([destinationFile fileSystemRepresentation], 0444) != 0) { NSLog(@"Change Permission Error.."); XCTFail(@"Failed setting file permissions"); } XCTAssertFalse([self testDirectoryHashEqualityWithSource:sourceDirectory destination:destinationDirectory]); }]; } - (void)testSymbolicLinkMove { [self createAndApplyPatchWithHandler:^(NSFileManager *fileManager, NSString *sourceDirectory, NSString *destinationDirectory) { NSString *sourceFile = [sourceDirectory stringByAppendingPathComponent:@"A/B/C/D/E/F/G/H/I/J/K/L/M/N/O/P/Q/R"]; NSString *destinationFile = [destinationDirectory stringByAppendingPathComponent:@"A/B/C/D/E/F/G/H/I/J/K/L/M/N/O/P/Q/R2"]; [fileManager createDirectoryAtURL:[NSURL fileURLWithPath:sourceFile.stringByDeletingLastPathComponent] withIntermediateDirectories:YES attributes:nil error:NULL]; [fileManager createDirectoryAtURL:[NSURL fileURLWithPath:destinationFile.stringByDeletingLastPathComponent] withIntermediateDirectories:YES attributes:nil error:NULL]; NSError *error = nil; if (![fileManager createSymbolicLinkAtPath:sourceFile withDestinationPath:@"C" error:&error]) { NSLog(@"Error in creating symlink: %@", error); XCTFail(@"Failed to create symlink"); } if (![fileManager createSymbolicLinkAtPath:destinationFile withDestinationPath:@"C" error:&error]) { NSLog(@"Error in creating symlink: %@", error); XCTFail(@"Failed to create symlink"); } XCTAssertFalse([self testDirectoryHashEqualityWithSource:sourceDirectory destination:destinationDirectory]); }]; } - (void)testLargerSymbolicLinkMove { [self createAndApplyPatchWithHandler:^(NSFileManager *fileManager, NSString *sourceDirectory, NSString *destinationDirectory) { NSString *sourceFile = [sourceDirectory stringByAppendingPathComponent:@"A"]; NSString *destinationFile = [destinationDirectory stringByAppendingPathComponent:@"B"]; NSError *error = nil; NSString *destinationPath = @"loltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltest"; if (![fileManager createSymbolicLinkAtPath:sourceFile withDestinationPath:destinationPath error:&error]) { NSLog(@"Error in creating symlink: %@", error); XCTFail(@"Failed to create symlink"); } if (![fileManager createSymbolicLinkAtPath:destinationFile withDestinationPath:destinationPath error:&error]) { NSLog(@"Error in creating symlink: %@", error); XCTFail(@"Failed to create symlink"); } XCTAssertFalse([self testDirectoryHashEqualityWithSource:sourceDirectory destination:destinationDirectory]); }]; } - (void)testSymbolicLinkMoveWithPermissionChange { [self createAndApplyPatchWithHandler:^(NSFileManager *fileManager, NSString *sourceDirectory, NSString *destinationDirectory) { NSString *sourceFile = [sourceDirectory stringByAppendingPathComponent:@"A/B/C/D/E/F/G/H/I/J/K/L/M/N/O/P/Q/R"]; NSString *destinationFile = [destinationDirectory stringByAppendingPathComponent:@"A/B/C/D/E/F/G/H/I/J/K/L/M/N/O/P/Q/R2"]; [fileManager createDirectoryAtURL:[NSURL fileURLWithPath:sourceFile.stringByDeletingLastPathComponent] withIntermediateDirectories:YES attributes:nil error:NULL]; [fileManager createDirectoryAtURL:[NSURL fileURLWithPath:destinationFile.stringByDeletingLastPathComponent] withIntermediateDirectories:YES attributes:nil error:NULL]; NSError *error = nil; if (![fileManager createSymbolicLinkAtPath:sourceFile withDestinationPath:@"C" error:&error]) { NSLog(@"Error in creating symlink: %@", error); XCTFail(@"Failed to create symlink"); } if (![fileManager createSymbolicLinkAtPath:destinationFile withDestinationPath:@"C" error:&error]) { NSLog(@"Error in creating symlink: %@", error); XCTFail(@"Failed to create symlink"); } if (lchmod([destinationFile fileSystemRepresentation], 0777) != 0) { NSLog(@"Change Permission Error.."); XCTFail(@"Failed setting file permissions"); } XCTAssertFalse([self testDirectoryHashEqualityWithSource:sourceDirectory destination:destinationDirectory]); }]; } - (void)testLargerSymbolicLinkMoveWithPermissionChange { [self createAndApplyPatchWithHandler:^(NSFileManager *fileManager, NSString *sourceDirectory, NSString *destinationDirectory) { NSString *sourceFile = [sourceDirectory stringByAppendingPathComponent:@"A"]; NSString *destinationFile = [destinationDirectory stringByAppendingPathComponent:@"B"]; NSError *error = nil; NSString *destinationPath = @"loltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltest"; if (![fileManager createSymbolicLinkAtPath:sourceFile withDestinationPath:destinationPath error:&error]) { NSLog(@"Error in creating symlink: %@", error); XCTFail(@"Failed to create symlink"); } if (![fileManager createSymbolicLinkAtPath:destinationFile withDestinationPath:destinationPath error:&error]) { NSLog(@"Error in creating symlink: %@", error); XCTFail(@"Failed to create symlink"); } if (lchmod([destinationFile fileSystemRepresentation], 0777) != 0) { NSLog(@"Change Permission Error.."); XCTFail(@"Failed setting file permissions"); } XCTAssertFalse([self testDirectoryHashEqualityWithSource:sourceDirectory destination:destinationDirectory]); }]; } - (void)testCaseSensitiveRegularFileMove { [self createAndApplyPatchWithHandler:^(NSFileManager *__unused fileManager, NSString *sourceDirectory, NSString *destinationDirectory) { NSString *sourceFile1 = [sourceDirectory stringByAppendingPathComponent:@"A"]; NSString *sourceFile2 = [sourceDirectory stringByAppendingPathComponent:@"b"]; NSString *destinationFile1 = [destinationDirectory stringByAppendingPathComponent:@"a"]; NSString *destinationFile2 = [destinationDirectory stringByAppendingPathComponent:@"B"]; NSData *data = [NSData dataWithBytes:"loltest" length:7]; XCTAssertTrue([data writeToFile:sourceFile1 atomically:YES]); XCTAssertTrue([data writeToFile:sourceFile2 atomically:YES]); XCTAssertTrue([data writeToFile:destinationFile1 atomically:YES]); XCTAssertTrue([data writeToFile:destinationFile2 atomically:YES]); XCTAssertFalse([self testDirectoryHashEqualityWithSource:sourceDirectory destination:destinationDirectory]); }]; } - (void)testRemovingSymlink { [self createAndApplyPatchWithHandler:^(NSFileManager *fileManager, NSString *sourceDirectory, NSString *destinationDirectory) { NSString *sourceFile = [sourceDirectory stringByAppendingPathComponent:@"A"]; NSError *error = nil; if (![fileManager createSymbolicLinkAtPath:sourceFile withDestinationPath:@"B" error:&error]) { NSLog(@"Error in creating symlink: %@", error); XCTFail(@"Failed to create symlink"); } XCTAssertFalse([self testDirectoryHashEqualityWithSource:sourceDirectory destination:destinationDirectory]); }]; } - (void)testAddingSymlink { [self createAndApplyPatchWithBeforeDiffHandler:^(NSFileManager *fileManager, NSString *sourceDirectory, NSString *destinationDirectory) { NSString *destinationFile = [destinationDirectory stringByAppendingPathComponent:@"A"]; NSError *error = nil; if (![fileManager createSymbolicLinkAtPath:destinationFile withDestinationPath:@"B" error:&error]) { NSLog(@"Error in creating symlink: %@", error); XCTFail(@"Failed to create symlink"); } XCTAssertFalse([self testDirectoryHashEqualityWithSource:sourceDirectory destination:destinationDirectory]); } afterDiffHandler:nil afterPatchHandler:^(NSFileManager *fileManager, NSString * __unused sourceDirectory, NSString *destinationDirectory) { NSString *destinationFile = [destinationDirectory stringByAppendingPathComponent:@"A"]; NSError *error = nil; NSDictionary *attributes = [fileManager attributesOfItemAtPath:destinationFile error:&error]; if (attributes == nil) { NSLog(@"Failed to retrieve attributes with error: %@", error); XCTFail("Failed to retrieve symlink attributes"); } NSNumber *permissionAttribute = attributes[NSFilePosixPermissions]; XCTAssertNotNil(permissionAttribute); // Test default symlink permissions are correct unsigned short permissions = permissionAttribute.unsignedShortValue & PERMISSION_FLAGS; XCTAssertEqual(permissions, VALID_SYMBOLIC_LINK_PERMISSIONS); }]; } - (void)testAddingSymlinkWithWrongPermissions { [self createAndApplyPatchWithBeforeDiffHandler:^(NSFileManager *fileManager, NSString *sourceDirectory, NSString *destinationDirectory) { NSString *destinationFile = [destinationDirectory stringByAppendingPathComponent:@"A"]; NSError *error = nil; if (![fileManager createSymbolicLinkAtPath:destinationFile withDestinationPath:@"B" error:&error]) { NSLog(@"Error in creating symlink: %@", error); XCTFail(@"Failed to create symlink"); } if (lchmod([destinationFile fileSystemRepresentation], 0777) != 0) { NSLog(@"Change Permission Error.."); XCTFail(@"Failed setting file permissions"); } XCTAssertFalse([self testDirectoryHashEqualityWithSource:sourceDirectory destination:destinationDirectory]); } afterDiffHandler:nil afterPatchHandler:^(NSFileManager *fileManager, NSString * __unused sourceDirectory, NSString *destinationDirectory) { NSString *destinationFile = [destinationDirectory stringByAppendingPathComponent:@"A"]; NSError *error = nil; NSDictionary *attributes = [fileManager attributesOfItemAtPath:destinationFile error:&error]; if (attributes == nil) { NSLog(@"Failed to retrieve attributes with error: %@", error); XCTFail("Failed to retrieve symlink attributes"); } NSNumber *permissionAttribute = attributes[NSFilePosixPermissions]; XCTAssertNotNil(permissionAttribute); // Test that we only respect valid symlink permissions for >= version 3 deltas unsigned short permissions = permissionAttribute.unsignedShortValue & PERMISSION_FLAGS; XCTAssertEqual(permissions, VALID_SYMBOLIC_LINK_PERMISSIONS); } testingVersion3Delta:YES testingVersion2Delta:NO]; } - (void)testSmallFilePermissionChangeWithNoContentChange { [self createAndApplyPatchWithHandler:^(NSFileManager *fileManager, NSString *sourceDirectory, NSString *destinationDirectory) { NSString *sourceFile = [sourceDirectory stringByAppendingPathComponent:@"A"]; NSString *destinationFile = [destinationDirectory stringByAppendingPathComponent:@"A"]; NSData *data = [NSData dataWithBytes:"loltest" length:7]; XCTAssertTrue([data writeToFile:sourceFile atomically:YES]); XCTAssertTrue([data writeToFile:destinationFile atomically:YES]); NSError *error = nil; if (![fileManager setAttributes:@{NSFilePosixPermissions : @0755} ofItemAtPath:destinationFile error:&error]) { NSLog(@"Change Permission Error: %@", error); XCTFail(@"Failed setting file permissions"); } XCTAssertFalse([self testDirectoryHashEqualityWithSource:sourceDirectory destination:destinationDirectory]); }]; } - (void)testBigFilePermissionChangeWithNoContentChange { [self createAndApplyPatchWithHandler:^(NSFileManager *fileManager, NSString *sourceDirectory, NSString *destinationDirectory) { NSString *sourceFile = [sourceDirectory stringByAppendingPathComponent:@"A"]; NSString *destinationFile = [destinationDirectory stringByAppendingPathComponent:@"A"]; NSData *data = [self bigData1]; XCTAssertTrue([data writeToFile:sourceFile atomically:YES]); XCTAssertTrue([data writeToFile:destinationFile atomically:YES]); NSError *error = nil; if (![fileManager setAttributes:@{NSFilePosixPermissions : @0755} ofItemAtPath:destinationFile error:&error]) { NSLog(@"Change Permission Error: %@", error); XCTFail(@"Failed setting file permissions"); } XCTAssertFalse([self testDirectoryHashEqualityWithSource:sourceDirectory destination:destinationDirectory]); }]; } - (void)testSmallFilePermissionChangeWithContentChange { [self createAndApplyPatchWithHandler:^(NSFileManager *fileManager, NSString *sourceDirectory, NSString *destinationDirectory) { NSString *sourceFile = [sourceDirectory stringByAppendingPathComponent:@"A"]; NSString *destinationFile = [destinationDirectory stringByAppendingPathComponent:@"A"]; XCTAssertTrue([[NSData data] writeToFile:sourceFile atomically:YES]); XCTAssertTrue([[NSData dataWithBytes:@"lawl" length:4] writeToFile:destinationFile atomically:YES]); NSError *error = nil; if (![fileManager setAttributes:@{NSFilePosixPermissions : @0755} ofItemAtPath:destinationFile error:&error]) { NSLog(@"Change Permission Error: %@", error); XCTFail(@"Failed setting file permissions"); } XCTAssertFalse([self testDirectoryHashEqualityWithSource:sourceDirectory destination:destinationDirectory]); }]; } - (void)testBigFilePermissionChangeWithContentChange { [self createAndApplyPatchWithHandler:^(NSFileManager *fileManager, NSString *sourceDirectory, NSString *destinationDirectory) { NSString *sourceFile = [sourceDirectory stringByAppendingPathComponent:@"A"]; NSString *destinationFile = [destinationDirectory stringByAppendingPathComponent:@"A"]; XCTAssertTrue([[self bigData1] writeToFile:sourceFile atomically:YES]); XCTAssertTrue([[self bigData2] writeToFile:destinationFile atomically:YES]); NSError *error = nil; if (![fileManager setAttributes:@{NSFilePosixPermissions : @0755} ofItemAtPath:destinationFile error:&error]) { NSLog(@"Change Permission Error: %@", error); XCTFail(@"Failed setting file permissions"); } XCTAssertFalse([self testDirectoryHashEqualityWithSource:sourceDirectory destination:destinationDirectory]); }]; } - (void)testBigFilePermissionChangeInDirectoriesWithContentChange { [self createAndApplyPatchWithHandler:^(NSFileManager *fileManager, NSString *sourceDirectory, NSString *destinationDirectory) { NSString *sourceA = [sourceDirectory stringByAppendingPathComponent:@"Contents/Hello/"]; NSString *destinationB = [destinationDirectory stringByAppendingPathComponent:@"Contents/Meek/"]; XCTAssertTrue([fileManager createDirectoryAtPath:sourceA withIntermediateDirectories:YES attributes:NULL error:NULL]); XCTAssertTrue([fileManager createDirectoryAtPath:destinationB withIntermediateDirectories:YES attributes:NULL error:NULL]); NSString *sourceFile = [sourceA stringByAppendingPathComponent:@"A"]; NSString *destinationFile = [destinationB stringByAppendingPathComponent:@"A"]; XCTAssertTrue([[self bigData1] writeToFile:sourceFile atomically:YES]); XCTAssertTrue([[self bigData2] writeToFile:destinationFile atomically:YES]); NSError *error = nil; if (![fileManager setAttributes:@{NSFilePosixPermissions : @0755} ofItemAtPath:destinationFile error:&error]) { NSLog(@"Change Permission Error: %@", error); XCTFail(@"Failed setting file permissions"); } XCTAssertFalse([self testDirectoryHashEqualityWithSource:sourceDirectory destination:destinationDirectory]); }]; } - (void)testBigFileChangeInDirectoriesWithContentChange { [self createAndApplyPatchWithHandler:^(NSFileManager *fileManager, NSString *sourceDirectory, NSString *destinationDirectory) { NSString *sourceA = [sourceDirectory stringByAppendingPathComponent:@"Contents/Hello/"]; NSString *destinationA = [destinationDirectory stringByAppendingPathComponent:@"Contents/Meek/"]; XCTAssertTrue([fileManager createDirectoryAtPath:sourceA withIntermediateDirectories:YES attributes:NULL error:NULL]); XCTAssertTrue([fileManager createDirectoryAtPath:destinationA withIntermediateDirectories:YES attributes:NULL error:NULL]); NSString *sourceFile = [sourceA stringByAppendingPathComponent:@"A"]; NSString *destinationFile = [destinationA stringByAppendingPathComponent:@"A"]; XCTAssertTrue([[self bigData1] writeToFile:sourceFile atomically:YES]); XCTAssertTrue([[self bigData2] writeToFile:destinationFile atomically:YES]); XCTAssertFalse([self testDirectoryHashEqualityWithSource:sourceDirectory destination:destinationDirectory]); }]; } - (void)testBigFileChangeInDirectoriesWithContentChangeAndNoWritablePermissionInSource { [self createAndApplyPatchWithHandler:^(NSFileManager *fileManager, NSString *sourceDirectory, NSString *destinationDirectory) { NSString *sourceA = [sourceDirectory stringByAppendingPathComponent:@"Contents/Hello/"]; NSString *destinationA = [destinationDirectory stringByAppendingPathComponent:@"Contents/Meek/"]; XCTAssertTrue([fileManager createDirectoryAtPath:sourceA withIntermediateDirectories:YES attributes:NULL error:NULL]); XCTAssertTrue([fileManager createDirectoryAtPath:destinationA withIntermediateDirectories:YES attributes:NULL error:NULL]); NSString *sourceFile = [sourceA stringByAppendingPathComponent:@"A"]; NSString *destinationFile = [destinationA stringByAppendingPathComponent:@"A"]; XCTAssertTrue([[self bigData1] writeToFile:sourceFile atomically:YES]); XCTAssertTrue([[self bigData2] writeToFile:destinationFile atomically:YES]); NSError *error = nil; if (![fileManager setAttributes:@{NSFilePosixPermissions : @0444} ofItemAtPath:sourceFile error:&error]) { NSLog(@"Change Permission Error: %@", error); XCTFail(@"Failed setting file permissions"); } XCTAssertFalse([self testDirectoryHashEqualityWithSource:sourceDirectory destination:destinationDirectory]); }]; } - (void)testBigFileChangeInDirectoriesWithContentChangeAndNoWritablePermissionInDestination { [self createAndApplyPatchWithHandler:^(NSFileManager *fileManager, NSString *sourceDirectory, NSString *destinationDirectory) { NSString *sourceA = [sourceDirectory stringByAppendingPathComponent:@"Contents/Hello/"]; NSString *destinationA = [destinationDirectory stringByAppendingPathComponent:@"Contents/Meek/"]; XCTAssertTrue([fileManager createDirectoryAtPath:sourceA withIntermediateDirectories:YES attributes:NULL error:NULL]); XCTAssertTrue([fileManager createDirectoryAtPath:destinationA withIntermediateDirectories:YES attributes:NULL error:NULL]); NSString *sourceFile = [sourceA stringByAppendingPathComponent:@"A"]; NSString *destinationFile = [destinationA stringByAppendingPathComponent:@"A"]; XCTAssertTrue([[self bigData1] writeToFile:sourceFile atomically:YES]); XCTAssertTrue([[self bigData2] writeToFile:destinationFile atomically:YES]); NSError *error = nil; if (![fileManager setAttributes:@{NSFilePosixPermissions : @0444} ofItemAtPath:destinationFile error:&error]) { NSLog(@"Change Permission Error: %@", error); XCTFail(@"Failed setting file permissions"); } XCTAssertFalse([self testDirectoryHashEqualityWithSource:sourceDirectory destination:destinationDirectory]); }]; } - (void)testMultipleBigFileChangeInDirectoriesWithContentChange { [self createAndApplyPatchWithHandler:^(NSFileManager *fileManager, NSString *sourceDirectory, NSString *destinationDirectory) { NSString *sourceA = [sourceDirectory stringByAppendingPathComponent:@"Contents/Hello/"]; NSString *sourceA2 = [sourceDirectory stringByAppendingPathComponent:@"Contents/whaat/"]; NSString *destinationB = [destinationDirectory stringByAppendingPathComponent:@"Contents/Meek/"]; XCTAssertTrue([fileManager createDirectoryAtPath:sourceA withIntermediateDirectories:YES attributes:NULL error:NULL]); XCTAssertTrue([fileManager createDirectoryAtPath:sourceA2 withIntermediateDirectories:YES attributes:NULL error:NULL]); XCTAssertTrue([fileManager createDirectoryAtPath:destinationB withIntermediateDirectories:YES attributes:NULL error:NULL]); NSString *sourceFile = [sourceA stringByAppendingPathComponent:@"A"]; NSString *sourceFile2 = [sourceA2 stringByAppendingPathComponent:@"A"]; NSString *destinationFile = [destinationB stringByAppendingPathComponent:@"A"]; NSMutableData *data2 = [NSMutableData dataWithData:[self bigData1]]; uint32_t foo = 100; [data2 appendBytes:&foo length:sizeof(foo)]; XCTAssertTrue([[self bigData1] writeToFile:sourceFile atomically:YES]); XCTAssertTrue([data2 writeToFile:sourceFile2 atomically:YES]); XCTAssertTrue([[self bigData2] writeToFile:destinationFile atomically:YES]); XCTAssertFalse([self testDirectoryHashEqualityWithSource:sourceDirectory destination:destinationDirectory]); }]; } - (void)testSmallFileNoWritablePermissionInSourceWithNoContentChange { [self createAndApplyPatchWithHandler:^(NSFileManager *fileManager, NSString *sourceDirectory, NSString *destinationDirectory) { NSString *sourceFile = [sourceDirectory stringByAppendingPathComponent:@"A"]; NSString *destinationFile = [destinationDirectory stringByAppendingPathComponent:@"A"]; XCTAssertTrue([[NSData dataWithBytes:@"lawl" length:4] writeToFile:sourceFile atomically:YES]); XCTAssertTrue([[NSData dataWithBytes:@"lawl" length:4] writeToFile:destinationFile atomically:YES]); NSError *error = nil; if (![fileManager setAttributes:@{NSFilePosixPermissions : @0444} ofItemAtPath:sourceFile error:&error]) { NSLog(@"Change Permission Error: %@", error); XCTFail(@"Failed setting file permissions"); } XCTAssertFalse([self testDirectoryHashEqualityWithSource:sourceDirectory destination:destinationDirectory]); }]; } - (void)testSmallFileNoWritablePermissionInDestinationWithNoContentChange { [self createAndApplyPatchWithHandler:^(NSFileManager *fileManager, NSString *sourceDirectory, NSString *destinationDirectory) { NSString *sourceFile = [sourceDirectory stringByAppendingPathComponent:@"A"]; NSString *destinationFile = [destinationDirectory stringByAppendingPathComponent:@"A"]; XCTAssertTrue([[NSData dataWithBytes:@"lawl" length:4] writeToFile:sourceFile atomically:YES]); XCTAssertTrue([[NSData dataWithBytes:@"lawl" length:4] writeToFile:destinationFile atomically:YES]); NSError *error = nil; if (![fileManager setAttributes:@{NSFilePosixPermissions : @0444} ofItemAtPath:destinationFile error:&error]) { NSLog(@"Change Permission Error: %@", error); XCTFail(@"Failed setting file permissions"); } XCTAssertFalse([self testDirectoryHashEqualityWithSource:sourceDirectory destination:destinationDirectory]); }]; } - (void)testBigFileNoWritablePermissionInSourceWithNoContentChange { [self createAndApplyPatchWithHandler:^(NSFileManager *fileManager, NSString *sourceDirectory, NSString *destinationDirectory) { NSString *sourceFile = [sourceDirectory stringByAppendingPathComponent:@"A"]; NSString *destinationFile = [destinationDirectory stringByAppendingPathComponent:@"A"]; XCTAssertTrue([[self bigData1] writeToFile:sourceFile atomically:YES]); XCTAssertTrue([[self bigData1] writeToFile:destinationFile atomically:YES]); NSError *error = nil; if (![fileManager setAttributes:@{NSFilePosixPermissions : @0444} ofItemAtPath:sourceFile error:&error]) { NSLog(@"Change Permission Error: %@", error); XCTFail(@"Failed setting file permissions"); } XCTAssertFalse([self testDirectoryHashEqualityWithSource:sourceDirectory destination:destinationDirectory]); }]; } - (void)testBigFileNoWritablePermissionInDestinationWithNoContentChange { [self createAndApplyPatchWithHandler:^(NSFileManager *fileManager, NSString *sourceDirectory, NSString *destinationDirectory) { NSString *sourceFile = [sourceDirectory stringByAppendingPathComponent:@"A"]; NSString *destinationFile = [destinationDirectory stringByAppendingPathComponent:@"A"]; XCTAssertTrue([[self bigData1] writeToFile:sourceFile atomically:YES]); XCTAssertTrue([[self bigData1] writeToFile:destinationFile atomically:YES]); NSError *error = nil; if (![fileManager setAttributes:@{NSFilePosixPermissions : @0444} ofItemAtPath:destinationFile error:&error]) { NSLog(@"Change Permission Error: %@", error); XCTFail(@"Failed setting file permissions"); } XCTAssertFalse([self testDirectoryHashEqualityWithSource:sourceDirectory destination:destinationDirectory]); }]; } - (void)testSmallFileNoWritablePermissionInSourceWithContentChange { [self createAndApplyPatchWithHandler:^(NSFileManager *fileManager, NSString *sourceDirectory, NSString *destinationDirectory) { NSString *sourceFile = [sourceDirectory stringByAppendingPathComponent:@"A"]; NSString *destinationFile = [destinationDirectory stringByAppendingPathComponent:@"A"]; XCTAssertTrue([[NSData data] writeToFile:sourceFile atomically:YES]); XCTAssertTrue([[NSData dataWithBytes:@"lawl" length:4] writeToFile:destinationFile atomically:YES]); NSError *error = nil; if (![fileManager setAttributes:@{NSFilePosixPermissions : @0444} ofItemAtPath:sourceFile error:&error]) { NSLog(@"Change Permission Error: %@", error); XCTFail(@"Failed setting file permissions"); } XCTAssertFalse([self testDirectoryHashEqualityWithSource:sourceDirectory destination:destinationDirectory]); }]; } - (void)testSmallFileNoWritablePermissionInDestinationWithContentChange { [self createAndApplyPatchWithHandler:^(NSFileManager *fileManager, NSString *sourceDirectory, NSString *destinationDirectory) { NSString *sourceFile = [sourceDirectory stringByAppendingPathComponent:@"A"]; NSString *destinationFile = [destinationDirectory stringByAppendingPathComponent:@"A"]; XCTAssertTrue([[NSData data] writeToFile:sourceFile atomically:YES]); XCTAssertTrue([[NSData dataWithBytes:@"lawl" length:4] writeToFile:destinationFile atomically:YES]); NSError *error = nil; if (![fileManager setAttributes:@{NSFilePosixPermissions : @0444} ofItemAtPath:destinationFile error:&error]) { NSLog(@"Change Permission Error: %@", error); XCTFail(@"Failed setting file permissions"); } XCTAssertFalse([self testDirectoryHashEqualityWithSource:sourceDirectory destination:destinationDirectory]); }]; } - (void)testBigFileNoWritablePermissionInSourceWithContentChange { [self createAndApplyPatchWithHandler:^(NSFileManager *fileManager, NSString *sourceDirectory, NSString *destinationDirectory) { NSString *sourceFile = [sourceDirectory stringByAppendingPathComponent:@"A"]; NSString *destinationFile = [destinationDirectory stringByAppendingPathComponent:@"A"]; XCTAssertTrue([[self bigData1] writeToFile:sourceFile atomically:YES]); XCTAssertTrue([[self bigData2] writeToFile:destinationFile atomically:YES]); NSError *error = nil; if (![fileManager setAttributes:@{NSFilePosixPermissions : @0444} ofItemAtPath:sourceFile error:&error]) { NSLog(@"Change Permission Error: %@", error); XCTFail(@"Failed setting file permissions"); } XCTAssertFalse([self testDirectoryHashEqualityWithSource:sourceDirectory destination:destinationDirectory]); }]; } - (void)testBigFileNoWritablePermissionInDestinationWithContentChange { [self createAndApplyPatchWithHandler:^(NSFileManager *fileManager, NSString *sourceDirectory, NSString *destinationDirectory) { NSString *sourceFile = [sourceDirectory stringByAppendingPathComponent:@"A"]; NSString *destinationFile = [destinationDirectory stringByAppendingPathComponent:@"A"]; XCTAssertTrue([[self bigData1] writeToFile:sourceFile atomically:YES]); XCTAssertTrue([[self bigData2] writeToFile:destinationFile atomically:YES]); NSError *error = nil; if (![fileManager setAttributes:@{NSFilePosixPermissions : @0444} ofItemAtPath:destinationFile error:&error]) { NSLog(@"Change Permission Error: %@", error); XCTFail(@"Failed setting file permissions"); } XCTAssertFalse([self testDirectoryHashEqualityWithSource:sourceDirectory destination:destinationDirectory]); }]; } - (void)testFileSystemCompression { [self createAndApplyPatchWithBeforeDiffHandler:^(NSFileManager *__unused fileManager, NSString *sourceDirectory, NSString *destinationDirectory) { NSString *sourceFile = [sourceDirectory stringByAppendingPathComponent:@"A"]; NSString *destinationFile = [destinationDirectory stringByAppendingPathComponent:@"A"]; NSString *destinationFile2 = [destinationDirectory stringByAppendingPathComponent:@"A2"]; XCTAssertTrue([[self bigData1] writeToFile:sourceFile atomically:YES]); XCTAssertTrue([[self bigData2] writeToFile:destinationFile atomically:YES]); { // We only track executable files to decide if we want to apply file system compression over the entire bundle int lchmodResult = lchmod(destinationFile.fileSystemRepresentation, 0755); XCTAssertEqual(lchmodResult, 0); } NSTask *dittoTask = [[NSTask alloc] init]; dittoTask.executableURL = [NSURL fileURLWithPath:@"/usr/bin/ditto" isDirectory:NO]; dittoTask.arguments = @[@"--hfsCompression", destinationFile, destinationFile2]; NSError *launchError = nil; BOOL launched = [dittoTask launchAndReturnError:&launchError]; if (!launched) { XCTFail(@"Failed to launch ditto: %@", launchError); } [dittoTask waitUntilExit]; XCTAssertEqual(dittoTask.terminationStatus, 0); XCTAssertFalse([self testDirectoryHashEqualityWithSource:sourceDirectory destination:destinationDirectory]); } afterDiffHandler:nil afterPatchHandler:^(NSFileManager *__unused fileManager, NSString *__unused sourceDirectory, NSString *destinationDirectory) { NSString *destinationFile = [destinationDirectory stringByAppendingPathComponent:@"A"]; NSString *destinationFile2 = [destinationDirectory stringByAppendingPathComponent:@"A2"]; // Both files should have file system compression applied { struct stat statStruct = {0}; int result = lstat(destinationFile.fileSystemRepresentation, &statStruct); XCTAssertEqual(result, 0); if ((statStruct.st_flags & UF_COMPRESSED) == 0) { XCTFail(@"First destination file is not compressed!"); } } { struct stat statStruct = {0}; int result = lstat(destinationFile2.fileSystemRepresentation, &statStruct); XCTAssertEqual(result, 0); if ((statStruct.st_flags & UF_COMPRESSED) == 0) { XCTFail(@"Second destination file is not compressed!"); } } } testingVersion3Delta:YES testingVersion2Delta:NO]; } - (void)testNoFileSystemCompression { [self createAndApplyPatchWithBeforeDiffHandler:^(NSFileManager *__unused fileManager, NSString *sourceDirectory, NSString *destinationDirectory) { NSString *sourceFile = [sourceDirectory stringByAppendingPathComponent:@"A"]; NSString *destinationFile = [destinationDirectory stringByAppendingPathComponent:@"A"]; NSString *destinationFile2 = [destinationDirectory stringByAppendingPathComponent:@"A2"]; XCTAssertTrue([[self bigData1] writeToFile:sourceFile atomically:YES]); XCTAssertTrue([[self bigData2] writeToFile:destinationFile atomically:YES]); XCTAssertTrue([[self bigData2] writeToFile:destinationFile2 atomically:YES]); { // We only usually track executable files to decide if we want to apply file system compression over the entire bundle int lchmodResult = lchmod(destinationFile.fileSystemRepresentation, 0755); XCTAssertEqual(lchmodResult, 0); } XCTAssertFalse([self testDirectoryHashEqualityWithSource:sourceDirectory destination:destinationDirectory]); } afterDiffHandler:nil afterPatchHandler:^(NSFileManager *__unused fileManager, NSString *__unused sourceDirectory, NSString *destinationDirectory) { NSString *destinationFile = [destinationDirectory stringByAppendingPathComponent:@"A"]; NSString *destinationFile2 = [destinationDirectory stringByAppendingPathComponent:@"A2"]; // Both files should not have file system compression applied { struct stat statStruct = {0}; int result = lstat(destinationFile.fileSystemRepresentation, &statStruct); XCTAssertEqual(result, 0); if ((statStruct.st_flags & UF_COMPRESSED) != 0) { XCTFail(@"First destination file is compressed!"); } } { struct stat statStruct = {0}; int result = lstat(destinationFile2.fileSystemRepresentation, &statStruct); XCTAssertEqual(result, 0); if ((statStruct.st_flags & UF_COMPRESSED) != 0) { XCTFail(@"Second destination file is compressed!"); } } } testingVersion3Delta:YES testingVersion2Delta:NO]; } - (void)testFrameworkVersionChanged { [self createAndApplyPatchWithHandler:^(NSFileManager *fileManager, NSString *sourceDirectory, NSString *destinationDirectory) { NSString *sourceA = [sourceDirectory stringByAppendingPathComponent:@"Frameworks/Foo.framework/Versions/A/"]; NSString *destinationB = [destinationDirectory stringByAppendingPathComponent:@"Frameworks/Foo.framework/Versions/B/"]; XCTAssertTrue([fileManager createDirectoryAtPath:sourceA withIntermediateDirectories:YES attributes:NULL error:NULL]); XCTAssertTrue([fileManager createDirectoryAtPath:destinationB withIntermediateDirectories:YES attributes:NULL error:NULL]); NSString *sourceFile = [sourceA stringByAppendingPathComponent:@"A"]; NSString *destinationFile = [destinationB stringByAppendingPathComponent:@"A"]; XCTAssertTrue([[self bigData1] writeToFile:sourceFile atomically:YES]); XCTAssertTrue([[self bigData2] writeToFile:destinationFile atomically:YES]); XCTAssertFalse([self testDirectoryHashEqualityWithSource:sourceDirectory destination:destinationDirectory]); }]; } - (void)testFrameworkExecutableVersionChanged { [self createAndApplyPatchWithHandler:^(NSFileManager *fileManager, NSString *sourceDirectory, NSString *destinationDirectory) { NSString *sourceA = [sourceDirectory stringByAppendingPathComponent:@"Frameworks/Foo.framework/Versions/A/"]; NSString *destinationB = [destinationDirectory stringByAppendingPathComponent:@"Frameworks/Foo.framework/Versions/B/"]; XCTAssertTrue([fileManager createDirectoryAtPath:sourceA withIntermediateDirectories:YES attributes:NULL error:NULL]); XCTAssertTrue([fileManager createDirectoryAtPath:destinationB withIntermediateDirectories:YES attributes:NULL error:NULL]); NSString *sourceFile = [sourceA stringByAppendingPathComponent:@"A"]; NSString *destinationFile = [destinationB stringByAppendingPathComponent:@"A"]; XCTAssertTrue([[self bigData1] writeToFile:sourceFile atomically:YES]); XCTAssertTrue([[self bigData2] writeToFile:destinationFile atomically:YES]); NSError *error = nil; if (![fileManager setAttributes:@{NSFilePosixPermissions : @0755} ofItemAtPath:sourceFile error:&error]) { NSLog(@"Change Permission Error: %@", error); XCTFail(@"Failed setting file permissions"); } if (![fileManager setAttributes:@{NSFilePosixPermissions : @0755} ofItemAtPath:destinationFile error:&error]) { NSLog(@"Change Permission Error: %@", error); XCTFail(@"Failed setting file permissions"); } XCTAssertFalse([self testDirectoryHashEqualityWithSource:sourceDirectory destination:destinationDirectory]); }]; } - (void)testFrameworkVersionChangedWithPermissionChange { [self createAndApplyPatchWithHandler:^(NSFileManager *fileManager, NSString *sourceDirectory, NSString *destinationDirectory) { NSString *sourceA = [sourceDirectory stringByAppendingPathComponent:@"Frameworks/Foo.framework/Versions/A/"]; NSString *destinationB = [destinationDirectory stringByAppendingPathComponent:@"Frameworks/Foo.framework/Versions/B/"]; XCTAssertTrue([fileManager createDirectoryAtPath:sourceA withIntermediateDirectories:YES attributes:NULL error:NULL]); XCTAssertTrue([fileManager createDirectoryAtPath:destinationB withIntermediateDirectories:YES attributes:NULL error:NULL]); NSString *sourceFile = [sourceA stringByAppendingPathComponent:@"A"]; NSString *destinationFile = [destinationB stringByAppendingPathComponent:@"A"]; XCTAssertTrue([[self bigData1] writeToFile:sourceFile atomically:YES]); XCTAssertTrue([[self bigData2] writeToFile:destinationFile atomically:YES]); NSError *error = nil; if (![fileManager setAttributes:@{NSFilePosixPermissions : @0755} ofItemAtPath:destinationFile error:&error]) { NSLog(@"Change Permission Error: %@", error); XCTFail(@"Failed setting file permissions"); } XCTAssertFalse([self testDirectoryHashEqualityWithSource:sourceDirectory destination:destinationDirectory]); }]; } - (void)testDirectoryPermissionChangeWithContentChange { [self createAndApplyPatchWithHandler:^(NSFileManager *fileManager, NSString *sourceDirectory, NSString *destinationDirectory) { NSString *sourceFile1 = [sourceDirectory stringByAppendingPathComponent:@"A"]; NSString *sourceFile2 = [sourceFile1 stringByAppendingPathComponent:@"B"]; NSString *destinationFile1 = [destinationDirectory stringByAppendingPathComponent:@"A"]; NSString *destinationFile2 = [destinationFile1 stringByAppendingPathComponent:@"B"]; NSError *error = nil; if (![fileManager createDirectoryAtPath:sourceFile1 withIntermediateDirectories:NO attributes:nil error:&error]) { NSLog(@"Failed creating directory with error: %@", error); XCTFail("Failed to create directory"); } if (![fileManager createDirectoryAtPath:destinationFile1 withIntermediateDirectories:NO attributes:nil error:&error]) { NSLog(@"Failed creating directory with error: %@", error); XCTFail("Failed to create directory"); } XCTAssertTrue([[self bigData1] writeToFile:sourceFile2 atomically:YES]); XCTAssertTrue([[self bigData1] writeToFile:destinationFile2 atomically:YES]); if (![fileManager setAttributes:@{NSFilePosixPermissions : @0766} ofItemAtPath:sourceFile1 error:&error]) { NSLog(@"Change Permission Error: %@", error); XCTFail(@"Failed setting file permissions"); } if (![fileManager setAttributes:@{NSFilePosixPermissions : @0755} ofItemAtPath:destinationFile1 error:&error]) { NSLog(@"Change Permission Error: %@", error); XCTFail(@"Failed setting file permissions"); } XCTAssertFalse([self testDirectoryHashEqualityWithSource:sourceDirectory destination:destinationDirectory]); }]; } - (void)testDirectoryChangeWithExecutableContentChange { [self createAndApplyPatchWithHandler:^(NSFileManager *fileManager, NSString *sourceDirectory, NSString *destinationDirectory) { NSString *sourceFile1 = [sourceDirectory stringByAppendingPathComponent:@"A"]; NSString *sourceFile2 = [sourceFile1 stringByAppendingPathComponent:@"B"]; NSString *destinationFile1 = [destinationDirectory stringByAppendingPathComponent:@"A"]; NSString *destinationFile2 = [destinationFile1 stringByAppendingPathComponent:@"B"]; NSError *error = nil; if (![fileManager createDirectoryAtPath:sourceFile1 withIntermediateDirectories:NO attributes:nil error:&error]) { NSLog(@"Failed creating directory with error: %@", error); XCTFail("Failed to create directory"); } if (![fileManager createDirectoryAtPath:destinationFile1 withIntermediateDirectories:NO attributes:nil error:&error]) { NSLog(@"Failed creating directory with error: %@", error); XCTFail("Failed to create directory"); } XCTAssertTrue([[self bigData1] writeToFile:sourceFile2 atomically:YES]); XCTAssertTrue([[self bigData1] writeToFile:destinationFile2 atomically:YES]); if (![fileManager setAttributes:@{NSFilePosixPermissions : @0755} ofItemAtPath:sourceFile1 error:&error]) { NSLog(@"Change Permission Error: %@", error); XCTFail(@"Failed setting file permissions"); } if (![fileManager setAttributes:@{NSFilePosixPermissions : @0755} ofItemAtPath:destinationFile1 error:&error]) { NSLog(@"Change Permission Error: %@", error); XCTFail(@"Failed setting file permissions"); } XCTAssertTrue([self testDirectoryHashEqualityWithSource:sourceDirectory destination:destinationDirectory]); }]; } - (void)testFrameworkVersionChangedWithDirectoryToFileChange { [self createAndApplyPatchWithHandler:^(NSFileManager *fileManager, NSString *sourceDirectory, NSString *destinationDirectory) { NSString *sourceA = [sourceDirectory stringByAppendingPathComponent:@"Frameworks/Foo.framework/Versions/A/"]; NSString *destinationB = [destinationDirectory stringByAppendingPathComponent:@"Frameworks/Foo.framework/Versions/B/"]; XCTAssertTrue([fileManager createDirectoryAtPath:sourceA withIntermediateDirectories:YES attributes:NULL error:NULL]); XCTAssertTrue([fileManager createDirectoryAtPath:destinationB withIntermediateDirectories:YES attributes:NULL error:NULL]); NSString *sourceFileDirectory = [sourceA stringByAppendingPathComponent:@"A"]; NSString *sourceFileInDirectory = [sourceFileDirectory stringByAppendingPathComponent:@"B"]; NSString *destinationFile = [destinationB stringByAppendingPathComponent:@"A"]; XCTAssertTrue([fileManager createDirectoryAtPath:sourceFileDirectory withIntermediateDirectories:YES attributes:NULL error:NULL]); XCTAssertTrue([[self bigData1] writeToFile:sourceFileInDirectory atomically:YES]); XCTAssertTrue([[self bigData2] writeToFile:destinationFile atomically:YES]); XCTAssertFalse([self testDirectoryHashEqualityWithSource:sourceDirectory destination:destinationDirectory]); }]; } - (void)testFrameworkVersionChangedWithFileToDirectoryChange { [self createAndApplyPatchWithHandler:^(NSFileManager *fileManager, NSString *sourceDirectory, NSString *destinationDirectory) { NSString *sourceA = [sourceDirectory stringByAppendingPathComponent:@"Frameworks/Foo.framework/Versions/A/"]; NSString *destinationB = [destinationDirectory stringByAppendingPathComponent:@"Frameworks/Foo.framework/Versions/B/"]; XCTAssertTrue([fileManager createDirectoryAtPath:sourceA withIntermediateDirectories:YES attributes:NULL error:NULL]); XCTAssertTrue([fileManager createDirectoryAtPath:destinationB withIntermediateDirectories:YES attributes:NULL error:NULL]); NSString *sourceFile = [sourceA stringByAppendingPathComponent:@"A"]; NSString *destinationFileDirectory = [destinationB stringByAppendingPathComponent:@"A"]; NSString *destinationFileInDirectory = [destinationFileDirectory stringByAppendingPathComponent:@"B"]; XCTAssertTrue([[self bigData2] writeToFile:sourceFile atomically:YES]); XCTAssertTrue([fileManager createDirectoryAtPath:destinationFileDirectory withIntermediateDirectories:YES attributes:NULL error:NULL]); XCTAssertTrue([[self bigData1] writeToFile:destinationFileInDirectory atomically:YES]); XCTAssertFalse([self testDirectoryHashEqualityWithSource:sourceDirectory destination:destinationDirectory]); }]; } - (void)testFrameworkVersionChangedWithSymbolicLinkToFileChange { [self createAndApplyPatchWithHandler:^(NSFileManager *fileManager, NSString *sourceDirectory, NSString *destinationDirectory) { NSString *sourceA = [sourceDirectory stringByAppendingPathComponent:@"Frameworks/Foo.framework/Versions/A/"]; NSString *destinationB = [destinationDirectory stringByAppendingPathComponent:@"Frameworks/Foo.framework/Versions/B/"]; XCTAssertTrue([fileManager createDirectoryAtPath:sourceA withIntermediateDirectories:YES attributes:NULL error:NULL]); XCTAssertTrue([fileManager createDirectoryAtPath:destinationB withIntermediateDirectories:YES attributes:NULL error:NULL]); NSString *sourceFile = [sourceA stringByAppendingPathComponent:@"A"]; NSString *destinationFile = [destinationB stringByAppendingPathComponent:@"A"]; NSError *error = nil; if (![fileManager createSymbolicLinkAtPath:sourceFile withDestinationPath:@"C" error:&error]) { NSLog(@"Error in creating symlink: %@", error); XCTFail(@"Failed to create symlink"); } XCTAssertTrue([[self bigData2] writeToFile:destinationFile atomically:YES]); XCTAssertFalse([self testDirectoryHashEqualityWithSource:sourceDirectory destination:destinationDirectory]); }]; } - (void)testFrameworkVersionChangedWithFileToSymbolicLinkChange { [self createAndApplyPatchWithHandler:^(NSFileManager *fileManager, NSString *sourceDirectory, NSString *destinationDirectory) { NSString *sourceA = [sourceDirectory stringByAppendingPathComponent:@"Frameworks/Foo.framework/Versions/A/"]; NSString *destinationB = [destinationDirectory stringByAppendingPathComponent:@"Frameworks/Foo.framework/Versions/B/"]; XCTAssertTrue([fileManager createDirectoryAtPath:sourceA withIntermediateDirectories:YES attributes:NULL error:NULL]); XCTAssertTrue([fileManager createDirectoryAtPath:destinationB withIntermediateDirectories:YES attributes:NULL error:NULL]); NSString *sourceFile = [sourceA stringByAppendingPathComponent:@"A"]; NSString *destinationFile = [destinationB stringByAppendingPathComponent:@"A"]; XCTAssertTrue([[self bigData2] writeToFile:sourceFile atomically:YES]); NSError *error = nil; if (![fileManager createSymbolicLinkAtPath:destinationFile withDestinationPath:@"C" error:&error]) { NSLog(@"Error in creating symlink: %@", error); XCTFail(@"Failed to create symlink"); } XCTAssertFalse([self testDirectoryHashEqualityWithSource:sourceDirectory destination:destinationDirectory]); }]; } - (void)testOddPermissionsInAfterTree { BOOL success = [self createAndApplyPatchWithBeforeDiffHandler:^(NSFileManager *fileManager, NSString *sourceDirectory, NSString *destinationDirectory) { NSString *sourceFile = [sourceDirectory stringByAppendingPathComponent:@"A"]; NSString *destinationFile = [destinationDirectory stringByAppendingPathComponent:@"A"]; NSData *data = [NSData dataWithBytes:"loltest" length:7]; XCTAssertTrue([data writeToFile:sourceFile atomically:YES]); XCTAssertTrue([data writeToFile:destinationFile atomically:YES]); NSError *error = nil; if (![fileManager setAttributes:@{NSFilePosixPermissions : @0777} ofItemAtPath:destinationFile error:&error]) { NSLog(@"Change Permission Error: %@", error); XCTFail(@"Failed setting file permissions"); } XCTAssertFalse([self testDirectoryHashEqualityWithSource:sourceDirectory destination:destinationDirectory]); } afterDiffHandler:nil afterPatchHandler:nil]; XCTAssertTrue(success); } - (void)testOddChangingPermissionsWithBigFilesInBothTrees { BOOL success = [self createAndApplyPatchWithBeforeDiffHandler:^(NSFileManager *fileManager, NSString *sourceDirectory, NSString *destinationDirectory) { NSString *sourceFile = [sourceDirectory stringByAppendingPathComponent:@"A"]; NSString *destinationFile = [destinationDirectory stringByAppendingPathComponent:@"A"]; XCTAssertTrue([[self bigData1] writeToFile:sourceFile atomically:YES]); XCTAssertTrue([[self bigData2] writeToFile:destinationFile atomically:YES]); NSError *error = nil; if (![fileManager setAttributes:@{NSFilePosixPermissions : @0777} ofItemAtPath:sourceFile error:&error]) { NSLog(@"Change Permission Error: %@", error); XCTFail(@"Failed setting file permissions"); } if (![fileManager setAttributes:@{NSFilePosixPermissions : @0774} ofItemAtPath:destinationFile error:&error]) { NSLog(@"Change Permission Error: %@", error); XCTFail(@"Failed setting file permissions"); } XCTAssertFalse([self testDirectoryHashEqualityWithSource:sourceDirectory destination:destinationDirectory]); } afterDiffHandler:nil afterPatchHandler:nil]; XCTAssertTrue(success); } - (void)testOddPermissionsWithBigFilesInBothTrees { BOOL success = [self createAndApplyPatchWithBeforeDiffHandler:^(NSFileManager *fileManager, NSString *sourceDirectory, NSString *destinationDirectory) { NSString *sourceFile = [sourceDirectory stringByAppendingPathComponent:@"A"]; NSString *destinationFile = [destinationDirectory stringByAppendingPathComponent:@"A"]; XCTAssertTrue([[self bigData1] writeToFile:sourceFile atomically:YES]); XCTAssertTrue([[self bigData2] writeToFile:destinationFile atomically:YES]); NSError *error = nil; if (![fileManager setAttributes:@{NSFilePosixPermissions : @0777} ofItemAtPath:sourceFile error:&error]) { NSLog(@"Change Permission Error: %@", error); XCTFail(@"Failed setting file permissions"); } if (![fileManager setAttributes:@{NSFilePosixPermissions : @0777} ofItemAtPath:destinationFile error:&error]) { NSLog(@"Change Permission Error: %@", error); XCTFail(@"Failed setting file permissions"); } XCTAssertFalse([self testDirectoryHashEqualityWithSource:sourceDirectory destination:destinationDirectory]); } afterDiffHandler:nil afterPatchHandler:nil]; XCTAssertTrue(success); } - (void)testBadPermissionsInBeforeTree { [self createAndApplyPatchWithHandler:^(NSFileManager *fileManager, NSString *sourceDirectory, NSString *destinationDirectory) { NSString *sourceFile = [sourceDirectory stringByAppendingPathComponent:@"A"]; NSString *destinationFile = [destinationDirectory stringByAppendingPathComponent:@"A"]; NSData *data = [NSData dataWithBytes:"loltest" length:7]; XCTAssertTrue([data writeToFile:sourceFile atomically:YES]); XCTAssertTrue([data writeToFile:destinationFile atomically:YES]); NSError *error = nil; if (![fileManager setAttributes:@{NSFilePosixPermissions : @0777} ofItemAtPath:sourceFile error:&error]) { NSLog(@"Change Permission Error: %@", error); XCTFail(@"Failed setting file permissions"); } XCTAssertFalse([self testDirectoryHashEqualityWithSource:sourceDirectory destination:destinationDirectory]); }]; } - (void)testInvalidRegularFileWithACLInBeforeTree { BOOL success = [self createAndApplyPatchWithBeforeDiffHandler:^(NSFileManager *__unused fileManager, NSString *sourceDirectory, NSString * __unused destinationDirectory) { NSString *sourceFile = [sourceDirectory stringByAppendingPathComponent:@"A"]; XCTAssertTrue([[NSData dataWithBytes:"loltest" length:7] writeToFile:sourceFile atomically:YES]); acl_t acl = acl_init(1); acl_entry_t entry; XCTAssertEqual(0, acl_create_entry(&acl, &entry)); acl_permset_t permset; XCTAssertEqual(0, acl_get_permset(entry, &permset)); XCTAssertEqual(0, acl_add_perm(permset, ACL_SEARCH)); XCTAssertEqual(0, acl_set_link_np([sourceFile fileSystemRepresentation], ACL_TYPE_EXTENDED, acl)); XCTAssertEqual(0, acl_free(acl)); } afterDiffHandler:nil afterPatchHandler:nil]; XCTAssertFalse(success); } - (void)testInvalidRegularFileWithACLInAfterTree { BOOL success = [self createAndApplyPatchWithBeforeDiffHandler:^(NSFileManager *__unused fileManager, NSString *__unused sourceDirectory, NSString *destinationDirectory) { NSString *destinationFile = [destinationDirectory stringByAppendingPathComponent:@"A"]; XCTAssertTrue([[NSData dataWithBytes:"loltest" length:7] writeToFile:destinationFile atomically:YES]); acl_t acl = acl_init(1); acl_entry_t entry; XCTAssertEqual(0, acl_create_entry(&acl, &entry)); acl_permset_t permset; XCTAssertEqual(0, acl_get_permset(entry, &permset)); XCTAssertEqual(0, acl_add_perm(permset, ACL_SEARCH)); XCTAssertEqual(0, acl_set_link_np([destinationFile fileSystemRepresentation], ACL_TYPE_EXTENDED, acl)); XCTAssertEqual(0, acl_free(acl)); } afterDiffHandler:nil afterPatchHandler:nil]; XCTAssertFalse(success); } - (void)testInvalidDirectoryWithACLInBeforeTree { BOOL success = [self createAndApplyPatchWithBeforeDiffHandler:^(NSFileManager *fileManager, NSString *sourceDirectory, NSString * __unused destinationDirectory) { NSString *sourceFile = [sourceDirectory stringByAppendingPathComponent:@"A"]; XCTAssertTrue([fileManager createDirectoryAtPath:sourceFile withIntermediateDirectories:NO attributes:nil error:nil]); acl_t acl = acl_init(1); acl_entry_t entry; XCTAssertEqual(0, acl_create_entry(&acl, &entry)); acl_permset_t permset; XCTAssertEqual(0, acl_get_permset(entry, &permset)); XCTAssertEqual(0, acl_add_perm(permset, ACL_SEARCH)); XCTAssertEqual(0, acl_set_link_np([sourceFile fileSystemRepresentation], ACL_TYPE_EXTENDED, acl)); XCTAssertEqual(0, acl_free(acl)); } afterDiffHandler:nil afterPatchHandler:nil]; XCTAssertFalse(success); } - (void)testInvalidDirectoryWithACLInAfterTree { BOOL success = [self createAndApplyPatchWithBeforeDiffHandler:^(NSFileManager *fileManager, NSString * __unused sourceDirectory, NSString *destinationDirectory) { NSString *destinationFile = [destinationDirectory stringByAppendingPathComponent:@"A"]; XCTAssertTrue([fileManager createDirectoryAtPath:destinationFile withIntermediateDirectories:NO attributes:nil error:nil]); acl_t acl = acl_init(1); acl_entry_t entry; XCTAssertEqual(0, acl_create_entry(&acl, &entry)); acl_permset_t permset; XCTAssertEqual(0, acl_get_permset(entry, &permset)); XCTAssertEqual(0, acl_add_perm(permset, ACL_SEARCH)); XCTAssertEqual(0, acl_set_link_np([destinationFile fileSystemRepresentation], ACL_TYPE_EXTENDED, acl)); XCTAssertEqual(0, acl_free(acl)); } afterDiffHandler:nil afterPatchHandler:nil]; XCTAssertFalse(success); } - (void)testRegularFileToSymlinkChange { [self createAndApplyPatchWithHandler:^(NSFileManager *fileManager, NSString *sourceDirectory, NSString *destinationDirectory) { NSString *sourceFile1 = [sourceDirectory stringByAppendingPathComponent:@"A"]; NSString *sourceFile2 = [sourceDirectory stringByAppendingPathComponent:@"B"]; NSString *destinationFile1 = [destinationDirectory stringByAppendingPathComponent:@"A"]; NSString *destinationFile2 = [destinationDirectory stringByAppendingPathComponent:@"B"]; NSData *data = [NSData dataWithBytes:"A" length:1]; XCTAssertTrue([data writeToFile:sourceFile1 atomically:YES]); XCTAssertTrue([data writeToFile:sourceFile2 atomically:YES]); XCTAssertTrue([data writeToFile:destinationFile1 atomically:YES]); NSError *error = nil; if (![fileManager createSymbolicLinkAtPath:destinationFile2 withDestinationPath:@"A" error:&error]) { NSLog(@"Error in creating symlink: %@", error); XCTFail(@"Failed to create symlink"); } // This would fail with version 1.0 XCTAssertFalse([self testDirectoryHashEqualityWithSource:sourceDirectory destination:destinationDirectory]); }]; } - (void)testLargerRegularFileToSymlinkChange { [self createAndApplyPatchWithHandler:^(NSFileManager *fileManager, NSString *sourceDirectory, NSString *destinationDirectory) { NSString *sourceFile1 = [sourceDirectory stringByAppendingPathComponent:@"A"]; NSString *sourceFile2 = [sourceDirectory stringByAppendingPathComponent:@"B"]; NSString *destinationFile1 = [destinationDirectory stringByAppendingPathComponent:@"A"]; NSString *destinationFile2 = [destinationDirectory stringByAppendingPathComponent:@"B"]; NSData *data = [NSData dataWithBytes:"loltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltest" length:420]; XCTAssertTrue([data writeToFile:sourceFile1 atomically:YES]); XCTAssertTrue([data writeToFile:sourceFile2 atomically:YES]); XCTAssertTrue([data writeToFile:destinationFile1 atomically:YES]); NSString *destination = @"loltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltest"; NSError *error = nil; if (![fileManager createSymbolicLinkAtPath:destinationFile2 withDestinationPath:destination error:&error]) { NSLog(@"Error in creating symlink: %@", error); XCTFail(@"Failed to create symlink"); } // This would fail with version 1.0 XCTAssertFalse([self testDirectoryHashEqualityWithSource:sourceDirectory destination:destinationDirectory]); }]; } - (void)testSymlinkToRegularFileChange { [self createAndApplyPatchWithHandler:^(NSFileManager *fileManager, NSString *sourceDirectory, NSString *destinationDirectory) { NSString *sourceFile1 = [sourceDirectory stringByAppendingPathComponent:@"A"]; NSString *sourceFile2 = [sourceDirectory stringByAppendingPathComponent:@"B"]; NSString *destinationFile1 = [destinationDirectory stringByAppendingPathComponent:@"A"]; NSString *destinationFile2 = [destinationDirectory stringByAppendingPathComponent:@"B"]; NSData *data = [NSData dataWithBytes:"loltes" length:6]; XCTAssertTrue([data writeToFile:sourceFile1 atomically:YES]); XCTAssertTrue([data writeToFile:destinationFile1 atomically:YES]); XCTAssertTrue([data writeToFile:destinationFile2 atomically:YES]); NSError *error = nil; if (![fileManager createSymbolicLinkAtPath:sourceFile2 withDestinationPath:@"A" error:&error]) { NSLog(@"Error in creating symlink: %@", error); XCTFail(@"Failed to create symlink"); } XCTAssertFalse([self testDirectoryHashEqualityWithSource:sourceDirectory destination:destinationDirectory]); }]; } - (void)testLargerSymlinkToRegularFileChange { [self createAndApplyPatchWithHandler:^(NSFileManager *fileManager, NSString *sourceDirectory, NSString *destinationDirectory) { NSString *sourceFile1 = [sourceDirectory stringByAppendingPathComponent:@"A"]; NSString *sourceFile2 = [sourceDirectory stringByAppendingPathComponent:@"B"]; NSString *destinationFile1 = [destinationDirectory stringByAppendingPathComponent:@"A"]; NSString *destinationFile2 = [destinationDirectory stringByAppendingPathComponent:@"B"]; NSData *data = [NSData dataWithBytes:"loltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltest" length:420]; XCTAssertTrue([data writeToFile:sourceFile1 atomically:YES]); XCTAssertTrue([data writeToFile:destinationFile1 atomically:YES]); XCTAssertTrue([data writeToFile:destinationFile2 atomically:YES]); NSString *destination = @"loltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltestloltest"; NSError *error = nil; if (![fileManager createSymbolicLinkAtPath:sourceFile2 withDestinationPath:destination error:&error]) { NSLog(@"Error in creating symlink: %@", error); XCTFail(@"Failed to create symlink"); } XCTAssertFalse([self testDirectoryHashEqualityWithSource:sourceDirectory destination:destinationDirectory]); }]; } - (void)testRegularFileToDirectoryChange { [self createAndApplyPatchWithHandler:^(NSFileManager *fileManager, NSString *sourceDirectory, NSString *destinationDirectory) { NSString *sourceFile = [sourceDirectory stringByAppendingPathComponent:@"A"]; NSString *destinationFile = [destinationDirectory stringByAppendingPathComponent:@"A"]; NSData *data = [NSData dataWithBytes:"loltes" length:6]; XCTAssertTrue([data writeToFile:sourceFile atomically:YES]); NSError *error = nil; if (![fileManager createDirectoryAtPath:destinationFile withIntermediateDirectories:NO attributes:nil error:&error]) { NSLog(@"Failed creating directory with error: %@", error); XCTFail("Failed to create directory"); } XCTAssertFalse([self testDirectoryHashEqualityWithSource:sourceDirectory destination:destinationDirectory]); }]; } - (void)testDirectoryToRegularFileChange { [self createAndApplyPatchWithHandler:^(NSFileManager *fileManager, NSString *sourceDirectory, NSString *destinationDirectory) { NSString *sourceFile = [sourceDirectory stringByAppendingPathComponent:@"A"]; NSString *destinationFile = [destinationDirectory stringByAppendingPathComponent:@"A"]; NSError *error = nil; if (![fileManager createDirectoryAtPath:sourceFile withIntermediateDirectories:NO attributes:nil error:&error]) { NSLog(@"Failed creating directory with error: %@", error); XCTFail("Failed to create directory"); } NSData *data = [NSData dataWithBytes:"loltest" length:7]; XCTAssertTrue([data writeToFile:destinationFile atomically:YES]); XCTAssertFalse([self testDirectoryHashEqualityWithSource:sourceDirectory destination:destinationDirectory]); }]; } // See issue #514 for more info - (void)testDirectoryToSymlinkChange { [self createAndApplyPatchWithHandler:^(NSFileManager *fileManager, NSString *sourceDirectory, NSString *destinationDirectory) { NSString *sourceFile1 = [sourceDirectory stringByAppendingPathComponent:@"A"]; NSString *sourceFile2 = [sourceDirectory stringByAppendingPathComponent:@"Current"]; NSString *sourceFile3 = [sourceFile2 stringByAppendingPathComponent:@"B"]; NSString *destinationFile1 = [destinationDirectory stringByAppendingPathComponent:@"A"]; NSString *destinationFile2 = [destinationFile1 stringByAppendingPathComponent:@"B"]; NSString *destinationFile3 = [destinationDirectory stringByAppendingPathComponent:@"Current"]; NSError *error = nil; if (![fileManager createDirectoryAtPath:sourceFile1 withIntermediateDirectories:NO attributes:nil error:&error]) { NSLog(@"Failed creating directory with error: %@", error); XCTFail("Failed to create directory A in source"); } if (![fileManager createDirectoryAtPath:sourceFile2 withIntermediateDirectories:NO attributes:nil error:&error]) { NSLog(@"Failed creating directory with error: %@", error); XCTFail("Failed to create directory Current in source"); } if (![fileManager createDirectoryAtPath:destinationFile1 withIntermediateDirectories:NO attributes:nil error:&error]) { NSLog(@"Failed creating directory with error: %@", error); XCTFail("Failed to create directory A in destination"); } if (![fileManager createSymbolicLinkAtPath:destinationFile3 withDestinationPath:@"A/" error:&error]) { NSLog(@"Error in creating symlink: %@", error); XCTFail(@"Failed to create symlink"); } XCTAssertTrue([[self bigData1] writeToFile:sourceFile3 atomically:YES]); XCTAssertTrue([[self bigData1] writeToFile:destinationFile2 atomically:YES]); XCTAssertFalse([self testDirectoryHashEqualityWithSource:sourceDirectory destination:destinationDirectory]); }]; } // Opposite of the test method testDirectoryToSymlinkChange - (void)testSymlinkToDirectoryChange { [self createAndApplyPatchWithHandler:^(NSFileManager *fileManager, NSString *sourceDirectory, NSString *destinationDirectory) { NSString *sourceFile1 = [sourceDirectory stringByAppendingPathComponent:@"A"]; NSString *sourceFile2 = [sourceFile1 stringByAppendingPathComponent:@"B"]; NSString *sourceFile3 = [sourceDirectory stringByAppendingPathComponent:@"Current"]; NSString *destinationFile1 = [destinationDirectory stringByAppendingPathComponent:@"A"]; NSString *destinationFile2 = [destinationDirectory stringByAppendingPathComponent:@"Current"]; NSString *destinationFile3 = [destinationFile2 stringByAppendingPathComponent:@"B"]; NSError *error = nil; if (![fileManager createDirectoryAtPath:sourceFile1 withIntermediateDirectories:NO attributes:nil error:&error]) { NSLog(@"Failed creating directory with error: %@", error); XCTFail("Failed to create directory A in source"); } if (![fileManager createDirectoryAtPath:destinationFile1 withIntermediateDirectories:NO attributes:nil error:&error]) { NSLog(@"Failed creating directory with error: %@", error); XCTFail("Failed to create directory A in destination"); } if (![fileManager createDirectoryAtPath:destinationFile2 withIntermediateDirectories:NO attributes:nil error:&error]) { NSLog(@"Failed creating directory with error: %@", error); XCTFail("Failed to create directory Current in destination"); } if (![fileManager createSymbolicLinkAtPath:sourceFile3 withDestinationPath:@"A/" error:&error]) { NSLog(@"Error in creating symlink: %@", error); XCTFail(@"Failed to create symlink"); } XCTAssertTrue([[self bigData1] writeToFile:sourceFile2 atomically:YES]); XCTAssertTrue([[self bigData1] writeToFile:destinationFile3 atomically:YES]); XCTAssertFalse([self testDirectoryHashEqualityWithSource:sourceDirectory destination:destinationDirectory]); }]; } - (void)testInvalidCodeSignatureExtendedAttributeInBeforeTree { BOOL success = [self createAndApplyPatchWithBeforeDiffHandler:^(NSFileManager *__unused fileManager, NSString *sourceDirectory, NSString *destinationDirectory) { NSString *sourceFile = [sourceDirectory stringByAppendingPathComponent:@"A"]; NSString *destinationFile = [destinationDirectory stringByAppendingPathComponent:@"A"]; XCTAssertTrue([[NSData data] writeToFile:sourceFile atomically:YES]); XCTAssertTrue([[NSData dataWithBytes:"loltest" length:7] writeToFile:destinationFile atomically:YES]); // the actual data doesn't matter for testing purposes const char xattrValue[] = "hello"; XCTAssertEqual(0, setxattr([sourceFile fileSystemRepresentation], APPLE_CODE_SIGN_XATTR_CODE_DIRECTORY_KEY, xattrValue, sizeof(xattrValue), 0, XATTR_CREATE)); XCTAssertEqual(0, setxattr([sourceFile fileSystemRepresentation], APPLE_CODE_SIGN_XATTR_CODE_REQUIREMENTS_KEY, xattrValue, sizeof(xattrValue), 0, XATTR_CREATE)); XCTAssertEqual(0, setxattr([sourceFile fileSystemRepresentation], APPLE_CODE_SIGN_XATTR_CODE_SIGNATURE_KEY, xattrValue, sizeof(xattrValue), 0, XATTR_CREATE)); } afterDiffHandler:nil afterPatchHandler:nil]; XCTAssertFalse(success); } - (void)testInvalidCodeSignatureExtendedAttributeInAfterTree { BOOL success = [self createAndApplyPatchWithBeforeDiffHandler:^(NSFileManager *__unused fileManager, NSString *sourceDirectory, NSString *destinationDirectory) { NSString *sourceFile = [sourceDirectory stringByAppendingPathComponent:@"A"]; NSString *destinationFile = [destinationDirectory stringByAppendingPathComponent:@"A"]; XCTAssertTrue([[NSData data] writeToFile:sourceFile atomically:YES]); XCTAssertTrue([[NSData dataWithBytes:"loltest" length:7] writeToFile:destinationFile atomically:YES]); // the actual data doesn't matter for testing purposes const char xattrValue[] = "hello"; XCTAssertEqual(0, setxattr([destinationFile fileSystemRepresentation], APPLE_CODE_SIGN_XATTR_CODE_DIRECTORY_KEY, xattrValue, sizeof(xattrValue), 0, XATTR_CREATE)); XCTAssertEqual(0, setxattr([destinationFile fileSystemRepresentation], APPLE_CODE_SIGN_XATTR_CODE_REQUIREMENTS_KEY, xattrValue, sizeof(xattrValue), 0, XATTR_CREATE)); XCTAssertEqual(0, setxattr([destinationFile fileSystemRepresentation], APPLE_CODE_SIGN_XATTR_CODE_SIGNATURE_KEY, xattrValue, sizeof(xattrValue), 0, XATTR_CREATE)); } afterDiffHandler:nil afterPatchHandler:nil]; XCTAssertFalse(success); } - (void)testCreatingPatchWithCustomIconInBeforeTree { BOOL success = [self createAndApplyPatchWithBeforeDiffHandler:^(NSFileManager *__unused fileManager, NSString *sourceDirectory, NSString *destinationDirectory) { NSString *sourceFile = [sourceDirectory stringByAppendingPathComponent:@"A"]; NSString *destinationFile = [destinationDirectory stringByAppendingPathComponent:@"A"]; XCTAssertTrue([[NSData data] writeToFile:sourceFile atomically:YES]); XCTAssertTrue([[NSData dataWithBytes:"loltest" length:7] writeToFile:destinationFile atomically:YES]); NSImage *iconImage = [NSImage imageNamed:NSImageNameAdvanced]; XCTAssertNotNil(iconImage); BOOL setIcon = [[NSWorkspace sharedWorkspace] setIcon:iconImage forFile:sourceDirectory options:(NSWorkspaceIconCreationOptions)0]; XCTAssertTrue(setIcon); } afterDiffHandler:nil afterPatchHandler:nil]; XCTAssertFalse(success); } - (void)testCreatingPatchWithCustomIconInAfterTree { BOOL success = [self createAndApplyPatchWithBeforeDiffHandler:^(NSFileManager *__unused fileManager, NSString *sourceDirectory, NSString *destinationDirectory) { NSString *sourceFile = [sourceDirectory stringByAppendingPathComponent:@"A"]; NSString *destinationFile = [destinationDirectory stringByAppendingPathComponent:@"A"]; XCTAssertTrue([[NSData data] writeToFile:sourceFile atomically:YES]); XCTAssertTrue([[NSData dataWithBytes:"loltest" length:7] writeToFile:destinationFile atomically:YES]); NSImage *iconImage = [NSImage imageNamed:NSImageNameAdvanced]; XCTAssertNotNil(iconImage); BOOL setIcon = [[NSWorkspace sharedWorkspace] setIcon:iconImage forFile:destinationDirectory options:(NSWorkspaceIconCreationOptions)0]; XCTAssertTrue(setIcon); } afterDiffHandler:nil afterPatchHandler:nil]; XCTAssertFalse(success); } - (void)testApplyingPatchAfterSettingCustomIcon { BOOL success = [self createAndApplyPatchWithBeforeDiffHandler:^(NSFileManager *__unused fileManager, NSString *sourceDirectory, NSString *destinationDirectory) { NSString *sourceFile = [sourceDirectory stringByAppendingPathComponent:@"A"]; NSString *destinationFile = [destinationDirectory stringByAppendingPathComponent:@"A"]; XCTAssertTrue([[NSData data] writeToFile:sourceFile atomically:YES]); XCTAssertTrue([[NSData dataWithBytes:"loltest" length:7] writeToFile:destinationFile atomically:YES]); } afterDiffHandler:^(NSFileManager *__unused fileManager, NSString *sourceDirectory, NSString *__unused destinationDirectory) { NSImage *iconImage = [NSImage imageNamed:NSImageNameAdvanced]; XCTAssertNotNil(iconImage); BOOL setIcon = [[NSWorkspace sharedWorkspace] setIcon:iconImage forFile:sourceDirectory options:(NSWorkspaceIconCreationOptions)0]; XCTAssertTrue(setIcon); } afterPatchHandler:nil]; XCTAssertTrue(success); } - (void)testRegularSparkleFrameworkPresence { [self createAndApplyPatchWithHandler:^(NSFileManager *fileManager, NSString *sourceDirectory, NSString *destinationDirectory) { NSString *sourceA = [sourceDirectory stringByAppendingPathComponent:@"Frameworks/Sparkle.framework/Versions/A/"]; NSString *destinationB = [destinationDirectory stringByAppendingPathComponent:@"Frameworks/Sparkle.framework/Versions/B/"]; XCTAssertTrue([fileManager createDirectoryAtPath:sourceA withIntermediateDirectories:YES attributes:NULL error:NULL]); XCTAssertTrue([fileManager createDirectoryAtPath:destinationB withIntermediateDirectories:YES attributes:NULL error:NULL]); NSString *sourceFile = [sourceA stringByAppendingPathComponent:@"Sparkle"]; NSString *destinationFile = [destinationB stringByAppendingPathComponent:@"Sparkle"]; XCTAssertTrue([[self bigData1] writeToFile:sourceFile atomically:YES]); XCTAssertTrue([[self bigData2] writeToFile:destinationFile atomically:YES]); NSError *error = nil; if (![fileManager setAttributes:@{NSFilePosixPermissions : @0755} ofItemAtPath:sourceFile error:&error]) { NSLog(@"Change Permission Error: %@", error); XCTFail(@"Failed setting file permissions"); } if (![fileManager setAttributes:@{NSFilePosixPermissions : @0755} ofItemAtPath:destinationFile error:&error]) { NSLog(@"Change Permission Error: %@", error); XCTFail(@"Failed setting file permissions"); } }]; } - (void)testInvalidSparkleFrameworkInBeforeTree { BOOL success = [self createAndApplyPatchWithBeforeDiffHandler:^(NSFileManager *fileManager, NSString *sourceDirectory, NSString *destinationDirectory) { NSString *sourceA = [sourceDirectory stringByAppendingPathComponent:@"Frameworks/Sparkle.framework/Versions/A/"]; NSString *destinationB = [destinationDirectory stringByAppendingPathComponent:@"Frameworks/Sparkle.framework/Versions/B/"]; XCTAssertTrue([fileManager createDirectoryAtPath:sourceA withIntermediateDirectories:YES attributes:NULL error:NULL]); XCTAssertTrue([fileManager createDirectoryAtPath:destinationB withIntermediateDirectories:YES attributes:NULL error:NULL]); NSString *sourceFile = [sourceA stringByAppendingPathComponent:@"Sparkle"]; NSString *destinationFile = [destinationB stringByAppendingPathComponent:@"Sparkle"]; XCTAssertTrue([[self bigData1] writeToFile:sourceFile atomically:YES]); XCTAssertTrue([[self bigData2] writeToFile:destinationFile atomically:YES]); NSError *error = nil; // Use invalid permission mode 0777 if (![fileManager setAttributes:@{NSFilePosixPermissions : @0777} ofItemAtPath:sourceFile error:&error]) { NSLog(@"Change Permission Error: %@", error); XCTFail(@"Failed setting file permissions"); } if (![fileManager setAttributes:@{NSFilePosixPermissions : @0755} ofItemAtPath:destinationFile error:&error]) { NSLog(@"Change Permission Error: %@", error); XCTFail(@"Failed setting file permissions"); } } afterDiffHandler:nil afterPatchHandler:nil]; XCTAssertFalse(success); } - (void)testInvalidSparkleFrameworkInAfterTree { BOOL success = [self createAndApplyPatchWithBeforeDiffHandler:^(NSFileManager *fileManager, NSString *sourceDirectory, NSString *destinationDirectory) { NSString *sourceA = [sourceDirectory stringByAppendingPathComponent:@"Frameworks/Sparkle.framework/Versions/A/"]; NSString *destinationB = [destinationDirectory stringByAppendingPathComponent:@"Frameworks/Sparkle.framework/Versions/B/"]; XCTAssertTrue([fileManager createDirectoryAtPath:sourceA withIntermediateDirectories:YES attributes:NULL error:NULL]); XCTAssertTrue([fileManager createDirectoryAtPath:destinationB withIntermediateDirectories:YES attributes:NULL error:NULL]); NSString *sourceFile = [sourceA stringByAppendingPathComponent:@"Sparkle"]; NSString *destinationFile = [destinationB stringByAppendingPathComponent:@"Sparkle"]; XCTAssertTrue([[self bigData1] writeToFile:sourceFile atomically:YES]); XCTAssertTrue([[self bigData2] writeToFile:destinationFile atomically:YES]); NSError *error = nil; if (![fileManager setAttributes:@{NSFilePosixPermissions : @0755} ofItemAtPath:sourceFile error:&error]) { NSLog(@"Change Permission Error: %@", error); XCTFail(@"Failed setting file permissions"); } // Use invalid permission mode 0700 if (![fileManager setAttributes:@{NSFilePosixPermissions : @0700} ofItemAtPath:destinationFile error:&error]) { NSLog(@"Change Permission Error: %@", error); XCTFail(@"Failed setting file permissions"); } } afterDiffHandler:nil afterPatchHandler:nil]; XCTAssertFalse(success); } - (void)testBundleCreationDate { NSDate *sourceDate = [NSDate dateWithTimeIntervalSinceReferenceDate:420111117.0]; NSDate *destinationDate = [NSDate dateWithTimeIntervalSinceReferenceDate:530112117.0]; BOOL success = [self createAndApplyPatchWithBeforeDiffHandler:^(NSFileManager *fileManager, NSString *sourceDirectory, NSString *destinationDirectory) { NSString *sourceFile = [sourceDirectory stringByAppendingPathComponent:@"A"]; NSString *destinationFile = [destinationDirectory stringByAppendingPathComponent:@"A"]; XCTAssertTrue([[NSData data] writeToFile:sourceFile atomically:YES]); XCTAssertTrue([[NSData dataWithBytes:"loltest" length:7] writeToFile:destinationFile atomically:YES]); { NSError *setFileCreationDateError = nil; if (![fileManager setAttributes:@{NSFileCreationDate: sourceDate} ofItemAtPath:sourceDirectory error:&setFileCreationDateError]) { XCTFail(@"Failed to modify file creation date for source directory: %@", setFileCreationDateError.localizedDescription); } } { NSError *setFileCreationDateError = nil; if (![fileManager setAttributes:@{NSFileCreationDate: destinationDate} ofItemAtPath:destinationDirectory error:&setFileCreationDateError]) { XCTFail(@"Failed to modify file creation date for destination directory: %@", setFileCreationDateError.localizedDescription); } } } afterDiffHandler:nil afterPatchHandler:^(NSFileManager *fileManager, NSString * __unused sourceDirectory, NSString *destinationDirectory) { NSError *fileAttributesError = nil; NSDictionary *fileAttributes = [fileManager attributesOfItemAtPath:destinationDirectory error:&fileAttributesError]; if (fileAttributes == nil) { XCTFail(@"Failed to retrieve file attributes from destination directory: %@", fileAttributesError.localizedDescription); } NSDate *fileCreationDate = fileAttributes[NSFileCreationDate]; XCTAssertNotNil(fileCreationDate); XCTAssertEqualObjects(destinationDate, fileCreationDate); } testingVersion3Delta:NO testingVersion2Delta:NO]; XCTAssertTrue(success); } @end ================================================ FILE: Tests/SUCodeSigningVerifierTest.m ================================================ // // SUCodeSigningVerifierTest.m // Sparkle // // Created by Isaac Wankerl on 04/13/2015. // Copyright (c) 2015 Sparkle Project. All rights reserved. // #import #import #import "SUCodeSigningVerifier.h" #import "SUAdHocCodeSigning.h" #import "SUFileManager.h" @interface SUCodeSigningVerifierTest : XCTestCase @end @implementation SUCodeSigningVerifierTest { NSURL *_notSignedAppURL; NSURL *_validSignedAppURL; NSURL *_invalidSignedAppURL; NSURL *_devSignedAppURL; NSURL *_devSignedVersion2AppURL; NSURL *_devInvalidSignedAppURL; NSURL *_devSignedDiskImageURL; NSURL *_unsignedDiskImageURL; NSURL *_adhocSignedDiskImageURL; } - (void)setUp { [super setUp]; NSBundle *unitTestBundle = [NSBundle bundleForClass:[self class]]; _devSignedDiskImageURL = [unitTestBundle URLForResource:@"DevSignedAppVersion2" withExtension:@"dmg"]; _unsignedDiskImageURL = [unitTestBundle URLForResource:@"SparkleTestCodeSign_apfs" withExtension:@"dmg"]; _adhocSignedDiskImageURL = [unitTestBundle URLForResource:@"SparkleTestCodeSign_apfs_lzma_aux_files_adhoc" withExtension:@"dmg"]; NSString *zippedAppURL = [unitTestBundle pathForResource:@"SparkleTestCodeSignApp" ofType:@"zip"]; SUFileManager *fileManager = [[SUFileManager alloc] init]; NSError *tempError = nil; NSURL *tempDir = [fileManager makeTemporaryDirectoryAppropriateForDirectoryURL:[NSURL fileURLWithPath:zippedAppURL] error:&tempError]; if (tempDir == nil) { XCTFail(@"Failed to create temporary directory with error: %@", tempError); return; } NSError *error = nil; if ([[NSFileManager defaultManager] createDirectoryAtURL:tempDir withIntermediateDirectories:YES attributes:nil error:&error]) { if ([self unzip:zippedAppURL toPath:tempDir.path]) { _notSignedAppURL = [tempDir URLByAppendingPathComponent:@"SparkleTestCodeSignApp.app"]; [self setUpValidSignedApp]; [self setUpDevSignedApps]; [self setUpInvalidSignedApp]; } else { NSLog(@"Failed to unzip %@", zippedAppURL); } } else { NSLog(@"Failed to created dir %@ with error %@", tempDir, error); } } - (void)tearDown { [super tearDown]; if (_notSignedAppURL != nil) { NSURL *tempDir = [_notSignedAppURL URLByDeletingLastPathComponent]; [[NSFileManager defaultManager] removeItemAtURL:tempDir error:nil]; } } - (void)setUpValidSignedApp { NSError *error = nil; NSURL *tempDir = [_notSignedAppURL URLByDeletingLastPathComponent]; NSURL *signedAndValid = [tempDir URLByAppendingPathComponent:@"valid-signed.app"]; [[NSFileManager defaultManager] removeItemAtURL:signedAndValid error:NULL]; if (![[NSFileManager defaultManager] copyItemAtURL:_notSignedAppURL toURL:signedAndValid error:&error]) { XCTFail("Failed to copy %@ to %@ with error: %@", _notSignedAppURL, signedAndValid, error); } _validSignedAppURL = signedAndValid; if (![self codesignAppURL:_validSignedAppURL]) { XCTFail(@"Failed to codesign %@", _validSignedAppURL); } } - (void)setUpDevSignedApps { NSURL *tempDir = [_notSignedAppURL URLByDeletingLastPathComponent]; NSURL *devSignedAppURL = [tempDir URLByAppendingPathComponent:@"DevSignedApp.app"]; NSURL *devSignedAppVersion2URL = [tempDir URLByAppendingPathComponent:@"DevSignedAppVersion2.app"]; NSURL *devInvalidSignedAppURL = [tempDir URLByAppendingPathComponent:@"DevInvalidSignedApp.app"]; _devSignedAppURL = devSignedAppURL; _devSignedVersion2AppURL = devSignedAppVersion2URL; _devInvalidSignedAppURL = devInvalidSignedAppURL; [[NSFileManager defaultManager] removeItemAtURL:devSignedAppURL error:NULL]; [[NSFileManager defaultManager] removeItemAtURL:devSignedAppVersion2URL error:NULL]; [[NSFileManager defaultManager] removeItemAtURL:devInvalidSignedAppURL error:NULL]; // Make a copy of a signed devID app so we can match signatures later // Matching signatures on ad-hoc signed apps does *not* work NSBundle *unitTestBundle = [NSBundle bundleForClass:[self class]]; { NSString *zippedAppURL = [unitTestBundle pathForResource:@"DevSignedApp" ofType:@"zip"]; if ([self unzip:zippedAppURL toPath:tempDir.path]) { BOOL copiedApp = [[NSFileManager defaultManager] copyItemAtURL:devSignedAppURL toURL:devInvalidSignedAppURL error:NULL]; XCTAssertTrue(copiedApp); BOOL wroteData = [[NSData data] writeToURL:(NSURL * _Nonnull)[devInvalidSignedAppURL URLByAppendingPathComponent:@"Contents/Resources/foo"] atomically:YES]; XCTAssertTrue(wroteData); } else { XCTFail(@"Failed to unzip dev signed app"); } } { NSString *zippedAppURL = [unitTestBundle pathForResource:@"DevSignedAppVersion2" ofType:@"zip"]; if (![self unzip:zippedAppURL toPath:tempDir.path]) { XCTFail(@"Failed to unzip dev signed app"); } } } - (void)setUpInvalidSignedApp { NSError *error = nil; NSURL *tempDir = [_notSignedAppURL URLByDeletingLastPathComponent]; NSURL *signedAndInvalid = [tempDir URLByAppendingPathComponent:@"invalid-signed.app"]; [[NSFileManager defaultManager] removeItemAtURL:signedAndInvalid error:NULL]; if ([[NSFileManager defaultManager] copyItemAtURL:_notSignedAppURL toURL:signedAndInvalid error:&error]) { _invalidSignedAppURL = signedAndInvalid; if ([self codesignAppURL:_invalidSignedAppURL]) { NSURL *fileInAppBundleToRemove = [_invalidSignedAppURL URLByAppendingPathComponent:@"Contents/Resources/test_app_only_dsa_pub.pem"]; if (![[NSFileManager defaultManager] removeItemAtURL:fileInAppBundleToRemove error:&error]) { NSLog(@"Failed to remove %@ with error %@", fileInAppBundleToRemove, error); } } else { NSLog(@"Failed to codesign %@", _invalidSignedAppURL); } } else { NSLog(@"Failed to copy %@ to %@ with error %@", _notSignedAppURL, signedAndInvalid, error); } } - (BOOL)unzip:(NSString *)zipPath toPath:(NSString *)destPath { BOOL success = NO; @try { NSTask *task = [[NSTask alloc] init]; task.launchPath = @"/usr/bin/unzip"; task.currentDirectoryPath = destPath; task.arguments = @[zipPath]; [task launch]; [task waitUntilExit]; success = (task.terminationStatus == 0); } @catch (NSException *exception) { NSLog(@"exception: %@", exception); } return success; } - (BOOL)codesignAppURL:(NSURL *)appURL { return [SUAdHocCodeSigning codeSignApplicationAtPath:appURL.path]; } - (void)testUnsignedApp { XCTAssertFalse([SUCodeSigningVerifier bundleAtURLIsCodeSigned:_notSignedAppURL], @"App not expected to be code signed"); NSError *error = nil; XCTAssertFalse([SUCodeSigningVerifier codeSignatureIsValidAtBundleURL:_notSignedAppURL error:&error], @"signature should not be valid as it's not code signed"); XCTAssertNotNil(error, @"error should not be nil"); } - (void)testValidSignedApp { XCTAssertTrue([SUCodeSigningVerifier bundleAtURLIsCodeSigned:_validSignedAppURL], @"App expected to be code signed"); NSError *error = nil; XCTAssertTrue([SUCodeSigningVerifier codeSignatureIsValidAtBundleURL:_validSignedAppURL error:&error], @"signature should be valid"); XCTAssertNil(error, @"error should be nil"); } - (void)testValidSignedDevIdApp { XCTAssertTrue([SUCodeSigningVerifier bundleAtURLIsCodeSigned:_devSignedAppURL], @"App expected to be code signed"); XCTAssertTrue([SUCodeSigningVerifier bundleAtURLIsCodeSigned:_devSignedVersion2AppURL], @"App expected to be code signed"); { NSError *error = nil; XCTAssertTrue([SUCodeSigningVerifier codeSignatureIsValidAtBundleURL:_devSignedAppURL error:&error], @"signature should be valid"); XCTAssertNil(error, @"error should be nil"); } { NSError *error = nil; XCTAssertTrue([SUCodeSigningVerifier codeSignatureIsValidAtBundleURL:_devSignedVersion2AppURL error:&error], @"signature should be valid"); XCTAssertNil(error, @"error should be nil"); } } - (void)testInvalidSignedDevIdApp { XCTAssertTrue([SUCodeSigningVerifier bundleAtURLIsCodeSigned:_devInvalidSignedAppURL], @"App expected to be code signed"); NSError *error = nil; XCTAssertFalse([SUCodeSigningVerifier codeSignatureIsValidAtBundleURL:_devInvalidSignedAppURL error:&error], @"signature should be invalid"); XCTAssertNotNil(error, @"error should be not be nil"); } - (void)testValidMatchingSelf { NSError *error = nil; XCTAssertTrue([SUCodeSigningVerifier codeSignatureIsValidAtBundleURL:_devSignedAppURL andMatchesSignatureAtBundleURL:_devSignedAppURL error:&error], @"Our valid signed app expected to having matching signature to itself"); } - (void)testValidMatchingDevIdApp { // We can't test our own app because matching with ad-hoc signed apps understandably does not succeed { NSError *error = nil; XCTAssertTrue([SUCodeSigningVerifier codeSignatureIsValidAtBundleURL:_devSignedAppURL andMatchesSignatureAtBundleURL:_devSignedVersion2AppURL error:&error], @"The dev ID signed app is expected to have a matching identity signature to a newer version"); XCTAssertNil(error); } { NSError *error = nil; XCTAssertTrue([SUCodeSigningVerifier codeSignatureIsValidAtBundleURL:_devSignedVersion2AppURL andMatchesSignatureAtBundleURL:_devSignedAppURL error:&error], @"The dev ID signed app is expected to have a matching identity signature to an older version"); XCTAssertNil(error); } } - (void)testValidMatchingDevIdDiskImage { NSError *error = nil; XCTAssertTrue([SUCodeSigningVerifier codeSignatureIsValidAtDownloadURL:_devSignedDiskImageURL andMatchesDeveloperIDTeamFromOldBundleURL:_devSignedAppURL error:&error]); XCTAssertNil(error); } - (void)testInvalidMatchingDevIdDiskImageWithAppNoSigning { NSError *error = nil; XCTAssertFalse([SUCodeSigningVerifier codeSignatureIsValidAtDownloadURL:_devSignedDiskImageURL andMatchesDeveloperIDTeamFromOldBundleURL:_notSignedAppURL error:&error]); XCTAssertNotNil(error); } - (void)testInvalidMatchingDevIdDiskImageWithAppAdhocSigning { NSError *error = nil; XCTAssertFalse([SUCodeSigningVerifier codeSignatureIsValidAtDownloadURL:_devSignedDiskImageURL andMatchesDeveloperIDTeamFromOldBundleURL:_validSignedAppURL error:&error]); XCTAssertNotNil(error); } - (void)testInvalidMatchWithNoDiskImageSigning { NSError *error = nil; XCTAssertFalse([SUCodeSigningVerifier codeSignatureIsValidAtDownloadURL:_unsignedDiskImageURL andMatchesDeveloperIDTeamFromOldBundleURL:_validSignedAppURL error:&error]); XCTAssertNotNil(error); } - (void)testInvalidMatchWithAdhocSignedDiskImage { NSError *error = nil; XCTAssertFalse([SUCodeSigningVerifier codeSignatureIsValidAtDownloadURL:_adhocSignedDiskImageURL andMatchesDeveloperIDTeamFromOldBundleURL:_devSignedAppURL error:&error]); XCTAssertNotNil(error); } - (void)testInvalidMatchingWithBrokenBundle { // We can't test our own app because matching with ad-hoc signed apps understandably does not succeed { NSError *error = nil; XCTAssertFalse([SUCodeSigningVerifier codeSignatureIsValidAtBundleURL:_devSignedAppURL andMatchesSignatureAtBundleURL:_invalidSignedAppURL error:&error], @"The dev ID signed app is expected to not have a matching identity signature to its altered invalid copy"); XCTAssertNotNil(error); } { NSError *error = nil; XCTAssertFalse([SUCodeSigningVerifier codeSignatureIsValidAtBundleURL:_invalidSignedAppURL andMatchesSignatureAtBundleURL:_devSignedAppURL error:&error], @"The invalid dev ID signed app is expected to not have a matching identity signature to the valid version"); XCTAssertNotNil(error); } } - (void)testInvalidMatching { NSError *error = nil; XCTAssertFalse([SUCodeSigningVerifier codeSignatureIsValidAtBundleURL:_validSignedAppURL andMatchesSignatureAtBundleURL:_devSignedAppURL error:&error], @"Dev ID signed app bundle expected to have different signature than our adhoc valid signed app"); } - (void)testInvalidSignedApp { XCTAssertTrue([SUCodeSigningVerifier bundleAtURLIsCodeSigned:_invalidSignedAppURL], @"App expected to be code signed, but signature is invalid"); NSError *error = nil; XCTAssertFalse([SUCodeSigningVerifier codeSignatureIsValidAtBundleURL:_invalidSignedAppURL error:&error], @"signature should not be valid"); XCTAssertNotNil(error, @"error should not be nil"); } @end ================================================ FILE: Tests/SUFeedSignatureVerifierTest.swift ================================================ // // SUFeedSignatureVerifierTest.swift // Sparkle Unit Tests // // Created on 12/30/25. // Copyright © 2025 Sparkle Project. All rights reserved. // import Foundation import XCTest class SUFeedSignatureVerifierTest: XCTestCase { var privateEdKey: Data! var publicEdKey: Data! override func setUp() { super.setUp() // These are the same private and public keys the Sparkle Test App uses let privateKeyBytes: [UInt8] = [ 200, 238, 135, 84, 10, 189, 3, 193, 61, 208, 203, 30, 133, 47, 12, 22, 19, 52, 252, 99, 110, 205, 209, 94, 215, 144, 201, 70, 27, 162, 163, 108, 0, 164, 68, 184, 226, 93, 121, 199, 172, 17, 26, 64, 89, 68, 232, 41, 2, 26, 245, 175, 158, 165, 42, 55, 5, 97, 8, 243, 251, 164, 93, 9 ] privateEdKey = Data(privateKeyBytes) let publicKeyBytes: [UInt8] = [ 121, 17, 79, 45, 155, 141, 51, 169, 188, 110, 91, 102, 182, 147, 215, 225, 252, 202, 110, 231, 200, 215, 62, 171, 40, 145, 237, 128, 130, 44, 150, 89 ] publicEdKey = Data(publicKeyBytes) } func testSigningAndValidatingAppcast() { let appcastURL = Bundle(for: SUFeedSignatureVerifierTest.self).url(forResource: "testappcast", withExtension: "xml")! let initialAppcastData = try! Data(contentsOf: appcastURL) let publicKeys = SUPublicKeys(ed: publicEdKey.base64EncodedString(), dsa: nil) let signatureVerifier = SUSignatureVerifier(publicKeys: publicKeys) // The content should be the same as the initial appcast data which isn't signed yet do { var outEdSignatureBase64: NSString? var outContentLength: UInt64 = 0 let appcastContentData = SPUExtractAppcastContent(initialAppcastData, &outEdSignatureBase64, &outContentLength) XCTAssertNil(outEdSignatureBase64) XCTAssertEqual(outContentLength, 0) XCTAssertEqual(initialAppcastData, appcastContentData) // Trying to verify this file should fail when no signature is present let signatures = SUSignatures(ed: outEdSignatureBase64 as? String, dsa: nil) let verifierInformation = SPUVerifierInformation(expectedVersion: nil, expectedContentLength: outContentLength) verifierInformation.actualContentLength = UInt64(appcastContentData.count) do { try signatureVerifier.verifyData(appcastContentData, signatures: signatures, fileKind: "appcast", verifierInformation: verifierInformation) XCTFail("Verification should have failed on unsigned file") } catch { XCTAssertNotNil(error) } } // Add sign warning to appcast let appcastDataWithSigningWarning = addSignWarningToAppcast(data: initialAppcastData) XCTAssertGreaterThan(appcastDataWithSigningWarning.count, initialAppcastData.count) // Adding a signing warning to appcast shouldn't change anything do { let appcastDataWithSigningWarningAgain = addSignWarningToAppcast(data: appcastDataWithSigningWarning) XCTAssertEqual(appcastDataWithSigningWarningAgain, appcastDataWithSigningWarning) } let signedAppcastData = try! signAppcast(data: appcastDataWithSigningWarning, publicEdKey: publicEdKey, privateEdKey: privateEdKey) // XML data should be valid after signing _ = try! XMLDocument(data: signedAppcastData, options: XMLNode.Options()) do { // Content of signed data should be same as content before it var outEdSignatureBase64: NSString? var outContentLength: UInt64 = 0 let contentSignedAppcastData = SPUExtractAppcastContent(signedAppcastData, &outEdSignatureBase64, &outContentLength) XCTAssertNotNil(outEdSignatureBase64) XCTAssertEqual(outContentLength, UInt64(appcastDataWithSigningWarning.count)) XCTAssertEqual(contentSignedAppcastData, appcastDataWithSigningWarning) // Verify the signature is correct let signatures = SUSignatures(ed: outEdSignatureBase64! as String, dsa: nil) let verifierInformation = SPUVerifierInformation(expectedVersion: nil, expectedContentLength: UInt64(appcastDataWithSigningWarning.count)) verifierInformation.actualContentLength = UInt64(contentSignedAppcastData.count) try! signatureVerifier.verifyData(contentSignedAppcastData, signatures: signatures, fileKind: "appcast", verifierInformation: verifierInformation) // Insert a byte somewhere in the middle and ensure signing validation fails var modifiedInvalidSignedAppcastData = signedAppcastData modifiedInvalidSignedAppcastData.insert(62, at: signedAppcastData.count / 2) do { try signatureVerifier.verifyData(modifiedInvalidSignedAppcastData, signatures: signatures, fileKind: "appcast", verifierInformation: nil) XCTFail("Signature verification of modified appcast should have failed") } catch { XCTAssertNotNil(error) } // Re-signing should fix the signature however let modifiedSignedAppcastData = try! signAppcast(data: modifiedInvalidSignedAppcastData, publicEdKey: publicEdKey, privateEdKey: privateEdKey) let modifiedSignedAppcastDataContent = SPUExtractAppcastContent(modifiedSignedAppcastData, &outEdSignatureBase64, &outContentLength) let signaturesAfterModification = SUSignatures(ed: outEdSignatureBase64! as String, dsa: nil) try! signatureVerifier.verifyData(modifiedSignedAppcastDataContent, signatures: signaturesAfterModification, fileKind: "appcast", verifierInformation: nil) } } func testAddingSignWarningToHTMLReleaseNotes() { let releaseNotesURL = Bundle(for: SUFeedSignatureVerifierTest.self).url(forResource: "testreleasenotes", withExtension: "html")! let releaseNotesData = try! Data(contentsOf: releaseNotesURL) // Add sign warning to release notes let releaseNotesDataWithSigningWarning = updateHTMLCommentSigningWarningInReleaseNotes(data: releaseNotesData)! XCTAssertGreaterThan(releaseNotesDataWithSigningWarning.count, releaseNotesData.count) // Test that the content is the same minus the beginning signing warning do { let byteDifference = releaseNotesDataWithSigningWarning.count - releaseNotesData.count XCTAssertEqual(releaseNotesDataWithSigningWarning.subdata(in: releaseNotesDataWithSigningWarning.startIndex.advanced(by: byteDifference) ..< releaseNotesDataWithSigningWarning.endIndex), releaseNotesData) } // Test the content using SPUExtractReleaseNotesContent() do { let releaseNotesContent = SPUExtractReleaseNotesContent(releaseNotesDataWithSigningWarning) XCTAssertEqual(releaseNotesContent, releaseNotesData) // Extracting content again should return same data let releaseNotesContentAgain = SPUExtractReleaseNotesContent(releaseNotesContent) XCTAssertEqual(releaseNotesContentAgain, releaseNotesData) } // Test signing warning data without newline do { let signWarning = "foobar" let signWarningData = Data(signWarning.utf8) let releaseNotesContent = SPUExtractReleaseNotesContent(signWarningData) XCTAssertEqual(releaseNotesContent, Data("foobar".utf8)) } // Test minimal signing warning data do { let signWarning = "" let signWarningData = Data(signWarning.utf8) let releaseNotesContent = SPUExtractReleaseNotesContent(signWarningData) XCTAssertEqual(releaseNotesContent.count, 0) } // Test minimal signing warning data with just a newline do { let signWarning = "\n" let signWarningData = Data(signWarning.utf8) // This should skip over the newline let releaseNotesContent = SPUExtractReleaseNotesContent(signWarningData) XCTAssertEqual(releaseNotesContent.count, 0) } } } ================================================ FILE: Tests/SUFileManagerTest.swift ================================================ // // SUFileManagerTest.swift // Sparkle // // Created by Mayur Pawashe on 9/26/15. // Copyright © 2015 Sparkle Project. All rights reserved. // import XCTest class SUFileManagerTest: XCTestCase { func makeTempFiles(_ testBlock: (SUFileManager, URL, URL, URL, URL, URL, URL) -> Void) { let fileManager = SUFileManager() let tempDirectoryURL = try! fileManager.makeTemporaryDirectoryAppropriate(forDirectoryURL: URL(fileURLWithPath: NSHomeDirectory())) defer { try! fileManager.removeItem(at: tempDirectoryURL) } let ordinaryFileURL = tempDirectoryURL.appendingPathComponent("a file written by sparkles unit tests") try! "foo".data(using: String.Encoding.utf8)!.write(to: ordinaryFileURL, options: .atomic) let directoryURL = tempDirectoryURL.appendingPathComponent("a directory written by sparkles unit tests") try! FileManager.default.createDirectory(at: directoryURL, withIntermediateDirectories: false, attributes: nil) let fileInDirectoryURL = directoryURL.appendingPathComponent("a file inside a directory written by sparkles unit tests") try! "bar baz".data(using: String.Encoding.utf8)!.write(to: fileInDirectoryURL, options: .atomic) let validSymlinkURL = tempDirectoryURL.appendingPathComponent("symlink test") try! FileManager.default.createSymbolicLink(at: validSymlinkURL, withDestinationURL: directoryURL) let invalidSymlinkURL = tempDirectoryURL.appendingPathComponent("symlink test 2") try! FileManager.default.createSymbolicLink(at: invalidSymlinkURL, withDestinationURL: (tempDirectoryURL.appendingPathComponent("does not exist"))) testBlock(fileManager, tempDirectoryURL, ordinaryFileURL, directoryURL, fileInDirectoryURL, validSymlinkURL, invalidSymlinkURL) } func testMoveFiles() { makeTempFiles() { fileManager, rootURL, ordinaryFileURL, directoryURL, fileInDirectoryURL, validSymlinkURL, invalidSymlinkURL in XCTAssertNil(try? fileManager.moveItem(at: ordinaryFileURL, to: directoryURL)) XCTAssertNil(try? fileManager.moveItem(at: ordinaryFileURL, to: directoryURL.appendingPathComponent("foo").appendingPathComponent("bar"))) XCTAssertNil(try? fileManager.moveItem(at: rootURL.appendingPathComponent("does not exist"), to: directoryURL)) let newFileURL = (ordinaryFileURL.deletingLastPathComponent().appendingPathComponent("new file")) try! fileManager.moveItem(at: ordinaryFileURL, to: newFileURL) XCTAssertFalse(fileManager._itemExists(at: ordinaryFileURL)) XCTAssertTrue(fileManager._itemExists(at: newFileURL)) let newValidSymlinkURL = (ordinaryFileURL.deletingLastPathComponent().appendingPathComponent("new symlink")) try! fileManager.moveItem(at: validSymlinkURL, to: newValidSymlinkURL) XCTAssertFalse(fileManager._itemExists(at: validSymlinkURL)) XCTAssertTrue(fileManager._itemExists(at: newValidSymlinkURL)) XCTAssertTrue(fileManager._itemExists(at: directoryURL)) let newInvalidSymlinkURL = (ordinaryFileURL.deletingLastPathComponent().appendingPathComponent("new invalid symlink")) try! fileManager.moveItem(at: invalidSymlinkURL, to: newInvalidSymlinkURL) XCTAssertFalse(fileManager._itemExists(at: invalidSymlinkURL)) XCTAssertTrue(fileManager._itemExists(at: newValidSymlinkURL)) let newDirectoryURL = (ordinaryFileURL.deletingLastPathComponent().appendingPathComponent("new directory")) try! fileManager.moveItem(at: directoryURL, to: newDirectoryURL) XCTAssertFalse(fileManager._itemExists(at: directoryURL)) XCTAssertTrue(fileManager._itemExists(at: newDirectoryURL)) XCTAssertFalse(fileManager._itemExists(at: fileInDirectoryURL)) XCTAssertTrue(fileManager._itemExists(at: newDirectoryURL.appendingPathComponent(fileInDirectoryURL.lastPathComponent))) } } func testCopyFiles() { makeTempFiles() { fileManager, rootURL, ordinaryFileURL, directoryURL, fileInDirectoryURL, _, invalidSymlinkURL in XCTAssertNil(try? fileManager.copyItem(at: ordinaryFileURL, to: directoryURL)) XCTAssertNil(try? fileManager.copyItem(at: ordinaryFileURL, to: directoryURL.appendingPathComponent("foo").appendingPathComponent("bar"))) XCTAssertNil(try? fileManager.copyItem(at: rootURL.appendingPathComponent("does not exist"), to: directoryURL)) let newFileURL = (ordinaryFileURL.deletingLastPathComponent().appendingPathComponent("new file")) try! fileManager.copyItem(at: ordinaryFileURL, to: newFileURL) XCTAssertTrue(fileManager._itemExists(at: ordinaryFileURL)) XCTAssertTrue(fileManager._itemExists(at: newFileURL)) let newSymlinkURL = (ordinaryFileURL.deletingLastPathComponent().appendingPathComponent("new symlink file")) try! fileManager.copyItem(at: invalidSymlinkURL, to: newSymlinkURL) XCTAssertTrue(fileManager._itemExists(at: newSymlinkURL)) let newDirectoryURL = (ordinaryFileURL.deletingLastPathComponent().appendingPathComponent("new directory")) try! fileManager.copyItem(at: directoryURL, to: newDirectoryURL) XCTAssertTrue(fileManager._itemExists(at: directoryURL)) XCTAssertTrue(fileManager._itemExists(at: newDirectoryURL)) XCTAssertTrue(fileManager._itemExists(at: fileInDirectoryURL)) XCTAssertTrue(fileManager._itemExists(at: newDirectoryURL.appendingPathComponent(fileInDirectoryURL.lastPathComponent))) } } func testRemoveFiles() { makeTempFiles() { fileManager, rootURL, ordinaryFileURL, directoryURL, fileInDirectoryURL, validSymlinkURL, _ in XCTAssertNil(try? fileManager.removeItem(at: rootURL.appendingPathComponent("does not exist"))) try! fileManager.removeItem(at: ordinaryFileURL) XCTAssertFalse(fileManager._itemExists(at: ordinaryFileURL)) try! fileManager.removeItem(at: validSymlinkURL) XCTAssertFalse(fileManager._itemExists(at: validSymlinkURL)) XCTAssertTrue(fileManager._itemExists(at: directoryURL)) try! fileManager.removeItem(at: directoryURL) XCTAssertFalse(fileManager._itemExists(at: directoryURL)) XCTAssertFalse(fileManager._itemExists(at: fileInDirectoryURL)) } } func testReleaseFilesFromQuarantine() { makeTempFiles() { fileManager, _, ordinaryFileURL, directoryURL, fileInDirectoryURL, validSymlinkURL, _ in try! fileManager.releaseItemFromQuarantine(atRootURL: ordinaryFileURL) try! fileManager.releaseItemFromQuarantine(atRootURL: directoryURL) try! fileManager.releaseItemFromQuarantine(atRootURL: validSymlinkURL) let quarantineData = "does not really matter what is here".cString(using: String.Encoding.utf8)! let quarantineDataLength = Int(strlen(quarantineData)) XCTAssertEqual(0, setxattr(ordinaryFileURL.path, SUAppleQuarantineIdentifier, quarantineData, quarantineDataLength, 0, XATTR_CREATE)) XCTAssertGreaterThan(getxattr(ordinaryFileURL.path, SUAppleQuarantineIdentifier, nil, 0, 0, XATTR_NOFOLLOW), 0) try! fileManager.releaseItemFromQuarantine(atRootURL: ordinaryFileURL) XCTAssertEqual(-1, getxattr(ordinaryFileURL.path, SUAppleQuarantineIdentifier, nil, 0, 0, XATTR_NOFOLLOW)) XCTAssertEqual(0, setxattr(directoryURL.path, SUAppleQuarantineIdentifier, quarantineData, quarantineDataLength, 0, XATTR_CREATE)) XCTAssertGreaterThan(getxattr(directoryURL.path, SUAppleQuarantineIdentifier, nil, 0, 0, XATTR_NOFOLLOW), 0) XCTAssertEqual(0, setxattr(fileInDirectoryURL.path, SUAppleQuarantineIdentifier, quarantineData, quarantineDataLength, 0, XATTR_CREATE)) XCTAssertGreaterThan(getxattr(fileInDirectoryURL.path, SUAppleQuarantineIdentifier, nil, 0, 0, XATTR_NOFOLLOW), 0) // Extended attributes can't be set on symbolic links currently try! fileManager.releaseItemFromQuarantine(atRootURL: validSymlinkURL) XCTAssertGreaterThan(getxattr(directoryURL.path, SUAppleQuarantineIdentifier, nil, 0, 0, XATTR_NOFOLLOW), 0) XCTAssertEqual(-1, getxattr(validSymlinkURL.path, SUAppleQuarantineIdentifier, nil, 0, 0, XATTR_NOFOLLOW)) try! fileManager.releaseItemFromQuarantine(atRootURL: directoryURL) XCTAssertEqual(-1, getxattr(directoryURL.path, SUAppleQuarantineIdentifier, nil, 0, 0, XATTR_NOFOLLOW)) XCTAssertEqual(-1, getxattr(fileInDirectoryURL.path, SUAppleQuarantineIdentifier, nil, 0, 0, XATTR_NOFOLLOW)) } } func groupIDAtPath(_ path: String) -> gid_t { let attributes = try! FileManager.default.attributesOfItem(atPath: path) let groupID = attributes[FileAttributeKey.groupOwnerAccountID] as! NSNumber return groupID.uint32Value } // Only the super user can alter user IDs, so changing user IDs is not tested here // Instead we try to change the group ID - we just have to be a member of that group func testAlterFilesGroupID() { makeTempFiles() { fileManager, rootURL, ordinaryFileURL, directoryURL, fileInDirectoryURL, validSymlinkURL, _ in XCTAssertNil(try? fileManager.changeOwnerAndGroupOfItem(atRootURL: ordinaryFileURL, toMatch: rootURL.appendingPathComponent("does not exist"))) XCTAssertNil(try? fileManager.changeOwnerAndGroupOfItem(atRootURL: rootURL.appendingPathComponent("does not exist"), toMatch: ordinaryFileURL)) let everyoneGroup = getgrnam("everyone") let everyoneGroupID = everyoneGroup?.pointee.gr_gid let staffGroup = getgrnam("staff") let staffGroupID = staffGroup?.pointee.gr_gid XCTAssertNotEqual(staffGroupID, everyoneGroupID) XCTAssertEqual(staffGroupID, self.groupIDAtPath(ordinaryFileURL.path)) XCTAssertEqual(staffGroupID, self.groupIDAtPath(directoryURL.path)) XCTAssertEqual(staffGroupID, self.groupIDAtPath(fileInDirectoryURL.path)) XCTAssertEqual(staffGroupID, self.groupIDAtPath(validSymlinkURL.path)) try! fileManager.changeOwnerAndGroupOfItem(atRootURL: fileInDirectoryURL, toMatch: ordinaryFileURL) try! fileManager.changeOwnerAndGroupOfItem(atRootURL: ordinaryFileURL, toMatch: ordinaryFileURL) try! fileManager.changeOwnerAndGroupOfItem(atRootURL: validSymlinkURL, toMatch: ordinaryFileURL) XCTAssertEqual(staffGroupID, self.groupIDAtPath(ordinaryFileURL.path)) XCTAssertEqual(staffGroupID, self.groupIDAtPath(directoryURL.path)) XCTAssertEqual(staffGroupID, self.groupIDAtPath(validSymlinkURL.path)) XCTAssertEqual(0, chown(ordinaryFileURL.path, getuid(), everyoneGroupID!)) XCTAssertEqual(everyoneGroupID, self.groupIDAtPath(ordinaryFileURL.path)) try! fileManager.changeOwnerAndGroupOfItem(atRootURL: fileInDirectoryURL, toMatch: ordinaryFileURL) XCTAssertEqual(everyoneGroupID, self.groupIDAtPath(fileInDirectoryURL.path)) try! fileManager.changeOwnerAndGroupOfItem(atRootURL: fileInDirectoryURL, toMatch: directoryURL) XCTAssertEqual(staffGroupID, self.groupIDAtPath(fileInDirectoryURL.path)) try! fileManager.changeOwnerAndGroupOfItem(atRootURL: validSymlinkURL, toMatch: ordinaryFileURL) XCTAssertEqual(everyoneGroupID, self.groupIDAtPath(validSymlinkURL.path)) try! fileManager.changeOwnerAndGroupOfItem(atRootURL: directoryURL, toMatch: ordinaryFileURL) XCTAssertEqual(everyoneGroupID, self.groupIDAtPath(directoryURL.path)) XCTAssertEqual(everyoneGroupID, self.groupIDAtPath(fileInDirectoryURL.path)) } } func testUpdateFileModificationTime() { makeTempFiles() { fileManager, rootURL, ordinaryFileURL, directoryURL, _, validSymlinkURL, _ in XCTAssertNil(try? fileManager.updateModificationAndAccessTimeOfItem(at: rootURL.appendingPathComponent("does not exist"))) let oldOrdinaryFileAttributes = try! FileManager.default.attributesOfItem(atPath: ordinaryFileURL.path) let oldDirectoryAttributes = try! FileManager.default.attributesOfItem(atPath: directoryURL.path) let oldValidSymlinkAttributes = try! FileManager.default.attributesOfItem(atPath: validSymlinkURL.path) sleep(1); // wait for clock to advance try! fileManager.updateModificationAndAccessTimeOfItem(at: ordinaryFileURL) try! fileManager.updateModificationAndAccessTimeOfItem(at: directoryURL) try! fileManager.updateModificationAndAccessTimeOfItem(at: validSymlinkURL) let newOrdinaryFileAttributes = try! FileManager.default.attributesOfItem(atPath: ordinaryFileURL.path) XCTAssertGreaterThan((newOrdinaryFileAttributes[FileAttributeKey.modificationDate] as! Date).timeIntervalSince(oldOrdinaryFileAttributes[FileAttributeKey.modificationDate] as! Date), 0) let newDirectoryAttributes = try! FileManager.default.attributesOfItem(atPath: directoryURL.path) XCTAssertGreaterThan((newDirectoryAttributes[FileAttributeKey.modificationDate] as! Date).timeIntervalSince(oldDirectoryAttributes[FileAttributeKey.modificationDate] as! Date), 0) let newSymlinkAttributes = try! FileManager.default.attributesOfItem(atPath: validSymlinkURL.path) XCTAssertGreaterThan((newSymlinkAttributes[FileAttributeKey.modificationDate] as! Date).timeIntervalSince(oldValidSymlinkAttributes[FileAttributeKey.modificationDate] as! Date), 0) } } func testUpdateFileAccessTime() { let accessTime: ((URL) -> timespec?) = { url in var outputStat = stat() let result = lstat(url.path, &outputStat) if result != 0 { return nil } else { return outputStat.st_atimespec } } let timespecEqual: (timespec, timespec) -> Bool = {t1, t2 in (t1.tv_sec == t2.tv_sec && t1.tv_nsec == t2.tv_nsec) } makeTempFiles() { fileManager, rootURL, ordinaryFileURL, directoryURL, fileInDirectoryURL, validSymlinkURL, _ in XCTAssertNil(try? fileManager.updateAccessTimeOfItem(atRootURL: rootURL.appendingPathComponent("does not exist"))) let oldOrdinaryFileTime = accessTime(ordinaryFileURL)! let oldDirectoryTime = accessTime(directoryURL)! let oldValidSymlinkTime = accessTime(validSymlinkURL)! sleep(1); // wait for clock to advance // Make sure access time haven't changed since; lstat() shouldn't have changed the access time.. XCTAssertTrue(timespecEqual(oldOrdinaryFileTime, accessTime(ordinaryFileURL)!)) XCTAssertTrue(timespecEqual(oldDirectoryTime, accessTime(directoryURL)!)) XCTAssertTrue(timespecEqual(oldValidSymlinkTime, accessTime(validSymlinkURL)!)) // Test the symlink and make sure the target directory doesn't change try! fileManager.updateAccessTimeOfItem(atRootURL: validSymlinkURL) XCTAssertFalse(timespecEqual(oldValidSymlinkTime, accessTime(validSymlinkURL)!)) XCTAssertTrue(timespecEqual(oldDirectoryTime, accessTime(directoryURL)!)) // Test an ordinary file try! fileManager.updateAccessTimeOfItem(atRootURL: ordinaryFileURL) XCTAssertFalse(timespecEqual(oldOrdinaryFileTime, accessTime(ordinaryFileURL)!)) // Test the directory and file inside the directory try! fileManager.updateAccessTimeOfItem(atRootURL: directoryURL) let newDirectoryTime = accessTime(directoryURL)! XCTAssertFalse(timespecEqual(oldDirectoryTime, newDirectoryTime)) XCTAssertTrue(timespecEqual(newDirectoryTime, accessTime(fileInDirectoryURL)!)) } } func testFileExists() { makeTempFiles() { fileManager, rootURL, ordinaryFileURL, directoryURL, _, validSymlinkURL, invalidSymlinkURL in XCTAssertTrue(fileManager._itemExists(at: ordinaryFileURL)) XCTAssertTrue(fileManager._itemExists(at: directoryURL)) XCTAssertFalse(fileManager._itemExists(at: rootURL.appendingPathComponent("does not exist"))) var isOrdinaryFileDirectory: ObjCBool = false XCTAssertTrue(fileManager._itemExists(at: ordinaryFileURL, isDirectory: &isOrdinaryFileDirectory) && !isOrdinaryFileDirectory.boolValue) var isDirectoryADirectory: ObjCBool = false XCTAssertTrue(fileManager._itemExists(at: directoryURL, isDirectory: &isDirectoryADirectory) && isDirectoryADirectory.boolValue) XCTAssertFalse(fileManager._itemExists(at: rootURL.appendingPathComponent("does not exist"), isDirectory: nil)) XCTAssertTrue(fileManager._itemExists(at: validSymlinkURL)) var validSymlinkIsADirectory: ObjCBool = false XCTAssertTrue(fileManager._itemExists(at: validSymlinkURL, isDirectory: &validSymlinkIsADirectory) && !validSymlinkIsADirectory.boolValue) // Symlink should still exist even if it doesn't point to a file that exists XCTAssertTrue(fileManager._itemExists(at: invalidSymlinkURL)) var invalidSymlinkIsADirectory: ObjCBool = false XCTAssertTrue(fileManager._itemExists(at: invalidSymlinkURL, isDirectory: &invalidSymlinkIsADirectory) && !invalidSymlinkIsADirectory.boolValue) } } func testMakeDirectory() { makeTempFiles() { fileManager, rootURL, ordinaryFileURL, directoryURL, _, validSymlinkURL, _ in XCTAssertNil(try? fileManager.makeDirectory(at: ordinaryFileURL)) XCTAssertNil(try? fileManager.makeDirectory(at: directoryURL)) XCTAssertNil(try? fileManager.makeDirectory(at: rootURL.appendingPathComponent("this should").appendingPathComponent("be a failure"))) let newDirectoryURL = rootURL.appendingPathComponent("new test directory") XCTAssertFalse(fileManager._itemExists(at: newDirectoryURL)) try! fileManager.makeDirectory(at: newDirectoryURL) var isDirectory: ObjCBool = false XCTAssertTrue(fileManager._itemExists(at: newDirectoryURL, isDirectory: &isDirectory)) try! fileManager.removeItem(at: directoryURL) XCTAssertNil(try? fileManager.makeDirectory(at: validSymlinkURL)) } } } ================================================ FILE: Tests/SUInstallerTest.m ================================================ // // SUInstallerTest.m // Sparkle // // Created by Kornel on 24/04/2015. // Copyright (c) 2015 Sparkle Project. All rights reserved. // #import #import #import "SUHost.h" #import "SUInstaller.h" #import "SUInstallerProtocol.h" #import "SPUInstallationType.h" #import @interface SUInstallerTest : XCTestCase @end @implementation SUInstallerTest - (void)setUp { [super setUp]; // Put setup code here. This method is called before the invocation of each test method in the class. } - (void)tearDown { // Put teardown code here. This method is called after the invocation of each test method in the class. [super tearDown]; } #if SPARKLE_BUILD_PACKAGE_SUPPORT - (void)testInstallIfRoot { uid_t uid = getuid(); if (uid != 0) { NSLog(@"Test must be run as root: sudo xcodebuild -project Sparkle.xcodeproj -scheme Sparkle '-only-testing:Sparkle Unit Tests/SUInstallerTest/testInstallIfRoot' test"); return; } NSString *expectedDestination = @"/tmp/sparklepkgtest.app"; NSFileManager *fm = [NSFileManager defaultManager]; [fm removeItemAtPath:expectedDestination error:nil]; XCTAssertFalse([fm fileExistsAtPath:expectedDestination isDirectory:nil]); NSBundle *bundle = [NSBundle bundleForClass:[self class]]; NSString *path = [bundle pathForResource:@"test" ofType:@"pkg"]; XCTAssertNotNil(path); SUHost *host = [[SUHost alloc] initWithBundle:bundle]; NSError *installerError = nil; // Note: we may not be using the "correct" home directory or user name (they will be root) but our test pkg does not have // pre/post install scripts so it doesn't matter id installer = [SUInstaller installerForHost:host expectedInstallationType:SPUInstallationTypeGuidedPackage updateDirectory:[path stringByDeletingLastPathComponent] connectionCodeSigningValidationSkipped:NO homeDirectory:NSHomeDirectory() userName:NSUserName() error:&installerError]; if (installer == nil) { XCTFail(@"Installer is nil with error: %@", installerError); return; } NSError *initialInstallError = nil; if (![installer performInitialInstallation:&initialInstallError]) { XCTFail(@"Initial Installation failed with error: %@", initialInstallError); return; } NSError *finalInstallError = nil; if (![installer performFinalInstallationProgressBlock:nil error:&finalInstallError]) { XCTFail(@"Final installation failed with error: %@", finalInstallError); return; } XCTAssertTrue([fm fileExistsAtPath:expectedDestination isDirectory:nil]); [fm removeItemAtPath:expectedDestination error:nil]; } #endif @end ================================================ FILE: Tests/SUSignatureVerifierTest.m ================================================ // // SUSignatureVerifierTest.m // Sparkle // // Created by Kornel on 25/07/2014. // Copyright (c) 2014 Sparkle Project. All rights reserved. // #import #import #import "SUSignatureVerifier.h" #import "SUSignatures.h" @interface SUSignatureVerifierTest : XCTestCase @end @implementation SUSignatureVerifierTest { NSString *_testFile; NSString *_pubDSAKeyFile; NSString *_pubEdKey; } - (void)setUp { [super setUp]; _testFile = [[NSBundle bundleForClass:[self class]] pathForResource:@"signed-test-file" ofType:@"txt"]; _pubDSAKeyFile = [[NSBundle bundleForClass:[self class]] pathForResource:@"test-pubkey" ofType:@"pem"]; _pubEdKey = @"rhHib+w769W2/6/t+oM1ZxgjBB93BfBKMLO0Qo1etQs="; } - (void)testVerifyFileAtPathUsingDSA { NSString *pubKey = [NSString stringWithContentsOfFile:_pubDSAKeyFile encoding:NSASCIIStringEncoding error:nil]; XCTAssertNotNil(pubKey, @"Public key must be readable"); NSString *validSig = @"MCwCFCIHCIYYkfZavNzTitTW5tlRp/k5AhQ40poFytqcVhIYdCxQznaXeJPJDQ=="; NSError *error = nil; #if SPARKLE_BUILD_LEGACY_DSA_SUPPORT XCTAssertTrue([self checkFile:_testFile withDSAKey:pubKey signature:validSig error:&error], @"Expected valid signature: %@", error); #else XCTAssertFalse([self checkFile:_testFile withDSAKey:pubKey signature:validSig error:&error], @"Expected DSA verification to fail: %@", error); #endif XCTAssertFalse([self checkFile:_testFile withDSAKey:@"lol" signature:validSig error:&error], @"Invalid pubkey: %@", error); XCTAssertFalse([self checkFile:_pubDSAKeyFile withDSAKey:pubKey signature:validSig error:&error], @"Wrong file checked: %@", error); XCTAssertFalse([self checkFile:_testFile withDSAKey:pubKey signature:@"MCwCFCIHCiYYkfZavNzTitTW5tlRp/k5AhQ40poFytqcVhIYdCxQznaXeJPJDQ==" error:&error], @"Expected invalid signature: %@", error); #if SPARKLE_BUILD_LEGACY_DSA_SUPPORT XCTAssertTrue([self checkFile:_testFile withDSAKey:pubKey signature:@"MC0CFAsKO7cq2q7L5/FWe6ybVIQkwAwSAhUA2Q8GKsE309eugi/v3Kh1W3w3N8c=" error:&error], @"Expected valid signature: %@", error); #else XCTAssertFalse([self checkFile:_testFile withDSAKey:pubKey signature:@"MC0CFAsKO7cq2q7L5/FWe6ybVIQkwAwSAhUA2Q8GKsE309eugi/v3Kh1W3w3N8c=" error:&error], @"Expected DSA verification to fail: %@", error); #endif XCTAssertFalse([self checkFile:_testFile withDSAKey:pubKey signature:@"MC0CFAsKO7cq2q7L5/FWe6ybVIQkwAwSAhUA2Q8GKsE309eugi/v3Kh1W3w3N8" error:&error], @"Expected invalid signature: %@", error); } - (void)testVerifyFileAtPathUsingED25519 { NSString *validSig = @"EIawm2YkDZ2gBfkEMF2+1VuuTeXnCGZOdnMdVgPPvDZioq7bvDayXqKkIIzSjKMmeFdcFJOHdnba5ZV60+gPBw=="; NSError *error = nil; XCTAssertTrue([self checkFile:_testFile withEdKey:_pubEdKey signature:validSig error:&error], @"Expected valid signature: %@", error); XCTAssertFalse([self checkFile:_testFile withEdKey:@"lol" signature:validSig error:&error], @"Invalid pubkey: %@", error); XCTAssertFalse([self checkFile:_pubDSAKeyFile withEdKey:_pubEdKey signature:validSig error:&error], @"Wrong file checked: %@", error); XCTAssertFalse([self checkFile:_testFile withEdKey:_pubEdKey signature:@"wTcpXCgWoa4NrJpsfzS61FXJIbv963//12U2ef9xstzVOLPHYK2N4/ojgpDV5N1/NGG1uWMBgK+kEWp0Z5zMDQ==" error:&error], @"Expected wrong signature: %@", error); XCTAssertFalse([self checkFile:_testFile withEdKey:_pubEdKey signature:@"lol" error:&error], @"Invalid signature: %@", error); } - (BOOL)checkFile:(NSString *)aFile withDSAKey:(NSString *)pubKey signature:(NSString *)sigString error:(NSError * __autoreleasing *)error { SUPublicKeys *pubKeys = [[SUPublicKeys alloc] initWithEd:nil dsa:pubKey]; SUSignatureVerifier *v = [[SUSignatureVerifier alloc] initWithPublicKeys:pubKeys]; SUSignatures *sig = [[SUSignatures alloc] initWithEd:nil #if SPARKLE_BUILD_LEGACY_DSA_SUPPORT dsa:sigString #endif ]; return [v verifyFileAtPath:aFile signatures:sig verifierInformation:nil error:error]; } static SUSignatures *createSignatures(NSString *edString, NSString *dsaString) { #if !SPARKLE_BUILD_LEGACY_DSA_SUPPORT (void)dsaString; #endif SUSignatures *sig = [[SUSignatures alloc] initWithEd:edString #if SPARKLE_BUILD_LEGACY_DSA_SUPPORT dsa:dsaString #endif ]; return sig; } - (BOOL)checkFile:(NSString *)aFile withEdKey:(NSString *)pubKey signature:(NSString *)sigString error:(NSError * __autoreleasing *)error { SUPublicKeys *pubKeys = [[SUPublicKeys alloc] initWithEd:pubKey dsa:nil]; SUSignatureVerifier *v = [[SUSignatureVerifier alloc] initWithPublicKeys:pubKeys]; SUSignatures *sig = createSignatures(sigString, nil); return [v verifyFileAtPath:aFile signatures:sig verifierInformation:nil error:error]; } - (void)testVerifyFileWithBothKeys { NSString *dsaKey = [NSString stringWithContentsOfFile:_pubDSAKeyFile encoding:NSASCIIStringEncoding error:nil]; XCTAssertNotNil(dsaKey, @"Public key must be readable"); SUPublicKeys *pubKeys = [[SUPublicKeys alloc] initWithEd:_pubEdKey dsa:dsaKey]; SUSignatureVerifier *v = [[SUSignatureVerifier alloc] initWithPublicKeys:pubKeys]; NSError *error = nil; XCTAssertFalse([v verifyFileAtPath:_testFile signatures:createSignatures(nil, nil) verifierInformation:nil error:&error], @"Fail if no signatures are provided: %@", error); XCTAssertFalse([v verifyFileAtPath:_testFile signatures:createSignatures(@"lol", @"lol") verifierInformation:nil error:&error], @"Fail if both signatures are invalid: %@", error); NSString *dsaSig = @"MCwCFCIHCIYYkfZavNzTitTW5tlRp/k5AhQ40poFytqcVhIYdCxQznaXeJPJDQ=="; NSString *wrongDSASig = @"MCwCFCIHCiYYkfZavNzTitTW5tlRp/k5AhQ40poFytqcVhIYdCxQznaXeJPJDQ=="; NSString *edSig = @"EIawm2YkDZ2gBfkEMF2+1VuuTeXnCGZOdnMdVgPPvDZioq7bvDayXqKkIIzSjKMmeFdcFJOHdnba5ZV60+gPBw=="; NSString *wrongEdSig = @"wTcpXCgWoa4NrJpsfzS61FXJIbv963//12U2ef9xstzVOLPHYK2N4/ojgpDV5N1/NGG1uWMBgK+kEWp0Z5zMDQ=="; XCTAssertFalse([v verifyFileAtPath:_testFile signatures:createSignatures(nil, dsaSig) verifierInformation:nil error:&error], @"EdDSA signature must be present if app has EdDSA key: %@", error); XCTAssertTrue([v verifyFileAtPath:_testFile signatures:createSignatures(edSig, nil) verifierInformation:nil error:&error], @"Allow just an EdDSA signature if that's all that's available: %@", error); XCTAssertFalse([v verifyFileAtPath:_testFile signatures:createSignatures(wrongEdSig, dsaSig) verifierInformation:nil error:&error], @"Fail on a bad Ed25519 signature regardless: %@", error); XCTAssertTrue([v verifyFileAtPath:_testFile signatures:createSignatures(edSig, wrongDSASig) verifierInformation:nil error:&error], @"Allow bad DSA signature if EdDSA signature is good: %@", error); XCTAssertFalse([v verifyFileAtPath:_testFile signatures:createSignatures(@"lol", dsaSig) verifierInformation:nil error:&error], @"Fail if the Ed25519 signature is invalid: %@", error); #if SPARKLE_BUILD_LEGACY_DSA_SUPPORT XCTAssertFalse([v verifyFileAtPath:_testFile signatures:createSignatures(edSig, @"lol") verifierInformation:nil error:&error], @"Fail if invalid DSA signature is used even if EdDSA signature is good: %@", error); #else XCTAssertTrue([v verifyFileAtPath:_testFile signatures:createSignatures(edSig, @"lol") error:&error], @"Allow invalid DSA signature if EdDSA signature is good: %@", error); #endif XCTAssertTrue([v verifyFileAtPath:_testFile signatures:createSignatures(edSig, dsaSig) verifierInformation:nil error:&error], @"Pass if both are valid: %@", error); } - (void)testVerifyFileWithWrongKey { NSError *error = nil; NSString *dsaSig = @"MCwCFCIHCIYYkfZavNzTitTW5tlRp/k5AhQ40poFytqcVhIYdCxQznaXeJPJDQ=="; NSString *edSig = @"EIawm2YkDZ2gBfkEMF2+1VuuTeXnCGZOdnMdVgPPvDZioq7bvDayXqKkIIzSjKMmeFdcFJOHdnba5ZV60+gPBw=="; NSString *dsaKey = [NSString stringWithContentsOfFile:_pubDSAKeyFile encoding:NSASCIIStringEncoding error:nil]; XCTAssertNotNil(dsaKey, @"Public key must be readable"); SUPublicKeys *dsaOnlyKeys = [[SUPublicKeys alloc] initWithEd:nil dsa:dsaKey]; SUSignatureVerifier *dsaOnlyVerifier = [[SUSignatureVerifier alloc] initWithPublicKeys:dsaOnlyKeys]; XCTAssertFalse([dsaOnlyVerifier verifyFileAtPath:_testFile signatures:createSignatures(edSig, nil) verifierInformation:nil error:&error], @"DSA cannot verify an Ed signature: %@", error); SUPublicKeys *edOnlyKeys = [[SUPublicKeys alloc] initWithEd:_pubEdKey dsa:nil]; SUSignatureVerifier *edOnlyVerifier = [[SUSignatureVerifier alloc] initWithPublicKeys:edOnlyKeys]; { SUSignatures *signatures = createSignatures(nil, dsaSig); XCTAssertFalse([edOnlyVerifier verifyFileAtPath:_testFile signatures:signatures verifierInformation:nil error:&error], @"Ed cannot verify an DSA signature: %@", error); } } - (void)testValidatePath { NSString *dsaStr = [NSString stringWithContentsOfFile:_pubDSAKeyFile encoding:NSASCIIStringEncoding error:nil]; XCTAssertNotNil(dsaStr); SUPublicKeys *pubkeys = [[SUPublicKeys alloc] initWithEd:nil dsa:dsaStr]; XCTAssertNotNil(pubkeys); XCTAssertNotNil(pubkeys.dsaPubKey); SUSignatures *sig = createSignatures(nil, @"MC0CFFMF3ha5kjvrJ9JTpTR8BenPN9QUAhUAzY06JRdtP17MJewxhK0twhvbKIE="); XCTAssertNotNil(sig); #if SPARKLE_BUILD_LEGACY_DSA_SUPPORT XCTAssertNotNil(sig.dsaSignature); #endif NSError *error = nil; #if SPARKLE_BUILD_LEGACY_DSA_SUPPORT XCTAssertTrue([SUSignatureVerifier validatePath:_testFile withSignatures:sig withPublicKeys:pubkeys verifierInformation:nil error:&error], @"Expected valid signature: %@", error); #else XCTAssertFalse([SUSignatureVerifier validatePath:_testFile withSignatures:sig withPublicKeys:pubkeys verifierInformation:nil error:&error], @"Expected verification to fail due to disabling DSA: %@", error); #endif } @end ================================================ FILE: Tests/SUSpotlightImporterTest.swift ================================================ // // SUSpotlightImporterTest.swift // Sparkle // // Created by Mayur Pawashe on 8/27/16. // Copyright © 2016 Sparkle Project. All rights reserved. // import XCTest class SUSpotlightImporterTest: XCTestCase { func testUpdatingSpotlightBundles() { let fileManager = SUFileManager() let tempDirectoryURL = try! fileManager.makeTemporaryDirectoryAppropriate(forDirectoryURL: URL(fileURLWithPath: NSHomeDirectory())) let bundleDirectory = tempDirectoryURL.appendingPathComponent("bundle.app") try! fileManager.makeDirectory(at: bundleDirectory) let innerDirectory = bundleDirectory.appendingPathComponent("foo") try! fileManager.makeDirectory(at: innerDirectory) try! Data().write(to: (bundleDirectory.appendingPathComponent("bar")), options: .atomicWrite) let importerDirectory = innerDirectory.appendingPathComponent("baz.mdimporter") try! fileManager.makeDirectory(at: importerDirectory) try! fileManager.makeDirectory(at: innerDirectory.appendingPathComponent("flag")) try! Data().write(to: (importerDirectory.appendingPathComponent("file")), options: .atomicWrite) let oldFooDirectoryAttributes = try! FileManager.default.attributesOfItem(atPath: innerDirectory.path) let oldBarFileAttributes = try! FileManager.default.attributesOfItem(atPath: bundleDirectory.appendingPathComponent("bar").path) let oldImporterAttributes = try! FileManager.default.attributesOfItem(atPath: importerDirectory.path) let oldFlagAttributes = try! FileManager.default.attributesOfItem(atPath: innerDirectory.appendingPathComponent("flag").path) let oldFileInImporterAttributes = try! FileManager.default.attributesOfItem(atPath: importerDirectory.appendingPathComponent("file").path) sleep(1) // wait for clock to advance // Update spotlight bundles SUBinaryDeltaUnarchiver.updateSpotlightImporters(atBundlePath: bundleDirectory.path) let newFooDirectoryAttributes = try! FileManager.default.attributesOfItem(atPath: innerDirectory.path) XCTAssertEqual((newFooDirectoryAttributes[FileAttributeKey.modificationDate] as! Date).timeIntervalSince(oldFooDirectoryAttributes[FileAttributeKey.modificationDate] as! Date), 0) let newBarFileAttributes = try! FileManager.default.attributesOfItem(atPath: bundleDirectory.appendingPathComponent("bar").path) XCTAssertEqual((newBarFileAttributes[FileAttributeKey.modificationDate] as! Date).timeIntervalSince(oldBarFileAttributes[FileAttributeKey.modificationDate] as! Date), 0) let newImporterAttributes = try! FileManager.default.attributesOfItem(atPath: importerDirectory.path) XCTAssertGreaterThan((newImporterAttributes[FileAttributeKey.modificationDate] as! Date).timeIntervalSince(oldImporterAttributes[FileAttributeKey.modificationDate] as! Date), 0) let newFlagAttributes = try! FileManager.default.attributesOfItem(atPath: innerDirectory.appendingPathComponent("flag").path) XCTAssertEqual((newFlagAttributes[FileAttributeKey.modificationDate] as! Date).timeIntervalSince(oldFlagAttributes[FileAttributeKey.modificationDate] as! Date), 0) let newFileInImporterAttributes = try! FileManager.default.attributesOfItem(atPath: importerDirectory.appendingPathComponent("file").path) XCTAssertEqual((newFileInImporterAttributes[FileAttributeKey.modificationDate] as! Date).timeIntervalSince(oldFileInImporterAttributes[FileAttributeKey.modificationDate] as! Date), 0) try! fileManager.removeItem(at: tempDirectoryURL) } } ================================================ FILE: Tests/SUUnarchiverTest.swift ================================================ // // SUUnarchiverTest.swift // Sparkle // // Created by Mayur Pawashe on 9/4/15. // Copyright © 2015 Sparkle Project. All rights reserved. // import XCTest class SUUnarchiverTest: XCTestCase { func unarchiveTestAppWithExtension(_ archiveExtension: String, password: String? = nil, resourceName: String = "SparkleTestCodeSignApp", extractedAppName: String = "SparkleTestCodeSignApp", expectingInstallationType installationType: String = SPUInstallationTypeApplication, expectingSuccess: Bool = true) { let appName = resourceName let archiveResourceURL = Bundle(for: type(of: self)).url(forResource: appName, withExtension: archiveExtension)! let fileManager = FileManager.default // Do not remove this temporary directory // If we do want to clean up and remove it (which isn't necessary but nice), we'd have to remove it // after *both* our unarchive success and failure calls below finish (they both have async completion blocks inside their implementation) let tempDirectoryURL = try! fileManager.url(for: .itemReplacementDirectory, in: .userDomainMask, appropriateFor: URL(fileURLWithPath: NSHomeDirectory()), create: true) let unarchivedSuccessExpectation = super.expectation(description: "Unarchived Success (format: \(archiveExtension))") let unarchivedFailureExpectation = super.expectation(description: "Unarchived Failure (format: \(archiveExtension))") self.unarchiveTestAppWithExtension(archiveExtension, appName: appName, tempDirectoryURL: tempDirectoryURL, archiveResourceURL: archiveResourceURL, password: password, expectingInstallationType: installationType, expectingSuccess: expectingSuccess, testExpectation: unarchivedSuccessExpectation) self.unarchiveNonExistentFileTestFailureAppWithExtension(archiveExtension, tempDirectoryURL: tempDirectoryURL, password: password, expectingInstallationType: installationType, testExpectation: unarchivedFailureExpectation) super.waitForExpectations(timeout: 30.0, handler: nil) if expectingSuccess { if installationType == SPUInstallationTypeApplication { let extractedAppURL = tempDirectoryURL.appendingPathComponent(extractedAppName).appendingPathExtension("app") XCTAssertTrue(fileManager.fileExists(atPath: extractedAppURL.path)) XCTAssertEqual("6a60ab31430cfca8fb499a884f4a29f73e59b472", hashOfTreeWithVersion(extractedAppURL.path, 3)) XCTAssertEqual("52111bc200000000000000000000000000000000", hashOfTree(extractedAppURL.path)) } else if archiveExtension != "pkg" { let extractedPackageURL = tempDirectoryURL.appendingPathComponent(extractedAppName).appendingPathExtension("pkg") XCTAssertTrue(fileManager.fileExists(atPath: extractedPackageURL.path)) } } } func unarchiveNonExistentFileTestFailureAppWithExtension(_ archiveExtension: String, tempDirectoryURL: URL, password: String?, expectingInstallationType installationType: String, testExpectation: XCTestExpectation) { let tempArchiveURL = tempDirectoryURL.deletingLastPathComponent().appendingPathComponent("error-invalid").appendingPathExtension(archiveExtension) let unarchiver = SUUnarchiver.unarchiver(forPath: tempArchiveURL.path, extractionDirectory: tempDirectoryURL.path, updatingHostBundlePath: nil, decryptionPassword: password, expectingInstallationType: installationType)! unarchiver.unarchive(completionBlock: {(error: Error?) -> Void in XCTAssertNotNil(error) testExpectation.fulfill() }, progressBlock: nil, waitForCleanup: true) } // swiftlint:disable function_parameter_count func unarchiveTestAppWithExtension(_ archiveExtension: String, appName: String, tempDirectoryURL: URL, archiveResourceURL: URL, password: String?, expectingInstallationType installationType: String, expectingSuccess: Bool, testExpectation: XCTestExpectation) { let unarchiver = SUUnarchiver.unarchiver(forPath: archiveResourceURL.path, extractionDirectory: tempDirectoryURL.path, updatingHostBundlePath: nil, decryptionPassword: password, expectingInstallationType: installationType)! unarchiver.unarchive(completionBlock: {(error: Error?) -> Void in if expectingSuccess { XCTAssertNil(error) } else { XCTAssertNotNil(error) } testExpectation.fulfill() }, progressBlock: nil, waitForCleanup: true) } func testUnarchivingZip() { self.unarchiveTestAppWithExtension("zip") } // This zip file has extraneous zero bytes added at the very end func testUnarchivingBadZipWithExtaneousTrailingBytes() { // We may receive a SIGPIPE error when writing data to a pipe // The Autoupdate installer ignores SIGPIPE too // We need to ignore it otherwise the xctest will terminate unexpectedly with exit code 13 signal(SIGPIPE, SIG_IGN) self.unarchiveTestAppWithExtension("zip", resourceName: "SparkleTestCodeSignApp_bad_extraneous", extractedAppName: "SparkleTestCodeSignApp", expectingSuccess: false) signal(SIGPIPE, SIG_DFL) } func testUnarchivingBadZipWithMissingHeaderBytes() { // We may receive a SIGPIPE error when writing data to a pipe // The Autoupdate installer ignores SIGPIPE too // We need to ignore it otherwise the xctest will terminate unexpectedly with exit code 13 signal(SIGPIPE, SIG_IGN) self.unarchiveTestAppWithExtension("zip", resourceName: "SparkleTestCodeSignApp_bad_header", extractedAppName: "SparkleTestCodeSignApp", expectingSuccess: false) signal(SIGPIPE, SIG_DFL) } func testUnarchivingTarDotGz() { self.unarchiveTestAppWithExtension("tar.gz") } func testUnarchivingTar() { self.unarchiveTestAppWithExtension("tar") } func testUnarchivingTarDotBz2() { self.unarchiveTestAppWithExtension("tar.bz2") } func testUnarchivingTarDotXz() { self.unarchiveTestAppWithExtension("tar.xz") } func testUnarchivingHFSDmgWithLicenseAgreement() { self.unarchiveTestAppWithExtension("dmg") } func testUnarchivingEncryptedDmgWithLicenseAgreement() { self.unarchiveTestAppWithExtension("enc.dmg", password: "testpass") } func testUnarchivingEncryptedDmgWithoutLicenseAgreement() { self.unarchiveTestAppWithExtension("enc.nolicense.dmg", password: "testpass") } func testUnarchivingEncryptedDmgWithLicenseAndWithIncorrectPassword() { self.unarchiveTestAppWithExtension("enc.dmg", password: "moo", expectingSuccess: false) } func testUnarchivingEncryptedDmgWithLicenseAndWithoutPassword() { self.unarchiveTestAppWithExtension("enc.dmg", expectingSuccess: false) } func testUnarchivingEncryptedDmgWithoutLicenseAndWithIncorrectPassword() { self.unarchiveTestAppWithExtension("enc.nolicense.dmg", password: "moo", expectingSuccess: false) } func testUnarchivingEncryptedDmgWithoutLicenseAndWithoutPassword() { self.unarchiveTestAppWithExtension("enc.nolicense.dmg", expectingSuccess: false) } func testUnarchivingAPFSDMG() { self.unarchiveTestAppWithExtension("dmg", resourceName: "SparkleTestCodeSign_apfs") } func testUnarchivingAPFSDMGWithBogusPassword() { self.unarchiveTestAppWithExtension("dmg", password: "moo", resourceName: "SparkleTestCodeSign_apfs") } func testUnarchivingAPFSAdhocSignedDMGWithAuxFiles() { self.unarchiveTestAppWithExtension("dmg", resourceName: "SparkleTestCodeSign_apfs_lzma_aux_files_adhoc") } func testUnarchivingAPFSDMGWithPackage() { self.unarchiveTestAppWithExtension("dmg", resourceName: "SparkleTestCodeSign_pkg", expectingInstallationType: SPUInstallationTypeGuidedPackage) } #if SPARKLE_BUILD_PACKAGE_SUPPORT func testUnarchivingBarePackage() { self.unarchiveTestAppWithExtension("pkg", resourceName: "test", expectingInstallationType: SPUInstallationTypeGuidedPackage) self.unarchiveTestAppWithExtension("pkg", resourceName: "test", expectingInstallationType: SPUInstallationTypeInteractivePackage, expectingSuccess: false) self.unarchiveTestAppWithExtension("pkg", resourceName: "test", expectingInstallationType: SPUInstallationTypeApplication, expectingSuccess: false) } #endif func testUnarchivingAppleArchive() { self.unarchiveTestAppWithExtension("aar", resourceName: "SparkleTestCodeSignApp") } // If we support encrypted archives one day we will use "aea" file extension // Password to this archive is whatisgoingonforeveroneday! func testUnarchivingEncryptedAppleArchiveWithoutPassword() { signal(SIGPIPE, SIG_IGN) self.unarchiveTestAppWithExtension("enc.aar", resourceName: "SparkleTestCodeSignApp", expectingSuccess: false) signal(SIGPIPE, SIG_DFL) } } ================================================ FILE: Tests/SUUpdateValidatorTest.swift ================================================ // // SUUpdateValidatorTest.swift // Sparkle // // Created by Jordan Rose on 2020-06-13. // Copyright © 2020 Sparkle Project. All rights reserved. // import Foundation import XCTest class SUUpdateValidatorTest: XCTestCase { enum BundleConfig: String, CaseIterable, Equatable { case none = "None" case dsaOnly = "DSAOnly" case edOnly = "EDOnly" case both = "Both" case codeSignedOnly = "CodeSignedOnly" case codeSignedBoth = "CodeSignedBoth" case codeSignedOnlyNew = "CodeSignedOnlyNew" case codeSignedBothNew = "CodeSignedBothNew" case codeSignedOldED = "CodeSignedOldED" case codeSignedInvalidOnly = "CodeSignedInvalidOnly" case codeSignedInvalid = "CodeSignedInvalid" var hasAnyKeys: Bool { switch self { case .none, .codeSignedOnly, .codeSignedOnlyNew, .codeSignedInvalidOnly: return false case .edOnly, .both, .codeSignedBoth, .codeSignedBothNew, .codeSignedOldED, .codeSignedInvalid: return true case .dsaOnly: return true } } var isValidCodeSigned: Bool { switch self { case .codeSignedOnly, .codeSignedOnlyNew, .codeSignedBothNew, .codeSignedOldED, .codeSignedBoth: return true case .none, .dsaOnly, .edOnly, .both, .codeSignedInvalid, .codeSignedInvalidOnly: return false } } } struct SignatureConfig: CaseIterable, Equatable, CustomDebugStringConvertible { enum State: CaseIterable, Equatable { case none, invalid, invalidFormat, valid } var ed: State #if SPARKLE_BUILD_LEGACY_DSA_SUPPORT var dsa: State #endif static let allCases: [SignatureConfig] = State.allCases.flatMap { dsaState in State.allCases.map { edState in #if SPARKLE_BUILD_LEGACY_DSA_SUPPORT SignatureConfig(ed: edState, dsa: dsaState) #else SignatureConfig(ed: edState) #endif } } var debugDescription: String { #if SPARKLE_BUILD_LEGACY_DSA_SUPPORT return "(ed: \(self.ed), dsa: \(self.dsa))" #else return "(ed: \(self.ed))" #endif } } func bundle(_ config: BundleConfig) -> Bundle { let testBundle = Bundle(for: SUUpdateValidatorTest.self) let configBundleURL = testBundle.url(forResource: config.rawValue, withExtension: "bundle", subdirectory: "SUUpdateValidatorTest")! return Bundle(url: configBundleURL)! } func signatures(_ config: SignatureConfig) -> SUSignatures { #if SPARKLE_BUILD_LEGACY_DSA_SUPPORT let dsaSig: String? switch config.dsa { case .none: dsaSig = nil case .invalid: dsaSig = "ABwCFCIHCIYYkfZavNzTitTW5tlRp/k5AhQ40poFytqcVhIYdCxQznaXeJPJDQ==" // Use some invalid base64 strings case .invalidFormat: dsaSig = "%%wCFCIHCIYYkfZavNzTitTW5tlRp/k5AhQ40poFytqcVhIYdCxQznaXeJPJDQ==" case .valid: dsaSig = "MCwCFCIHCIYYkfZavNzTitTW5tlRp/k5AhQ40poFytqcVhIYdCxQznaXeJPJDQ==" } #endif let edSig: String? switch config.ed { case .none: edSig = nil case .invalid: edSig = "wTcpXCgWoa4NrJpsfzS61FXJIbv963//12U2ef9xstzVOLPHYK2N4/ojgpDV5N1/NGG1uWMBgK+kEWp0Z5zMDQ==" // Use some invalid base64 strings case .invalidFormat: edSig = "%%cpXCgWoa4NrJpsfzS61FXJIbv963//12U2ef9xstzVOLPHYK2N4/ojgpDV5N1/NGG1uWMBgK+kEWp0Z5zMDQ==" case .valid: edSig = "EIawm2YkDZ2gBfkEMF2+1VuuTeXnCGZOdnMdVgPPvDZioq7bvDayXqKkIIzSjKMmeFdcFJOHdnba5ZV60+gPBw==" } #if SPARKLE_BUILD_LEGACY_DSA_SUPPORT return SUSignatures(ed: edSig, dsa: dsaSig) #else return SUSignatures(ed: edSig) #endif } var signedTestFilePath: String { let testBundle = Bundle(for: SUUpdateValidatorTest.self) return testBundle.path(forResource: "signed-test-file", ofType: "txt")! } func testPrevalidation(bundle bundleConfig: BundleConfig, signatures signatureConfig: SignatureConfig, expectedResult: Bool, line: UInt = #line) { let host = SUHost(bundle: self.bundle(bundleConfig)) let signatures = self.signatures(signatureConfig) let validator = SUUpdateValidator(downloadPath: self.signedTestFilePath, signatures: signatures, host: host, verifierInformation: nil) let result = (try? validator.validateHostHasPublicKeys()) != nil && (try? validator.validateDownloadPathWithFallback(onCodeSigning: false)) != nil XCTAssertEqual(result, expectedResult, "bundle: \(bundleConfig), signatures: \(signatureConfig)", line: line) } func testPrevalidation() { for signatureConfig in SignatureConfig.allCases { testPrevalidation(bundle: .none, signatures: signatureConfig, expectedResult: false) #if SPARKLE_BUILD_LEGACY_DSA_SUPPORT testPrevalidation(bundle: .dsaOnly, signatures: signatureConfig, expectedResult: signatureConfig.dsa == .valid) #endif #if SPARKLE_BUILD_LEGACY_DSA_SUPPORT testPrevalidation(bundle: .edOnly, signatures: signatureConfig, expectedResult: signatureConfig.ed == .valid && signatureConfig.dsa != .invalidFormat) #else testPrevalidation(bundle: .edOnly, signatures: signatureConfig, expectedResult: signatureConfig.ed == .valid) #endif #if SPARKLE_BUILD_LEGACY_DSA_SUPPORT testPrevalidation(bundle: .both, signatures: signatureConfig, expectedResult: signatureConfig.ed == .valid && signatureConfig.dsa != .invalidFormat) #else testPrevalidation(bundle: .both, signatures: signatureConfig, expectedResult: signatureConfig.ed == .valid) #endif } } func testPostValidation(oldBundle oldBundleConfig: BundleConfig, newBundle newBundleConfig: BundleConfig, signatures signatureConfig: SignatureConfig, expectedResult: Bool, line: UInt = #line) { let oldBundle = self.bundle(oldBundleConfig) let host = SUHost(bundle: oldBundle) let signatures = self.signatures(signatureConfig) let validator = SUUpdateValidator(downloadPath: self.signedTestFilePath, signatures: signatures, host: host, verifierInformation: nil) let updateDirectory = temporaryDirectory("SUUpdateValidatorTest")! defer { try! FileManager.default.removeItem(atPath: updateDirectory) } let newBundle = self.bundle(newBundleConfig) try! FileManager.default.copyItem(at: newBundle.bundleURL, to: URL(fileURLWithPath: updateDirectory).appendingPathComponent(oldBundle.bundleURL.lastPathComponent)) let result = (try? validator.validate(withUpdateDirectory: updateDirectory)) != nil XCTAssertEqual(result, expectedResult, "oldBundle: \(oldBundleConfig), newBundle: \(newBundleConfig), signatures: \(signatureConfig)", line: line) } func testPostValidation(bundle bundleConfig: BundleConfig, signatures signatureConfig: SignatureConfig, expectedResult: Bool, line: UInt = #line) { testPostValidation(oldBundle: bundleConfig, newBundle: bundleConfig, signatures: signatureConfig, expectedResult: expectedResult, line: line) } func testPostValidationWithoutCodeSigning() { for signatureConfig in SignatureConfig.allCases { testPostValidation(bundle: .none, signatures: signatureConfig, expectedResult: false) #if SPARKLE_BUILD_LEGACY_DSA_SUPPORT testPostValidation(bundle: .dsaOnly, signatures: signatureConfig, expectedResult: signatureConfig.dsa == .valid) #endif #if SPARKLE_BUILD_LEGACY_DSA_SUPPORT testPostValidation(bundle: .edOnly, signatures: signatureConfig, expectedResult: signatureConfig.ed == .valid && signatureConfig.dsa != .invalidFormat) #else testPostValidation(bundle: .edOnly, signatures: signatureConfig, expectedResult: signatureConfig.ed == .valid) #endif #if SPARKLE_BUILD_LEGACY_DSA_SUPPORT testPostValidation(bundle: .both, signatures: signatureConfig, expectedResult: signatureConfig.ed == .valid && signatureConfig.dsa != .invalidFormat) #else testPostValidation(bundle: .both, signatures: signatureConfig, expectedResult: signatureConfig.ed == .valid) #endif } } func testPostValidationWithCodeSigning() { for signatureConfig in SignatureConfig.allCases { testPostValidation(bundle: .codeSignedOnly, signatures: signatureConfig, expectedResult: true) #if SPARKLE_BUILD_LEGACY_DSA_SUPPORT testPostValidation(bundle: .codeSignedBoth, signatures: signatureConfig, expectedResult: signatureConfig.ed == .valid && signatureConfig.dsa != .invalidFormat) #else testPostValidation(bundle: .codeSignedBoth, signatures: signatureConfig, expectedResult: signatureConfig.ed == .valid) #endif testPostValidation(bundle: .codeSignedInvalidOnly, signatures: signatureConfig, expectedResult: false) testPostValidation(bundle: .codeSignedInvalid, signatures: signatureConfig, expectedResult: false) } } func testPostValidationWithKeyRemoval() { for bundleConfig in BundleConfig.allCases { #if SPARKLE_BUILD_LEGACY_DSA_SUPPORT testPostValidation(oldBundle: .dsaOnly, newBundle: bundleConfig, signatures: SignatureConfig(ed: .valid, dsa: .valid), expectedResult: bundleConfig.hasAnyKeys && bundleConfig != .codeSignedInvalid) #else testPostValidation(oldBundle: .dsaOnly, newBundle: bundleConfig, signatures: SignatureConfig(ed: .valid), expectedResult: false) #endif do { #if SPARKLE_BUILD_LEGACY_DSA_SUPPORT let signatureConfig = SignatureConfig(ed: .valid, dsa: .valid) #else let signatureConfig = SignatureConfig(ed: .valid) #endif testPostValidation(oldBundle: .edOnly, newBundle: bundleConfig, signatures: signatureConfig, expectedResult: bundleConfig.hasAnyKeys && bundleConfig != .codeSignedInvalid) } #if SPARKLE_BUILD_LEGACY_DSA_SUPPORT testPostValidation(oldBundle: .both, newBundle: bundleConfig, signatures: SignatureConfig(ed: .valid, dsa: .valid), expectedResult: bundleConfig.hasAnyKeys && bundleConfig != .codeSignedInvalid) #else testPostValidation(oldBundle: .both, newBundle: bundleConfig, signatures: SignatureConfig(ed: .valid), expectedResult: bundleConfig.hasAnyKeys && bundleConfig != .codeSignedInvalid) #endif do { #if SPARKLE_BUILD_LEGACY_DSA_SUPPORT let signatureConfig = SignatureConfig(ed: .valid, dsa: .valid) #else let signatureConfig = SignatureConfig(ed: .valid) #endif testPostValidation(oldBundle: .codeSignedBoth, newBundle: bundleConfig, signatures: signatureConfig, expectedResult: bundleConfig.hasAnyKeys && bundleConfig.isValidCodeSigned) } } } func testPostValidationWithKeyRotation() { for signatureConfig in SignatureConfig.allCases { #if SPARKLE_BUILD_LEGACY_DSA_SUPPORT let signatureIsValid = (signatureConfig.ed == .valid && signatureConfig.dsa != .invalidFormat) #else let signatureIsValid = (signatureConfig.ed == .valid) #endif // It's okay to add DSA keys or add code signing. testPostValidation(oldBundle: .codeSignedOnly, newBundle: .codeSignedBoth, signatures: signatureConfig, expectedResult: signatureIsValid) testPostValidation(oldBundle: .both, newBundle: .codeSignedBoth, signatures: signatureConfig, expectedResult: signatureIsValid) // If you want to change your code signing, you have to be using both forms of auth. testPostValidation(oldBundle: .codeSignedOnly, newBundle: .codeSignedOnlyNew, signatures: signatureConfig, expectedResult: false) testPostValidation(oldBundle: .codeSignedBoth, newBundle: .codeSignedOnlyNew, signatures: signatureConfig, expectedResult: false) testPostValidation(oldBundle: .codeSignedOnly, newBundle: .codeSignedBothNew, signatures: signatureConfig, expectedResult: false) testPostValidation(oldBundle: .codeSignedBoth, newBundle: .codeSignedBothNew, signatures: signatureConfig, expectedResult: signatureIsValid) // If you want to change your keys, you have to be using both forms of auth. testPostValidation(oldBundle: .codeSignedOldED, newBundle: .codeSignedOnly, signatures: signatureConfig, expectedResult: false) testPostValidation(oldBundle: .codeSignedOldED, newBundle: .codeSignedBoth, signatures: signatureConfig, expectedResult: signatureIsValid) // You can't change two things at once. testPostValidation(oldBundle: .codeSignedOldED, newBundle: .codeSignedBothNew, signatures: signatureConfig, expectedResult: false) // You can't remove code signing. testPostValidation(oldBundle: .codeSignedBoth, newBundle: .both, signatures: signatureConfig, expectedResult: false) } } } ================================================ FILE: Tests/SUUpdaterTest.m ================================================ // // SUUpdaterTest.m // Sparkle // // Created by Jake Petroules on 2014-06-29. // Copyright (c) 2014 Sparkle Project. All rights reserved. // #import #import "SUConstants.h" #import "SPUUpdater.h" #import "SPUStandardUserDriver.h" #import "SPUUpdaterDelegate.h" @interface SUUpdaterTest : XCTestCase @end @implementation SUUpdaterTest { SPUUpdater *_updater; } - (void)setUp { [super setUp]; NSBundle *bundle = [NSBundle bundleForClass:[self class]]; #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wnonnull" // We really want a useless / not really functional user driver so we will pass nil here // For real world applications we should pass a valid user driver which is why this is not a nullable parameter _updater = [[SPUUpdater alloc] initWithHostBundle:bundle applicationBundle:bundle userDriver:nil delegate:self]; #pragma clang diagnostic pop NSError *error = nil; if (![_updater startUpdater:&error]) { NSLog(@"Updater error: %@", error); abort(); } [_updater clearFeedURLFromUserDefaults]; } - (void)tearDown { _updater = nil; [super tearDown]; } - (NSString *)feedURLStringForUpdater:(id)__unused updater { return @"https://test.example.com"; } - (void)testFeedURL { [_updater feedURL]; // this WON'T throw } - (void)testSetTestFeedURL { NSURL *emptyURL = [NSURL URLWithString:@""]; XCTAssertNotNil(emptyURL); #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdeprecated-declarations" [_updater setFeedURL:emptyURL]; // this WON'T throw #pragma clang diagnostic pop } @end ================================================ FILE: Tests/SUVersionComparisonTest.m ================================================ // // SUVersionComparisonTest.m // Sparkle // // Created by Andy Matuschak on 4/15/08. // Copyright 2008 Andy Matuschak. All rights reserved. // #import "SUStandardVersionComparator.h" #import @interface SUVersionComparisonTestCase : XCTestCase { } @end @implementation SUVersionComparisonTestCase #define SUAssertOrder(comparator, a, b, c) XCTAssertTrue([comparator compareVersion:a toVersion:b] == c, @"b should be newer than a!") #define SUAssertAscending(comparator, a, b) SUAssertOrder(comparator, a, b, NSOrderedAscending) #define SUAssertDescending(comparator, a, b) SUAssertOrder(comparator, a, b, NSOrderedDescending) #define SUAssertEqual(comparator, a, b) SUAssertOrder(comparator, a, b, NSOrderedSame) - (void)testNumbers { SUStandardVersionComparator *comparator = [[SUStandardVersionComparator alloc] init]; SUAssertAscending(comparator, @"1.0", @"1.1"); SUAssertEqual(comparator, @"1.0", @"1.0"); SUAssertDescending(comparator, @"2.0", @"1.1"); SUAssertDescending(comparator, @"0.1", @"0.0.1"); //SUAssertDescending(comparator, @".1", @"0.0.1"); Known bug, but I'm not sure I care. SUAssertAscending(comparator, @"0.1", @"0.1.2"); SUAssertEqual(comparator, @"1.0", @"1.0.0"); SUAssertEqual(comparator, @"1.0.0", @"1.0"); } - (void)testCommitSHAs { SUStandardVersionComparator *comparator = [[SUStandardVersionComparator alloc] init]; SUAssertAscending(comparator, @"1.5.5-335d3e2", @"1.5.6-b252311"); SUAssertEqual(comparator, @"1.5.5-335d3e2", @"1.5.5-a655360"); SUAssertDescending(comparator, @"1.5.6-b252311", @"1.5.5-335d3e2"); SUAssertEqual(comparator, @"1.5-335d3e2", @"1.5.0-335d3e2"); SUAssertEqual(comparator, @"1.5.0-335d3e2", @"1.5-335d3e2"); } - (void)testPrereleases { SUStandardVersionComparator *comparator = [[SUStandardVersionComparator alloc] init]; SUAssertAscending(comparator, @"1.5.5", @"1.5.6a1"); SUAssertAscending(comparator, @"1.1.0b1", @"1.1.0b2"); SUAssertAscending(comparator, @"1.1.1b2", @"1.1.2b1"); SUAssertAscending(comparator, @"1.1.1b2", @"1.1.2a1"); SUAssertAscending(comparator, @"1.0a1", @"1.0b1"); SUAssertAscending(comparator, @"1.0b1", @"1.0"); SUAssertAscending(comparator, @"0.9", @"1.0a1"); SUAssertAscending(comparator, @"1.0b", @"1.0b2"); SUAssertAscending(comparator, @"1.0b10", @"1.0b11"); SUAssertAscending(comparator, @"1.0b9", @"1.0b10"); SUAssertAscending(comparator, @"1.0rc", @"1.0"); SUAssertAscending(comparator, @"1.0b", @"1.0"); SUAssertAscending(comparator, @"1.0pre1", @"1.0"); SUAssertEqual(comparator, @"1.0pre1", @"1.0.0pre1"); SUAssertEqual(comparator, @"1.0.0pre1", @"1.0pre1"); } - (void)testVersionsWithBuildNumbers { SUStandardVersionComparator *comparator = [[SUStandardVersionComparator alloc] init]; SUAssertAscending(comparator, @"1.0 (1234)", @"1.0 (1235)"); SUAssertAscending(comparator, @"1.0b1 (1234)", @"1.0 (1234)"); SUAssertAscending(comparator, @"1.0b5 (1234)", @"1.0b5 (1235)"); SUAssertAscending(comparator, @"1.0b5 (1234)", @"1.0.1b5 (1234)"); SUAssertAscending(comparator, @"1.0.1b5 (1234)", @"1.0.1b6 (1234)"); SUAssertAscending(comparator, @"2.0.0.2429", @"2.0.0.2430"); SUAssertAscending(comparator, @"1.1.1.1818", @"2.0.0.2430"); SUAssertAscending(comparator, @"3.3 (5847)", @"3.3.1b1 (5902)"); SUAssertEqual(comparator, @"3.3 (5847)", @"3.3.0 (5847)"); SUAssertEqual(comparator, @"3.3.0 (5847)", @"3.3 (5847)"); SUAssertEqual(comparator, @"1.1.1.1818", @"1.1.1.1818.0"); SUAssertEqual(comparator, @"1.1.1.1818.0", @"1.1.1.1818"); SUAssertEqual(comparator, @"3.3b1 (5902)", @"3.3.0b1 (5902)"); SUAssertEqual(comparator, @"3.3.0b1 (5902)", @"3.3b1 (5902)"); } - (void)testWordsWithSpaceInFront { // SUStandardVersionComparator *comparator = [[SUStandardVersionComparator alloc] init]; // SUAssertAscending(comparator, @"1.0 beta", @"1.0"); // SUAssertAscending(comparator, @"1.0 - beta", @"1.0"); // SUAssertAscending(comparator, @"1.0 alpha", @"1.0 beta"); // SUAssertEqual(comparator, @"1.0 - beta", @"1.0beta"); // SUAssertEqual(comparator, @"1.0 - beta", @"1.0 beta"); } - (void)testVersionsWithReverseDateBasedNumbers { SUStandardVersionComparator *comparator = [[SUStandardVersionComparator alloc] init]; SUAssertAscending(comparator, @"201210251627", @"201211051041"); SUAssertEqual(comparator, @"201210251627.0", @"201210251627"); SUAssertEqual(comparator, @"201210251627", @"201210251627.0"); } @end ================================================ FILE: Tests/Sparkle Unit Tests-Bridging-Header.h ================================================ // // Use this file to import your target's public headers that you would like to expose to Swift. // #import "SUUnarchiver.h" #import "SUUnarchiverProtocol.h" #import "SUBinaryDeltaUnarchiver.h" #import "SUPipedUnarchiver.h" #import "SUBinaryDeltaCommon.h" #import "SUFileManager.h" #import "SUExport.h" #import "SUAppcast.h" #import "SUAppcast+Private.h" #import "SUAppcastItem.h" #import "SUAppcastDriver.h" #import "SUVersionComparisonProtocol.h" #import "SUStandardVersionComparator.h" #import "SUUpdateValidator.h" #import "SPUVerifierInformation.h" #import "SUHost.h" #import "SPUSkippedUpdate.h" #import "SUSignatures.h" #import "SPUInstallationType.h" #import "SPUAppcastItemStateResolver.h" #import "SPUExtractSignedFeed.h" #import "SUSignatureVerifier.h" #import "ed25519.h" NS_ASSUME_NONNULL_BEGIN // Duplicated to avoid exporting a private symbol from SUFileManager static const char *SUAppleQuarantineIdentifier = "com.apple.quarantine"; @interface SUFileManager (Private) - (BOOL)_itemExistsAtURL:(NSURL *)fileURL; - (BOOL)_itemExistsAtURL:(NSURL *)fileURL isDirectory:(nullable BOOL *)isDirectory; @end @interface SUAppcastDriver (Private) + (SUAppcastItem *)bestItemFromAppcastItems:(NSArray *)appcastItems getDeltaItem:(SUAppcastItem *_Nullable __autoreleasing *_Nullable)deltaItem withHostVersion:(NSString *)hostVersion comparator:(id)comparator; + (SUAppcast *)filterSupportedAppcast:(SUAppcast *)appcast phasedUpdateGroup:(NSNumber * _Nullable)phasedUpdateGroup skippedUpdate:(SPUSkippedUpdate * _Nullable)skippedUpdate currentDate:(NSDate *)currentDate hostVersion:(NSString *)hostVersion versionComparator:(id)versionComparator testMinimumSystemRequirements:(BOOL)testMinimumSystemRequirements testMinimumAutoupdateVersion:(BOOL)testMinimumAutoupdateVersion; + (SUAppcast *)filterAppcast:(SUAppcast *)appcast forMacOSAndAllowedChannels:(NSSet *)allowedChannels; @end @interface SUBinaryDeltaUnarchiver (Private) + (void)updateSpotlightImportersAtBundlePath:(NSString *)targetPath; @end NS_ASSUME_NONNULL_END ================================================ FILE: Tests/SparkleTests-Info.plist ================================================ CFBundleDevelopmentRegion English CFBundleExecutable ${EXECUTABLE_NAME} CFBundleIdentifier $(PRODUCT_BUNDLE_IDENTIFIER) CFBundleInfoDictionaryVersion 6.0 CFBundlePackageType BNDL CFBundleSignature ???? CFBundleVersion 1.0 SUEnableAutomaticChecks SUEnableSystemProfiling SUPublicDSAKeyFile test-pubkey.pem ================================================ FILE: UITests/.swiftlint.yml ================================================ # Inherits from top level config disabled_rules: - force_try - force_cast ================================================ FILE: UITests/SUTestApplicationTest.swift ================================================ // // SUTestApplicationTest.swift // Sparkle // // Created by Mayur Pawashe on 8/27/15. // Copyright © 2015 Sparkle Project. All rights reserved. // import XCTest // The debugger may catch the Test app receiving a SIGTERM signal when Sparkle quits the app before installing the new one // So you may have better luck running these tests from the command line without the debugger attached: // xcodebuild -scheme UITests -configuration Debug test class SUTestApplicationTest: XCTestCase { // TODO: don't hardcode bundle ID? let TEST_APP_BUNDLE_ID = "org.sparkle-project.SparkleTestApp" func runningTestApplication() -> NSRunningApplication { let runningApplications = NSRunningApplication.runningApplications(withBundleIdentifier: TEST_APP_BUNDLE_ID) XCTAssertEqual(runningApplications.count, 1, "More than one or zero running instances of the Test Application are found") return runningApplications[0] } func runTestApplication(testMode: String, automatic: Bool, expectedFinalVersion: String, launchSleep: UInt32, extractSleep: UInt32) { let app = XCUIApplication() app.launchArguments = [ "-SUHasLaunchedBefore", automatic ? "YES" : "NO", "-SUEnableAutomaticChecks", automatic ? "YES" : "NO", "-SUAutomaticallyUpdate", automatic ? "YES" : "NO", "-SUScheduledCheckInterval", "60" ] app.launchEnvironment = ["TEST_MODE": testMode] app.launch() XCTAssertFalse(app.dialogs["alert"].staticTexts["Update succeeded!"].exists, "Update is already installed; please do a clean build") let initialRunningApplication = runningTestApplication() let bundleURL = initialRunningApplication.bundleURL! // Give some time for the Test App to initialize its web server, create an update, and start its updater sleep(launchSleep) let menuBarsQuery = app.menuBars menuBarsQuery.menuBarItems["Sparkle Test App"].click() let checkForUpdatesMenuItem = menuBarsQuery.menuItems["Check for Updates…"] if checkForUpdatesMenuItem.isEnabled { checkForUpdatesMenuItem.click() // Give some time to wait for window to show up sleep(5) } else { // We haven't checked for updates in a while so an automatic check was already done // in this case click the main menu again to deactivate it menuBarsQuery.menuBarItems["Sparkle Test App"].click() } if !automatic { app.windows["SUUpdateAlert"].buttons["SPUUserUpdateChoiceInstall"].click() // Give some time for the update to finish downloading / extracting sleep(extractSleep) app.windows["SUStatus"].buttons["SUStatusInstallAndRelaunch"].click() } else { XCTAssertTrue(app.windows["SUUpdateAlert"].buttons["SPUUserUpdateChoiceInstall"].exists) // The app should install automatically on termination app.terminate() } // Wait for the new updated app to finish launching so we can test if it's the frontmost app sleep(10) // From now on, do not rely on XCUIApplication as it only works properly when the XCUITest framework launches the app. // Our new updated app should be launched now. Test if it's the active app and the old app is terminated. // We used to run into timing issues where the updated app sometimes may not show up as the frontmost one XCTAssertTrue(initialRunningApplication.isTerminated) // Verify the new bundle version let infoCFDictionary = CFBundleCopyInfoDictionaryInDirectory(bundleURL as CFURL) let infoDictionary = infoCFDictionary! as Dictionary let updatedVersion = infoDictionary[kCFBundleVersionKey] as! String XCTAssertEqual(updatedVersion, expectedFinalVersion) // Clean up if !automatic { let newRunningApplication = self.runningTestApplication() XCTAssertTrue(newRunningApplication.isActive) newRunningApplication.forceTerminate() } sleep(10) } func test1RegularUpdate() { runTestApplication(testMode: "REGULAR", automatic: false, expectedFinalVersion: "2.0", launchSleep: 60, extractSleep: 30) } func test2DeltaUpdate() { runTestApplication(testMode: "DELTA_AND_MARKDOWN", automatic: false, expectedFinalVersion: "2.1", launchSleep: 75, extractSleep: 45) } func test3AutomaticUpdate() { runTestApplication(testMode: "AUTOMATIC", automatic: true, expectedFinalVersion: "2.2", launchSleep: 75, extractSleep: 30) } } ================================================ FILE: UITests/UITests-Info.plist ================================================ CFBundleDevelopmentRegion en CFBundleExecutable $(EXECUTABLE_NAME) CFBundleIdentifier $(PRODUCT_BUNDLE_IDENTIFIER) CFBundleInfoDictionaryVersion 6.0 CFBundleName $(PRODUCT_NAME) CFBundlePackageType BNDL CFBundleShortVersionString 1.0 CFBundleSignature ???? CFBundleVersion 1 ================================================ FILE: Vendor/bsdiff/bscommon.c ================================================ /* * bscommon.c * Sparkle * * Created by Mayur Pawashe on 5/16/16. */ #include "bscommon.h" #include u_char *readfile(const char *filename, off_t *outSize) { int success = -1; u_char *buffer = NULL; long offset = 0; size_t size = 0; FILE *file = fopen(filename, "r"); if (file == NULL) { goto cleanup; } if (fseek(file, 0L, SEEK_END) != 0) { goto cleanup; } offset = ftell(file); if (offset == -1) { goto cleanup; } size = (size_t)offset; if (outSize != NULL) { *outSize = (off_t)size; } /* Allocate size + 1 bytes instead of newsize bytes to ensure that we never try to malloc(0) and get a NULL pointer */ buffer = (u_char *)malloc(size + 1); if (buffer == NULL) { goto cleanup; } if (fseek(file, 0L, SEEK_SET) != 0) { goto cleanup; } if (fread(buffer, 1, size, file) < size) { goto cleanup; } success = 0; cleanup: if (success == -1) { free(buffer); buffer = NULL; } if (file != NULL) { fclose(file); } return buffer; } ================================================ FILE: Vendor/bsdiff/bscommon.h ================================================ /* * bscommon.h * Sparkle * * Created by Mayur Pawashe on 5/16/16. */ #ifndef BS_COMMON_H #define BS_COMMON_H #include #include u_char *readfile(const char *filename, off_t *outSize); #endif ================================================ FILE: Vendor/bsdiff/bsdiff.c ================================================ /*- * Copyright 2003 - 2005 Colin Percival * All rights reserved * * Redistribution and use in source and binary forms, with or without * modification, are permitted providing that the following conditions * are met: * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING * IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE. */ #if 0 __FBSDID("$FreeBSD: src/usr.bin/bsdiff/bsdiff/bsdiff.c, v 1.1 2005/08/06 01:59:05 cperciva Exp $"); #endif #include #include "sais.h" #include #include #include #include #include #include #include "bscommon.h" #define MIN(x, y) (((x)<(y)) ? (x) : (y)) /* matchlen(old, oldsize, new, newsize) * * Returns the length of the longest common prefix between 'old' and 'new'. */ static off_t matchlen(u_char *old, off_t oldsize, u_char *newp, off_t newsize) { off_t i; for (i = 0; (i < oldsize) && (i < newsize); i++) { if (old[i] != newp[i]) break; } return i; } /* search(I, old, oldsize, new, newsize, st, en, pos) * * Searches for the longest prefix of 'new' that occurs in 'old', stores its * offset in '*pos', and returns its length. 'I' should be the suffix sort of * 'old', and 'st' and 'en' are the lowest and highest indices in the suffix * sort to consider. If you're searching all suffixes, 'st = 0' and 'en = * oldsize - 1'. */ static off_t search(off_t *I, u_char *old, off_t oldsize, u_char *newp, off_t newsize, off_t st, off_t en, off_t *pos) { off_t x, y; if (en - st < 2) { x = matchlen(old + I[st], oldsize - I[st], newp, newsize); y = matchlen(old + I[en], oldsize - I[en], newp, newsize); if (x > y) { *pos = I[st]; return x; } else { *pos = I[en]; return y; } } x = st + (en - st)/2; /* Modification ported from ChromiumOS project: * https://chromium.googlesource.com/chromiumos/third_party/bsdiff/+/58146f74abd6b1b69693943195f37f4ac6a6acef%5E%21/#F0 */ if (memcmp(old + I[x], newp, (size_t)(MIN(oldsize - I[x], newsize))) <= 0) { return search(I, old, oldsize, newp, newsize, x, en, pos); } else { return search(I, old, oldsize, newp, newsize, st, x, pos); } } /* offtout(x, buf) * * Writes the off_t 'x' portably to the array 'buf'. */ static void offtout(off_t x, u_char *buf) { off_t y; if (x < 0) y = -x; else y = x; buf[0] = (u_char)(y % 256); y -= buf[0]; y = y/256; buf[1] = (u_char)(y%256); y -= buf[1]; y = y/256; buf[2] = (u_char)(y%256); y -= buf[2]; y = y/256; buf[3] = (u_char)(y%256); y -= buf[3]; y = y/256; buf[4] = (u_char)(y%256); y -= buf[4]; y = y/256; buf[5] = (u_char)(y%256); y -= buf[5]; y = y/256; buf[6] = (u_char)(y%256); y -= buf[6]; y = y/256; buf[7] = (u_char)(y%256); if (x < 0) buf[7] |= 0x80; } int bsdiff(int argc, char *argv[]); // Added by AMM: suppresses a warning about the following not having a prototype. int bsdiff(int argc, char *argv[]) { u_char *old = NULL,*newp = NULL; /* contents of old, new files */ off_t oldsize = 0, newsize = 0; /* length of old, new files */ off_t *I = NULL,*V = NULL; /* arrays used for suffix sort; I is ordering */ off_t scan = 0; /* position of current match in old file */ off_t pos = 0; /* position of current match in new file */ off_t len = 0; /* length of current match */ off_t lastscan = 0; /* position of previous match in old file */ off_t lastpos = 0; /* position of previous match in new file */ off_t lastoffset = 0; /* lastpos - lastscan */ off_t oldscore = 0, scsc = 0; /* temp variables in match search */ off_t s = 0, Sf = 0, lenf = 0, Sb = 0, lenb = 0; /* temp vars in match extension */ off_t overlap = 0, Ss = 0, lens = 0; off_t i = 0; off_t dblen = 0, eblen = 0; /* length of diff, extra sections */ u_char *db = NULL,*eb = NULL; /* contents of diff, extra sections */ u_char buf[8] = {0}; u_char header[32] = {0}; FILE * pf = NULL; int exitstatus = -1; if (argc != 4) { warnx("usage: %s oldfile newfile patchfile\n", argv[0]); goto cleanup; } old = readfile(argv[1], &oldsize); if (old == NULL) { warn("old file error: %s", argv[1]); goto cleanup; } if (((I = (off_t *)malloc(((size_t)oldsize + 1) * sizeof(off_t))) == NULL) || ((V = (off_t *)malloc(((size_t)oldsize + 1) * sizeof(off_t))) == NULL)) { warn("Failed to allocate memory for I or V"); goto cleanup; } /* Do a suffix sort on the old file. */ I[0] = oldsize; sais(old, I+1, (int)oldsize); free(V); V = NULL; newp = readfile(argv[2], &newsize); if (newp == NULL) { warn("new file error: %s", argv[2]); goto cleanup; } if (((db = (u_char *)malloc((size_t)newsize + 1)) == NULL) || ((eb = (u_char *)malloc((size_t)newsize + 1)) == NULL)) { warn("Failed to allocate memory for db or eb"); goto cleanup; } dblen = 0; eblen = 0; /* Create the patch file */ if ((pf = fopen(argv[3], "w")) == NULL) { warn("%s", argv[3]); goto cleanup; } /* Header is 0 8 "BSDIFN40" 8 8 length of ctrl block 16 8 length of diff block 24 8 length of new file */ /* File is 0 32 Header 32 ?? ctrl block ?? ?? diff block ?? ?? extra block */ memcpy(header, "BSDIFN40", 8); offtout(0, header + 8); offtout(0, header + 16); offtout(newsize, header + 24); if (fwrite(header, 32, 1, pf) != 1) { warn("fwrite(%s)", argv[3]); goto cleanup; } /* Compute the differences, writing ctrl as we go */ scan = 0; len = 0; lastscan = 0; lastpos = 0; lastoffset = 0; while (scan < newsize) { oldscore = 0; /* Modification ported from ChromiumOS project: * https://chromium.googlesource.com/chromiumos/third_party/bsdiff/+/a055996c743add7a9558839276fd1e4994d16bd3%5E%21/#F0 */ /* If we come across a large block of data that only differs * by less than 8 bytes, this loop will take a long time to * go past that block of data. We need to track the number of * times we're stuck in the block and break out of it. */ int num_less_than_eight = 0; off_t prev_len, prev_oldscore, prev_pos; for (scsc = scan += len; scan < newsize; scan++) { prev_len = len; prev_oldscore = oldscore; prev_pos = pos; /* 'oldscore' is the number of characters that match between the * substrings 'old[lastoffset + scan:lastoffset + scsc]' and * 'new[scan:scsc]'. */ len = search(I, old, oldsize, newp + scan, newsize - scan, 0, oldsize, &pos); /* If this match extends further than the last one, add any new * matching characters to 'oldscore'. */ for (; scsc < scan + len; scsc++) { if ((scsc + lastoffset < oldsize) && (old[scsc + lastoffset] == newp[scsc])) oldscore++; } /* Choose this as our match if it contains more than eight * characters that would be wrong if matched with a forward * extension of the previous match instead. */ if (((len == oldscore) && (len != 0)) || (len > oldscore + 8)) break; /* Since we're advancing 'scan' by 1, remove the character under it * from 'oldscore' if it matches. */ if ((scan + lastoffset < oldsize) && (old[scan + lastoffset] == newp[scan])) oldscore--; /* Modification ported from ChromiumOS project: * https://chromium.googlesource.com/chromiumos/third_party/bsdiff/+/426e4aa1cbeb3c8a73002047d7a796ca8e5e17d4%5E%21/#F0 */ const off_t fuzz = 8; if (prev_len - fuzz <= len && len <= prev_len && prev_oldscore - fuzz <= oldscore && oldscore <= prev_oldscore && prev_pos <= pos && pos <= prev_pos + fuzz && oldscore <= len && len <= oldscore + fuzz) ++num_less_than_eight; else num_less_than_eight=0; if (num_less_than_eight > 100) break; } /* Skip this section if we found an exact match that would be * better serviced by a forward extension of the previous match. */ if ((len != oldscore) || (scan == newsize)) { /* Figure out how far forward the previous match should be * extended... */ s = 0; Sf = 0; lenf = 0; for (i = 0; (lastscan + i < scan) && (lastpos + i < oldsize);) { if (old[lastpos + i] == newp[lastscan + i]) s++; i++; if (s * 2 - i > Sf * 2 - lenf) { Sf = s; lenf = i; } } /* ... and how far backwards the next match should be extended. */ lenb = 0; if (scan < newsize) { s = 0; Sb = 0; for (i = 1; (scan >= lastscan + i) && (pos >= i); i++) { if (old[pos - i] == newp[scan - i]) s++; if (s * 2 - i > Sb * 2 - lenb) { Sb = s; lenb = i; } } } /* If there is an overlap between the extensions, find the best * dividing point in the middle and reset 'lenf' and 'lenb' * accordingly. */ if (lastscan + lenf > scan - lenb) { overlap = (lastscan + lenf) - (scan - lenb); s = 0; Ss = 0; lens = 0; for (i = 0; i < overlap; i++) { if (newp[lastscan + lenf - overlap + i] == old[lastpos + lenf - overlap + i]) s++; if (newp[scan - lenb + i] == old[pos - lenb + i]) s--; if (s > Ss) { Ss = s; lens = i + 1; } } lenf += lens - overlap; lenb -= lens; } /* Write the diff data for the last match to the diff section... */ for (i = 0; i < lenf; i++) db[dblen + i] = newp[lastscan + i] - old[lastpos + i]; /* ... and, if there's a gap between the extensions just * calculated, write the data in that gap to the extra section. */ for (i = 0; i< (scan - lenb) - (lastscan + lenf); i++) eb[eblen + i] = newp[lastscan + lenf + i]; /* Update the diff and extra section lengths accordingly. */ dblen += lenf; eblen += (scan - lenb) - (lastscan + lenf); /* Write the following triple of integers to the control section: * - length of the diff * - length of the extra section * - offset between the end of the diff and the start of the next * diff, in the old file */ offtout(lenf, buf); if (fwrite(buf, 8, 1, pf) != 1) { warnx("fwrite"); goto cleanup; } offtout((scan - lenb) - (lastscan + lenf), buf); if (fwrite(buf, 8, 1, pf) != 1) { warn("fwrite"); goto cleanup; } offtout((pos - lenb) - (lastpos + lenf), buf); if (fwrite(buf, 8, 1, pf) != 1) { warn("fwrite"); goto cleanup; } /* Update the variables describing the last match. Note that * 'lastscan' is set to the start of the current match _after_ the * backwards extension; the data in that extension will be written * in the next pass. */ lastscan = scan - lenb; lastpos = pos - lenb; lastoffset = pos - scan; } } /* Compute size of compressed ctrl data */ if ((len = ftello(pf)) == -1) { warn("ftello"); goto cleanup; } offtout(len - 32, header + 8); /* Write diff data */ if (dblen && fwrite(db, (size_t)dblen, 1, pf) != 1) { warn("fwrite"); goto cleanup; } /* Compute size of compressed diff data */ if ((newsize = ftello(pf)) == -1) { warn("ftello"); goto cleanup; } offtout(newsize - len, header + 16); /* Write extra data */ if (eblen && fwrite(eb, (size_t)eblen, 1, pf) != 1) { warn("fwrite"); goto cleanup; } /* Seek to the beginning, write the header, and close the file */ if (fseeko(pf, 0, SEEK_SET)) { warn("fseeko"); goto cleanup; } if (fwrite(header, 32, 1, pf) != 1) { warn("fwrite(%s)", argv[3]); goto cleanup; } if (fclose(pf)) { warn("fclose"); pf = NULL; goto cleanup; } pf = NULL; exitstatus = 0; cleanup: if (pf != NULL) { fclose(pf); } /* Free the memory we used */ free(db); free(eb); free(I); free(V); free(old); free(newp); return exitstatus; } ================================================ FILE: Vendor/bsdiff/bspatch.c ================================================ /*- * Copyright 2003-2005 Colin Percival * All rights reserved * * Redistribution and use in source and binary forms, with or without * modification, are permitted providing that the following conditions * are met: * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING * IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE. */ #if 0 __FBSDID("$FreeBSD: src/usr.bin/bsdiff/bspatch/bspatch.c,v 1.1 2005/08/06 01:59:06 cperciva Exp $"); #endif #include "bspatch.h" #include #include #include #include #include #include #include #include "bscommon.h" /* Compatibility layer for reading either the old BSDIFF40 or the new BSDIFN40 patch formats: */ typedef void* stream_t; typedef struct { stream_t (*open)(FILE*); void (*close)(stream_t); off_t (*read)(stream_t, void*, off_t); } io_funcs_t; static stream_t BSDIFF40_open(FILE *f) { int bzerr = 0; BZFILE *s = NULL; if ((s = BZ2_bzReadOpen(&bzerr, f, 0, 0, NULL, 0)) == NULL) { warnx("BZ2_bzReadOpen, bz2err = %d", bzerr); } return s; } static void BSDIFF40_close(stream_t s) { int bzerr; BZ2_bzReadClose(&bzerr, (BZFILE*)s); } static off_t BSDIFF40_read(stream_t s, void *buf, off_t len) { int bzerr = 0, lenread = 0; lenread = BZ2_bzRead(&bzerr, (BZFILE*)s, buf, (int)len); if (bzerr != BZ_OK && bzerr != BZ_STREAM_END) { warnx("Corrupt patch\n"); lenread = -1; } return lenread; } static io_funcs_t BSDIFF40_funcs = { BSDIFF40_open, BSDIFF40_close, BSDIFF40_read }; static stream_t BSDIFN40_open(FILE *f) { return f; } static void BSDIFN40_close(stream_t __unused s) { } static off_t BSDIFN40_read(stream_t s, void *buf, off_t len) { return (off_t)fread(buf, 1, (size_t)len, (FILE*)s); } static io_funcs_t BSDIFN40_funcs = { BSDIFN40_open, BSDIFN40_close, BSDIFN40_read }; #ifndef u_char typedef unsigned char u_char; #endif static off_t offtin(u_char *buf) { off_t y; y=buf[7]&0x7F; y=y*256;y+=buf[6]; y=y*256;y+=buf[5]; y=y*256;y+=buf[4]; y=y*256;y+=buf[3]; y=y*256;y+=buf[2]; y=y*256;y+=buf[1]; y=y*256;y+=buf[0]; if(buf[7]&0x80) y=-y; return y; } int bspatch(int argc,const char * const argv[]) { FILE * f = NULL, * cpf = NULL, * dpf = NULL, * epf = NULL; stream_t cstream = NULL, dstream = NULL, estream = NULL; ssize_t oldsize = 0,newsize = 0; ssize_t bzctrllen = 0,bzdatalen = 0; u_char header[32] = {0},buf[8] = {0}; u_char *old = NULL, *newp = NULL; off_t oldpos = 0,newpos = 0; off_t ctrl[3] = {0}; off_t lenread = 0; off_t i = 0; io_funcs_t * io = NULL; int exitstatus = -1; off_t size = 0; if(argc!=4) { warnx("usage: %s oldfile newfile patchfile\n",argv[0]); goto cleanup; } /* Open patch file */ if ((f = fopen(argv[3], "r")) == NULL) { warn("fopen(%s)", argv[3]); goto cleanup; } /* File format: 0 8 "BSDIFF40" (bzip2) or "BSDIFN40" (raw) 8 8 X 16 8 Y 24 8 sizeof(newfile) 32 X bzip2(control block) 32+X Y bzip2(diff block) 32+X+Y ??? bzip2(extra block) with control block a set of triples (x,y,z) meaning "add x bytes from oldfile to x bytes from the diff block; copy y bytes from the extra block; seek forwards in oldfile by z bytes". */ /* Read header */ if (fread(header, 1, 32, f) < 32) { if (feof(f)) { warnx("Corrupt patch\n"); } else { warn("fread(%s)", argv[3]); } goto cleanup; } /* Check for appropriate magic */ if (memcmp(header, "BSDIFF40", 8) == 0) io = &BSDIFF40_funcs; else if (memcmp(header, "BSDIFN40", 8) == 0) io = &BSDIFN40_funcs; else { warnx("Corrupt patch\n"); goto cleanup; } /* Read lengths from header */ bzctrllen=offtin(header+8); bzdatalen=offtin(header+16); newsize=offtin(header+24); if((bzctrllen<0) || (bzdatalen<0) || (newsize<0)) { warnx("Corrupt patch\n"); goto cleanup; } /* Close patch file and re-open it via libbzip2 at the right places */ if (fclose(f)) { warn("fclose(%s)", argv[3]); f = NULL; goto cleanup; } f = NULL; if ((cpf = fopen(argv[3], "r")) == NULL) { warn("fopen(%s)", argv[3]); goto cleanup; } if (fseeko(cpf, 32, SEEK_SET)) { warn("fseeko(%s, %lld)", argv[3], (long long)32); goto cleanup; } cstream = io->open(cpf); if (cstream == NULL) { warn("cstream open"); goto cleanup; } if ((dpf = fopen(argv[3], "r")) == NULL) { warn("fopen(%s)", argv[3]); goto cleanup; } if (fseeko(dpf, 32 + bzctrllen, SEEK_SET)) { warn("fseeko(%s, %lld)", argv[3], (long long)(32 + bzctrllen)); goto cleanup; } dstream = io->open(dpf); if (dstream == NULL) { warn("dstream open"); goto cleanup; } if ((epf = fopen(argv[3], "r")) == NULL) { warn("fopen(%s)", argv[3]); goto cleanup; } if (fseeko(epf, 32 + bzctrllen + bzdatalen, SEEK_SET)) { warn("fseeko(%s, %lld)", argv[3], (long long)(32 + bzctrllen + bzdatalen)); goto cleanup; } estream = io->open(epf); if (estream == NULL) { warn("estream open"); goto cleanup; } old = readfile(argv[1], &size); if (old == NULL) { warn("old file: %s", argv[1]); goto cleanup; } oldsize = size; if((newp=(u_char *)malloc((size_t)newsize+1))==NULL) { warn("Failed to allocate memory for new"); goto cleanup; } oldpos=0;newpos=0; while(newposread(cstream, buf, 8); if (lenread < 8) { warnx("Corrupt patch\n"); goto cleanup; } ctrl[i]=offtin(buf); } /* Sanity-check */ if(newpos+ctrl[0]>newsize) { warnx("Corrupt patch\n"); goto cleanup; } /* Read diff string */ lenread = io->read(dstream, newp + newpos, ctrl[0]); if (lenread < 0 || lenread < ctrl[0]) { warnx("Corrupt patch\n"); goto cleanup; } /* Add old data to diff string */ for(i=0;i=0) && (oldpos+inewsize) { warnx("Corrupt patch\n"); goto cleanup; } /* Read extra string */ lenread = io->read(estream, newp + newpos, ctrl[1]); if (lenread < 0 || lenread < ctrl[1]) { warnx("Corrupt patch\n"); goto cleanup; } /* Adjust pointers */ newpos+=ctrl[1]; oldpos+=ctrl[2]; } /* Clean up the bzip2 reads */ io->close(cstream); cstream = NULL; io->close(dstream); dstream = NULL; io->close(estream); estream = NULL; if (fclose(cpf) != 0) { warn("fclose cpf(%s)", argv[3]); cpf = NULL; goto cleanup; } cpf = NULL; if (fclose(dpf) != 0) { warn("fclose dpf(%s)", argv[3]); dpf = NULL; goto cleanup; } dpf = NULL; if (fclose(epf) != 0) { warn("fclose epf(%s)", argv[3]); epf = NULL; goto cleanup; } epf = NULL; /* Write the new file */ f = fopen(argv[2], "w"); if (f == NULL) { warn("failed to write new file: %s", argv[2]); goto cleanup; } if (fwrite(newp, 1, (size_t)newsize, f) < (size_t)newsize) { warn("failed to write to new file: %s", argv[2]); goto cleanup; } if (fclose(f) != 0) { warn("failed to close new file: %s", argv[2]); f = NULL; goto cleanup; } f = NULL; exitstatus = 0; cleanup: free(newp); free(old); if (f != NULL) { fclose(f); } if (estream != NULL) { io->close(estream); } if (epf != NULL) { fclose(epf); } if (dstream != NULL) { io->close(dstream); } if (dpf != NULL) { fclose(dpf); } if (cstream != NULL) { io->close(cstream); } if (cpf != NULL) { fclose(cpf); } return exitstatus; } ================================================ FILE: Vendor/bsdiff/bspatch.h ================================================ /* * bspatch.h * Sparkle * * Created by Andy Matuschak on 1/11/10. * Copyright 2010 Andy Matuschak. All rights reserved. * */ // So that we can use this method in SUBinaryDeltaApply.m. // Silences the GCC warning that the prototype doesn't exist. int bspatch(int argc, const char * const argv[]); ================================================ FILE: Vendor/bsdiff/sais.c ================================================ /* * sais.c for sais-lite * Copyright (c) 2008-2010 Yuta Mori All Rights Reserved. * * Permission is hereby granted, free of charge, to any person * obtaining a copy of this software and associated documentation * files (the "Software"), to deal in the Software without * restriction, including without limitation the rights to use, * copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the * Software is furnished to do so, subject to the following * conditions: * * The above copyright notice and this permission notice shall be * included in all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR * OTHER DEALINGS IN THE SOFTWARE. */ #include #include #include "sais.h" #ifndef UCHAR_SIZE # define UCHAR_SIZE 256 #endif #ifndef MINBUCKETSIZE # define MINBUCKETSIZE 256 #endif #define sais_bool_type int #define SAIS_LMSSORT2_LIMIT 0x3fffffff #define SAIS_MYMALLOC(_num, _type) ((_type *)malloc((_num) * sizeof(_type))) #define SAIS_MYFREE(_ptr, _num, _type) free((_ptr)) #define chr(_a) (cs == sizeof(sais_index_type) ? ((const sais_index_type *)T)[(_a)] : ((const unsigned char *)T)[(_a)]) // This file exhibits several "suspicious comma" warnings. Rather than risk // changing a nuanced behavior, I'm assuming the file has been time-tested and // behaves as expected in spite of the unconventional use of comma operators // in some expressions. #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wcomma" /* find the start or end of each bucket */ static void getCounts(const void *T, sais_index_type *C, sais_index_type n, sais_index_type k, int cs) { sais_index_type i; for(i = 0; i < k; ++i) { C[i] = 0; } for(i = 0; i < n; ++i) { ++C[chr(i)]; } } static void getBuckets(const sais_index_type *C, sais_index_type *B, sais_index_type k, sais_bool_type end) { sais_index_type i, sum = 0; if(end) { for(i = 0; i < k; ++i) { sum += C[i]; B[i] = sum; } } else { for(i = 0; i < k; ++i) { sum += C[i]; B[i] = sum - C[i]; } } } /* sort all type LMS suffixes */ static void LMSsort1(const void *T, sais_index_type *SA, sais_index_type *C, sais_index_type *B, sais_index_type n, sais_index_type k, int cs) { sais_index_type *b, i, j; sais_index_type c0, c1; /* compute SAl */ if(C == B) { getCounts(T, C, n, k, cs); } getBuckets(C, B, k, 0); /* find starts of buckets */ j = n - 1; b = SA + B[c1 = chr(j)]; --j; *b++ = (chr(j) < c1) ? ~j : j; for(i = 0; i < n; ++i) { if(0 < (j = SA[i])) { assert(chr(j) >= chr(j + 1)); if((c0 = chr(j)) != c1) { B[c1] = b - SA; b = SA + B[c1 = c0]; } assert(i < (b - SA)); --j; *b++ = (chr(j) < c1) ? ~j : j; SA[i] = 0; } else if(j < 0) { SA[i] = ~j; } } /* compute SAs */ if(C == B) { getCounts(T, C, n, k, cs); } getBuckets(C, B, k, 1); /* find ends of buckets */ for(i = n - 1, b = SA + B[c1 = 0]; 0 <= i; --i) { if(0 < (j = SA[i])) { assert(chr(j) <= chr(j + 1)); if((c0 = chr(j)) != c1) { B[c1] = b - SA; b = SA + B[c1 = c0]; } assert((b - SA) <= i); --j; *--b = (chr(j) > c1) ? ~(j + 1) : j; SA[i] = 0; } } } static sais_index_type LMSpostproc1(const void *T, sais_index_type *SA, sais_index_type n, sais_index_type m, int cs) { sais_index_type i, j, p, q, plen, qlen, name; sais_index_type c0, c1; sais_bool_type diff; /* compact all the sorted substrings into the first m items of SA 2*m must be not larger than n (proveable) */ assert(0 < n); for(i = 0; (p = SA[i]) < 0; ++i) { SA[i] = ~p; assert((i + 1) < n); } if(i < m) { for(j = i, ++i;; ++i) { assert(i < n); if((p = SA[i]) < 0) { SA[j++] = ~p; SA[i] = 0; if(j == m) { break; } } } } /* store the length of all substrings */ i = n - 1; j = n - 1; c0 = chr(n - 1); do { c1 = c0; } while((0 <= --i) && ((c0 = chr(i)) >= c1)); for(; 0 <= i;) { do { c1 = c0; } while((0 <= --i) && ((c0 = chr(i)) <= c1)); if(0 <= i) { SA[m + ((i + 1) >> 1)] = j - i; j = i + 1; do { c1 = c0; } while((0 <= --i) && ((c0 = chr(i)) >= c1)); } } /* find the lexicographic names of all substrings */ for(i = 0, name = 0, q = n, qlen = 0; i < m; ++i) { p = SA[i], plen = SA[m + (p >> 1)], diff = 1; if((plen == qlen) && ((q + plen) < n)) { for(j = 0; (j < plen) && (chr(p + j) == chr(q + j)); ++j) { } if(j == plen) { diff = 0; } } if(diff != 0) { ++name, q = p, qlen = plen; } SA[m + (p >> 1)] = name; } return name; } static void LMSsort2(const void *T, sais_index_type *SA, sais_index_type *C, sais_index_type *B, sais_index_type *D, sais_index_type n, sais_index_type k, int cs) { sais_index_type *b, i, j, t, d; sais_index_type c0, c1; assert(C != B); /* compute SAl */ getBuckets(C, B, k, 0); /* find starts of buckets */ j = n - 1; b = SA + B[c1 = chr(j)]; --j; t = (chr(j) < c1); j += n; *b++ = (t & 1) ? ~j : j; for(i = 0, d = 0; i < n; ++i) { if(0 < (j = SA[i])) { if(n <= j) { d += 1; j -= n; } assert(chr(j) >= chr(j + 1)); if((c0 = chr(j)) != c1) { B[c1] = b - SA; b = SA + B[c1 = c0]; } assert(i < (b - SA)); --j; t = c0; t = (t << 1) | (chr(j) < c1); if(D[t] != d) { j += n; D[t] = d; } *b++ = (t & 1) ? ~j : j; SA[i] = 0; } else if(j < 0) { SA[i] = ~j; } } for(i = n - 1; 0 <= i; --i) { if(0 < SA[i]) { if(SA[i] < n) { SA[i] += n; for(j = i - 1; SA[j] < n; --j) { } SA[j] -= n; i = j; } } } /* compute SAs */ getBuckets(C, B, k, 1); /* find ends of buckets */ for(i = n - 1, d += 1, b = SA + B[c1 = 0]; 0 <= i; --i) { if(0 < (j = SA[i])) { if(n <= j) { d += 1; j -= n; } assert(chr(j) <= chr(j + 1)); if((c0 = chr(j)) != c1) { B[c1] = b - SA; b = SA + B[c1 = c0]; } assert((b - SA) <= i); --j; t = c0; t = (t << 1) | (chr(j) > c1); if(D[t] != d) { j += n; D[t] = d; } *--b = (t & 1) ? ~(j + 1) : j; SA[i] = 0; } } } static sais_index_type LMSpostproc2(sais_index_type *SA, sais_index_type n, sais_index_type m) { sais_index_type i, j, d, name; /* compact all the sorted LMS substrings into the first m items of SA */ assert(0 < n); for(i = 0, name = 0; (j = SA[i]) < 0; ++i) { j = ~j; if(n <= j) { name += 1; } SA[i] = j; assert((i + 1) < n); } if(i < m) { for(d = i, ++i;; ++i) { assert(i < n); if((j = SA[i]) < 0) { j = ~j; if(n <= j) { name += 1; } SA[d++] = j; SA[i] = 0; if(d == m) { break; } } } } if(name < m) { /* store the lexicographic names */ for(i = m - 1, d = name + 1; 0 <= i; --i) { if(n <= (j = SA[i])) { j -= n; --d; } SA[m + (j >> 1)] = d; } } else { /* unset flags */ for(i = 0; i < m; ++i) { if(n <= (j = SA[i])) { j -= n; SA[i] = j; } } } return name; } /* compute SA and BWT */ static void induceSA(const void *T, sais_index_type *SA, sais_index_type *C, sais_index_type *B, sais_index_type n, sais_index_type k, int cs) { sais_index_type *b, i, j; sais_index_type c0, c1; /* compute SAl */ if(C == B) { getCounts(T, C, n, k, cs); } getBuckets(C, B, k, 0); /* find starts of buckets */ j = n - 1; b = SA + B[c1 = chr(j)]; *b++ = ((0 < j) && (chr(j - 1) < c1)) ? ~j : j; for(i = 0; i < n; ++i) { j = SA[i], SA[i] = ~j; if(0 < j) { --j; assert(chr(j) >= chr(j + 1)); if((c0 = chr(j)) != c1) { B[c1] = b - SA; b = SA + B[c1 = c0]; } assert(i < (b - SA)); *b++ = ((0 < j) && (chr(j - 1) < c1)) ? ~j : j; } } /* compute SAs */ if(C == B) { getCounts(T, C, n, k, cs); } getBuckets(C, B, k, 1); /* find ends of buckets */ for(i = n - 1, b = SA + B[c1 = 0]; 0 <= i; --i) { if(0 < (j = SA[i])) { --j; assert(chr(j) <= chr(j + 1)); if((c0 = chr(j)) != c1) { B[c1] = b - SA; b = SA + B[c1 = c0]; } assert((b - SA) <= i); *--b = ((j == 0) || (chr(j - 1) > c1)) ? ~j : j; } else { SA[i] = ~j; } } } static sais_index_type computeBWT(const void *T, sais_index_type *SA, sais_index_type *C, sais_index_type *B, sais_index_type n, sais_index_type k, int cs) { sais_index_type *b, i, j, pidx = -1; sais_index_type c0, c1; /* compute SAl */ if(C == B) { getCounts(T, C, n, k, cs); } getBuckets(C, B, k, 0); /* find starts of buckets */ j = n - 1; b = SA + B[c1 = chr(j)]; *b++ = ((0 < j) && (chr(j - 1) < c1)) ? ~j : j; for(i = 0; i < n; ++i) { if(0 < (j = SA[i])) { --j; assert(chr(j) >= chr(j + 1)); SA[i] = ~((sais_index_type)(c0 = chr(j))); if(c0 != c1) { B[c1] = b - SA; b = SA + B[c1 = c0]; } assert(i < (b - SA)); *b++ = ((0 < j) && (chr(j - 1) < c1)) ? ~j : j; } else if(j != 0) { SA[i] = ~j; } } /* compute SAs */ if(C == B) { getCounts(T, C, n, k, cs); } getBuckets(C, B, k, 1); /* find ends of buckets */ for(i = n - 1, b = SA + B[c1 = 0]; 0 <= i; --i) { if(0 < (j = SA[i])) { --j; assert(chr(j) <= chr(j + 1)); SA[i] = (c0 = chr(j)); if(c0 != c1) { B[c1] = b - SA; b = SA + B[c1 = c0]; } assert((b - SA) <= i); *--b = ((0 < j) && (chr(j - 1) > c1)) ? ~((sais_index_type)chr(j - 1)) : j; } else if(j != 0) { SA[i] = ~j; } else { pidx = i; } } return pidx; } /* find the suffix array SA of T[0..n-1] in {0..255}^n */ static sais_index_type sais_main(const void *T, sais_index_type *SA, sais_index_type fs, sais_index_type n, sais_index_type k, int cs, sais_bool_type isbwt) { sais_index_type *C, *B, *D, *RA, *b; sais_index_type i, j, m, p, q, t, name, pidx = 0, newfs; sais_index_type c0, c1; unsigned int flags; assert((T != NULL) && (SA != NULL)); assert((0 <= fs) && (0 < n) && (1 <= k)); if(k <= MINBUCKETSIZE) { if((C = SAIS_MYMALLOC((size_t)k, sais_index_type)) == NULL) { return -2; } if(k <= fs) { B = SA + (n + fs - k); flags = 1; } else { if((B = SAIS_MYMALLOC((size_t)k, sais_index_type)) == NULL) { SAIS_MYFREE(C, k, sais_index_type); return -2; } flags = 3; } } else if(k <= fs) { C = SA + (n + fs - k); if(k <= (fs - k)) { B = C - k; flags = 0; } else if(k <= (MINBUCKETSIZE * 4)) { if((B = SAIS_MYMALLOC((size_t)k, sais_index_type)) == NULL) { return -2; } flags = 2; } else { B = C; flags = 8; } } else { if((C = B = SAIS_MYMALLOC((size_t)k, sais_index_type)) == NULL) { return -2; } flags = 4 | 8; } if((n <= SAIS_LMSSORT2_LIMIT) && (2 <= (n / k))) { if(flags & 1) { flags |= ((k * 2) <= (fs - k)) ? 32 : 16; } else if((flags == 0) && ((k * 2) <= (fs - k * 2))) { flags |= 32; } } /* stage 1: reduce the problem by at least 1/2 sort all the LMS-substrings */ getCounts(T, C, n, k, cs); getBuckets(C, B, k, 1); /* find ends of buckets */ for(i = 0; i < n; ++i) { SA[i] = 0; } b = &t; i = n - 1; j = n; m = 0; c0 = chr(n - 1); do { c1 = c0; } while((0 <= --i) && ((c0 = chr(i)) >= c1)); for(; 0 <= i;) { do { c1 = c0; } while((0 <= --i) && ((c0 = chr(i)) <= c1)); if(0 <= i) { *b = j; b = SA + --B[c1]; j = i; ++m; do { c1 = c0; } while((0 <= --i) && ((c0 = chr(i)) >= c1)); } } if(1 < m) { if(flags & (16 | 32)) { if(flags & 16) { if((D = SAIS_MYMALLOC((size_t)k * 2, sais_index_type)) == NULL) { if(flags & (1 | 4)) { SAIS_MYFREE(C, k, sais_index_type); } if(flags & 2) { SAIS_MYFREE(B, k, sais_index_type); } return -2; } } else { D = B - k * 2; } assert((j + 1) < n); ++B[chr(j + 1)]; for(i = 0, j = 0; i < k; ++i) { j += C[i]; if(B[i] != j) { assert(SA[B[i]] != 0); SA[B[i]] += n; } D[i] = D[i + k] = 0; } LMSsort2(T, SA, C, B, D, n, k, cs); name = LMSpostproc2(SA, n, m); if(flags & 16) { SAIS_MYFREE(D, k * 2, sais_index_type); } } else { LMSsort1(T, SA, C, B, n, k, cs); name = LMSpostproc1(T, SA, n, m, cs); } } else if(m == 1) { *b = j + 1; name = 1; } else { name = 0; } /* stage 2: solve the reduced problem recurse if names are not yet unique */ if(name < m) { if(flags & 4) { SAIS_MYFREE(C, k, sais_index_type); } if(flags & 2) { SAIS_MYFREE(B, k, sais_index_type); } newfs = (n + fs) - (m * 2); if((flags & (1 | 4 | 8)) == 0) { if((k + name) <= newfs) { newfs -= k; } else { flags |= 8; } } assert((n >> 1) <= (newfs + m)); RA = SA + m + newfs; for(i = m + (n >> 1) - 1, j = m - 1; m <= i; --i) { if(SA[i] != 0) { RA[j--] = SA[i] - 1; } } if(sais_main(RA, SA, newfs, m, name, sizeof(sais_index_type), 0) != 0) { if(flags & 1) { SAIS_MYFREE(C, k, sais_index_type); } return -2; } i = n - 1; j = m - 1; c0 = chr(n - 1); do { c1 = c0; } while((0 <= --i) && ((c0 = chr(i)) >= c1)); for(; 0 <= i;) { do { c1 = c0; } while((0 <= --i) && ((c0 = chr(i)) <= c1)); if(0 <= i) { RA[j--] = i + 1; do { c1 = c0; } while((0 <= --i) && ((c0 = chr(i)) >= c1)); } } for(i = 0; i < m; ++i) { SA[i] = RA[SA[i]]; } if(flags & 4) { if((C = B = SAIS_MYMALLOC((size_t)k, sais_index_type)) == NULL) { return -2; } } if(flags & 2) { if((B = SAIS_MYMALLOC((size_t)k, sais_index_type)) == NULL) { if(flags & 1) { SAIS_MYFREE(C, k, sais_index_type); } return -2; } } } /* stage 3: induce the result for the original problem */ if(flags & 8) { getCounts(T, C, n, k, cs); } /* put all left-most S characters into their buckets */ if(1 < m) { getBuckets(C, B, k, 1); /* find ends of buckets */ i = m - 1, j = n, p = SA[m - 1], c1 = chr(p); do { q = B[c0 = c1]; while(q < j) { SA[--j] = 0; } do { SA[--j] = p; if(--i < 0) { break; } p = SA[i]; } while((c1 = chr(p)) == c0); } while(0 <= i); while(0 < j) { SA[--j] = 0; } } if(isbwt == 0) { induceSA(T, SA, C, B, n, k, cs); } else { pidx = computeBWT(T, SA, C, B, n, k, cs); } if(flags & (1 | 4)) { SAIS_MYFREE(C, k, sais_index_type); } if(flags & 2) { SAIS_MYFREE(B, k, sais_index_type); } return pidx; } /*---------------------------------------------------------------------------*/ sais_index_type sais(const unsigned char *T, sais_index_type *SA, int n) { if((T == NULL) || (SA == NULL) || (n < 0)) { return -1; } if(n <= 1) { if(n == 1) { SA[0] = 0; } return 0; } return sais_main(T, SA, 0, n, UCHAR_SIZE, sizeof(unsigned char), 0); } sais_index_type sais_int(const int *T, sais_index_type *SA, int n, int k) { if((T == NULL) || (SA == NULL) || (n < 0) || (k <= 0)) { return -1; } if(n <= 1) { if(n == 1) { SA[0] = 0; } return 0; } return sais_main(T, SA, 0, n, k, sizeof(int), 0); } sais_index_type sais_bwt(const unsigned char *T, unsigned char *U, sais_index_type *A, int n) { int i; sais_index_type pidx; if((T == NULL) || (U == NULL) || (A == NULL) || (n < 0)) { return -1; } if(n <= 1) { if(n == 1) { U[0] = T[0]; } return n; } pidx = sais_main(T, A, 0, n, UCHAR_SIZE, sizeof(unsigned char), 1); if(pidx < 0) { return pidx; } U[0] = T[n - 1]; for(i = 0; i < pidx; ++i) { U[i + 1] = (unsigned char)A[i]; } for(i += 1; i < n; ++i) { U[i] = (unsigned char)A[i]; } pidx += 1; return pidx; } sais_index_type sais_int_bwt(const sais_index_type *T, sais_index_type *U, sais_index_type *A, int n, int k) { int i; sais_index_type pidx; if((T == NULL) || (U == NULL) || (A == NULL) || (n < 0) || (k <= 0)) { return -1; } if(n <= 1) { if(n == 1) { U[0] = T[0]; } return n; } pidx = sais_main(T, A, 0, n, k, sizeof(int), 1); if(pidx < 0) { return pidx; } U[0] = T[n - 1]; for(i = 0; i < pidx; ++i) { U[i + 1] = A[i]; } for(i += 1; i < n; ++i) { U[i] = A[i]; } pidx += 1; return pidx; } #pragma clang diagnostic pop ================================================ FILE: Vendor/bsdiff/sais.h ================================================ /* * sais.h for sais-lite * Copyright (c) 2008-2010 Yuta Mori All Rights Reserved. * * Permission is hereby granted, free of charge, to any person * obtaining a copy of this software and associated documentation * files (the "Software"), to deal in the Software without * restriction, including without limitation the rights to use, * copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the * Software is furnished to do so, subject to the following * conditions: * * The above copyright notice and this permission notice shall be * included in all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR * OTHER DEALINGS IN THE SOFTWARE. */ #ifndef SAIS_H #define SAIS_H 1 #ifdef __cplusplus extern "C" { #endif /* __cplusplus */ #include #define sais_index_type off_t /* find the suffix array SA of T[0..n-1] use a working space (excluding T and SA) of at most 2n+O(lg n) */ sais_index_type sais(const unsigned char *T, sais_index_type *SA, int n); /* find the suffix array SA of T[0..n-1] in {0..k-1}^n use a working space (excluding T and SA) of at most MAX(4k,2n) */ sais_index_type sais_int(const int *T, sais_index_type *SA, int n, int k); /* burrows-wheeler transform */ sais_index_type sais_bwt(const unsigned char *T, unsigned char *U, sais_index_type *A, int n); sais_index_type sais_int_bwt(const sais_index_type *T, sais_index_type *U, sais_index_type *A, int n, int k); #ifdef __cplusplus } /* extern "C" */ #endif /* __cplusplus */ #endif /* SAIS_H */ ================================================ FILE: Vendor/ed25519-sparkle/alterations.txt ================================================ Files removed: ed25519_32.dll ed25519_64.dll test.c Retrieved source: git@github.com:sparkle-project/ed25519.git Branch: master Commit: 7fa6712ef5d581a6981ec2b08ee623314cd1d1c4 ================================================ FILE: Vendor/ed25519-sparkle/license.txt ================================================ Copyright (c) 2015 Orson Peters This software is provided 'as-is', without any express or implied warranty. In no event will the authors be held liable for any damages arising from the use of this software. Permission is granted to anyone to use this software for any purpose, including commercial applications, and to alter it and redistribute it freely, subject to the following restrictions: 1. The origin of this software must not be misrepresented; you must not claim that you wrote the original software. If you use this software in a product, an acknowledgment in the product documentation would be appreciated but is not required. 2. Altered source versions must be plainly marked as such, and must not be misrepresented as being the original software. 3. This notice may not be removed or altered from any source distribution. ================================================ FILE: Vendor/ed25519-sparkle/readme.md ================================================ Ed25519 ======= This is a portable implementation of [Ed25519](http://ed25519.cr.yp.to/) based on the SUPERCOP "ref10" implementation. Additionally there is key exchanging and scalar addition included to further aid building a PKI using Ed25519. All code is licensed under the permissive zlib license. All code is pure ANSI C without any dependencies, except for the random seed generation which uses standard OS cryptography APIs (`CryptGenRandom` on Windows, `/dev/urandom` on nix). If you wish to be entirely portable define `ED25519_NO_SEED`. This disables the `ed25519_create_seed` function, so if your application requires key generation you must supply your own seeding function (which is simply a 256 bit (32 byte) cryptographic random number generator). Performance ----------- On a Windows machine with an Intel Pentium B970 @ 2.3GHz I got the following speeds (running on only one a single core): Seed generation: 64us (15625 per second) Key generation: 88us (11364 per second) Message signing (short message): 87us (11494 per second) Message verifying (short message): 228us (4386 per second) Scalar addition: 100us (10000 per second) Key exchange: 220us (4545 per second) The speeds on other machines may vary. Sign/verify times will be higher with longer messages. The implementation significantly benefits from 64 bit architectures, if possible compile as 64 bit. Usage ----- Simply add all .c and .h files in the `src/` folder to your project and include `ed25519.h` in any file you want to use the API. If you prefer to use a shared library, only copy `ed25519.h` and define `ED25519_DLL` before importing. A windows DLL is pre-built. There are no defined types for seeds, private keys, public keys, shared secrets or signatures. Instead simple `unsigned char` buffers are used with the following sizes: ```c unsigned char seed[32]; unsigned char signature[64]; unsigned char public_key[32]; unsigned char private_key[64]; unsigned char scalar[32]; unsigned char shared_secret[32]; ``` API --- ```c int ed25519_create_seed(unsigned char *seed); ``` Creates a 32 byte random seed in `seed` for key generation. `seed` must be a writable 32 byte buffer. Returns 0 on success, and nonzero on failure. ```c void ed25519_create_keypair(unsigned char *public_key, unsigned char *private_key, const unsigned char *seed); ``` Creates a new key pair from the given seed. `public_key` must be a writable 32 byte buffer, `private_key` must be a writable 64 byte buffer and `seed` must be a 32 byte buffer. ```c void ed25519_sign(unsigned char *signature, const unsigned char *message, size_t message_len, const unsigned char *public_key, const unsigned char *private_key); ``` Creates a signature of the given message with the given key pair. `signature` must be a writable 64 byte buffer. `message` must have at least `message_len` bytes to be read. ```c int ed25519_verify(const unsigned char *signature, const unsigned char *message, size_t message_len, const unsigned char *public_key); ``` Verifies the signature on the given message using `public_key`. `signature` must be a readable 64 byte buffer. `message` must have at least `message_len` bytes to be read. Returns 1 if the signature matches, 0 otherwise. ```c void ed25519_add_scalar(unsigned char *public_key, unsigned char *private_key, const unsigned char *scalar); ``` Adds `scalar` to the given key pair where scalar is a 32 byte buffer (possibly generated with `ed25519_create_seed`), generating a new key pair. You can calculate the public key sum without knowing the private key and vice versa by passing in `NULL` for the key you don't know. This is useful for enforcing randomness on a key pair by a third party while only knowing the public key, among other things. Warning: the last bit of the scalar is ignored - if comparing scalars make sure to clear it with `scalar[31] &= 127`. ```c void ed25519_key_exchange(unsigned char *shared_secret, const unsigned char *public_key, const unsigned char *private_key); ``` Performs a key exchange on the given public key and private key, producing a shared secret. It is recommended to hash the shared secret before using it. `shared_secret` must be a 32 byte writable buffer where the shared secret will be stored. Example ------- ```c unsigned char seed[32], public_key[32], private_key[64], signature[64]; unsigned char other_public_key[32], other_private_key[64], shared_secret[32]; const unsigned char message[] = "TEST MESSAGE"; /* create a random seed, and a key pair out of that seed */ if (ed25519_create_seed(seed)) { printf("error while generating seed\n"); exit(1); } ed25519_create_keypair(public_key, private_key, seed); /* create signature on the message with the key pair */ ed25519_sign(signature, message, strlen(message), public_key, private_key); /* verify the signature */ if (ed25519_verify(signature, message, strlen(message), public_key)) { printf("valid signature\n"); } else { printf("invalid signature\n"); } /* create a dummy keypair to use for a key exchange, normally you'd only have the public key and receive it through some communication channel */ if (ed25519_create_seed(seed)) { printf("error while generating seed\n"); exit(1); } ed25519_create_keypair(other_public_key, other_private_key, seed); /* do a key exchange with other_public_key */ ed25519_key_exchange(shared_secret, other_public_key, private_key); /* the magic here is that ed25519_key_exchange(shared_secret, public_key, other_private_key); would result in the same shared_secret */ ``` License ------- All code is released under the zlib license. See license.txt for details. ================================================ FILE: Vendor/ed25519-sparkle/src/add_scalar.c ================================================ #include "ed25519.h" #include "ge.h" #include "sc.h" #include "sha512.h" /* see http://crypto.stackexchange.com/a/6215/4697 */ void ed25519_add_scalar(unsigned char *public_key, unsigned char *private_key, const unsigned char *scalar) { const unsigned char SC_1[32] = {1}; /* scalar with value 1 */ unsigned char n[32]; ge_p3 nB; ge_p1p1 A_p1p1; ge_p3 A; ge_p3 public_key_unpacked; ge_cached T; sha512_context hash; unsigned char hashbuf[64]; int i; /* copy the scalar and clear highest bit */ for (i = 0; i < 31; ++i) { n[i] = scalar[i]; } n[31] = scalar[31] & 127; /* private key: a = n + t */ if (private_key) { sc_muladd(private_key, SC_1, n, private_key); // https://github.com/orlp/ed25519/issues/3 sha512_init(&hash); sha512_update(&hash, private_key + 32, 32); sha512_update(&hash, scalar, 32); sha512_final(&hash, hashbuf); for (i = 0; i < 32; ++i) { private_key[32 + i] = hashbuf[i]; } } /* public key: A = nB + T */ if (public_key) { /* if we know the private key we don't need a point addition, which is faster */ /* using a "timing attack" you could find out wether or not we know the private key, but this information seems rather useless - if this is important pass public_key and private_key seperately in 2 function calls */ if (private_key) { ge_scalarmult_base(&A, private_key); } else { /* unpack public key into T */ ge_frombytes_negate_vartime(&public_key_unpacked, public_key); fe_neg(public_key_unpacked.X, public_key_unpacked.X); /* undo negate */ fe_neg(public_key_unpacked.T, public_key_unpacked.T); /* undo negate */ ge_p3_to_cached(&T, &public_key_unpacked); /* calculate n*B */ ge_scalarmult_base(&nB, n); /* A = n*B + T */ ge_add(&A_p1p1, &nB, &T); ge_p1p1_to_p3(&A, &A_p1p1); } /* pack public key */ ge_p3_tobytes(public_key, &A); } } ================================================ FILE: Vendor/ed25519-sparkle/src/ed25519.h ================================================ #ifndef ED25519_H #define ED25519_H #include #if defined(_WIN32) #if defined(ED25519_BUILD_DLL) #define ED25519_DECLSPEC __declspec(dllexport) #elif defined(ED25519_DLL) #define ED25519_DECLSPEC __declspec(dllimport) #else #define ED25519_DECLSPEC #endif #else #define ED25519_DECLSPEC #endif #ifdef __cplusplus extern "C" { #endif #ifndef ED25519_NO_SEED int ED25519_DECLSPEC ed25519_create_seed(unsigned char *seed); #endif void ED25519_DECLSPEC ed25519_create_keypair(unsigned char *public_key, unsigned char *private_key, const unsigned char *seed); void ED25519_DECLSPEC ed25519_sign(unsigned char *signature, const unsigned char *message, size_t message_len, const unsigned char *public_key, const unsigned char *private_key); int ED25519_DECLSPEC ed25519_verify(const unsigned char *signature, const unsigned char *message, size_t message_len, const unsigned char *public_key); void ED25519_DECLSPEC ed25519_add_scalar(unsigned char *public_key, unsigned char *private_key, const unsigned char *scalar); void ED25519_DECLSPEC ed25519_key_exchange(unsigned char *shared_secret, const unsigned char *public_key, const unsigned char *private_key); #ifdef __cplusplus } #endif #endif ================================================ FILE: Vendor/ed25519-sparkle/src/fe.c ================================================ #include "fixedint.h" #include "fe.h" /* helper functions */ static uint64_t load_3(const unsigned char *in) { uint64_t result; result = (uint64_t) in[0]; result |= ((uint64_t) in[1]) << 8; result |= ((uint64_t) in[2]) << 16; return result; } static uint64_t load_4(const unsigned char *in) { uint64_t result; result = (uint64_t) in[0]; result |= ((uint64_t) in[1]) << 8; result |= ((uint64_t) in[2]) << 16; result |= ((uint64_t) in[3]) << 24; return result; } /* h = 0 */ void fe_0(fe h) { h[0] = 0; h[1] = 0; h[2] = 0; h[3] = 0; h[4] = 0; h[5] = 0; h[6] = 0; h[7] = 0; h[8] = 0; h[9] = 0; } /* h = 1 */ void fe_1(fe h) { h[0] = 1; h[1] = 0; h[2] = 0; h[3] = 0; h[4] = 0; h[5] = 0; h[6] = 0; h[7] = 0; h[8] = 0; h[9] = 0; } /* h = f + g Can overlap h with f or g. Preconditions: |f| bounded by 1.1*2^25,1.1*2^24,1.1*2^25,1.1*2^24,etc. |g| bounded by 1.1*2^25,1.1*2^24,1.1*2^25,1.1*2^24,etc. Postconditions: |h| bounded by 1.1*2^26,1.1*2^25,1.1*2^26,1.1*2^25,etc. */ void fe_add(fe h, const fe f, const fe g) { int32_t f0 = f[0]; int32_t f1 = f[1]; int32_t f2 = f[2]; int32_t f3 = f[3]; int32_t f4 = f[4]; int32_t f5 = f[5]; int32_t f6 = f[6]; int32_t f7 = f[7]; int32_t f8 = f[8]; int32_t f9 = f[9]; int32_t g0 = g[0]; int32_t g1 = g[1]; int32_t g2 = g[2]; int32_t g3 = g[3]; int32_t g4 = g[4]; int32_t g5 = g[5]; int32_t g6 = g[6]; int32_t g7 = g[7]; int32_t g8 = g[8]; int32_t g9 = g[9]; int32_t h0 = f0 + g0; int32_t h1 = f1 + g1; int32_t h2 = f2 + g2; int32_t h3 = f3 + g3; int32_t h4 = f4 + g4; int32_t h5 = f5 + g5; int32_t h6 = f6 + g6; int32_t h7 = f7 + g7; int32_t h8 = f8 + g8; int32_t h9 = f9 + g9; h[0] = h0; h[1] = h1; h[2] = h2; h[3] = h3; h[4] = h4; h[5] = h5; h[6] = h6; h[7] = h7; h[8] = h8; h[9] = h9; } /* Replace (f,g) with (g,g) if b == 1; replace (f,g) with (f,g) if b == 0. Preconditions: b in {0,1}. */ void fe_cmov(fe f, const fe g, unsigned int b) { int32_t f0 = f[0]; int32_t f1 = f[1]; int32_t f2 = f[2]; int32_t f3 = f[3]; int32_t f4 = f[4]; int32_t f5 = f[5]; int32_t f6 = f[6]; int32_t f7 = f[7]; int32_t f8 = f[8]; int32_t f9 = f[9]; int32_t g0 = g[0]; int32_t g1 = g[1]; int32_t g2 = g[2]; int32_t g3 = g[3]; int32_t g4 = g[4]; int32_t g5 = g[5]; int32_t g6 = g[6]; int32_t g7 = g[7]; int32_t g8 = g[8]; int32_t g9 = g[9]; int32_t x0 = f0 ^ g0; int32_t x1 = f1 ^ g1; int32_t x2 = f2 ^ g2; int32_t x3 = f3 ^ g3; int32_t x4 = f4 ^ g4; int32_t x5 = f5 ^ g5; int32_t x6 = f6 ^ g6; int32_t x7 = f7 ^ g7; int32_t x8 = f8 ^ g8; int32_t x9 = f9 ^ g9; b = (unsigned int) (- (int) b); /* silence warning */ x0 &= b; x1 &= b; x2 &= b; x3 &= b; x4 &= b; x5 &= b; x6 &= b; x7 &= b; x8 &= b; x9 &= b; f[0] = f0 ^ x0; f[1] = f1 ^ x1; f[2] = f2 ^ x2; f[3] = f3 ^ x3; f[4] = f4 ^ x4; f[5] = f5 ^ x5; f[6] = f6 ^ x6; f[7] = f7 ^ x7; f[8] = f8 ^ x8; f[9] = f9 ^ x9; } /* Replace (f,g) with (g,f) if b == 1; replace (f,g) with (f,g) if b == 0. Preconditions: b in {0,1}. */ void fe_cswap(fe f,fe g,unsigned int b) { int32_t f0 = f[0]; int32_t f1 = f[1]; int32_t f2 = f[2]; int32_t f3 = f[3]; int32_t f4 = f[4]; int32_t f5 = f[5]; int32_t f6 = f[6]; int32_t f7 = f[7]; int32_t f8 = f[8]; int32_t f9 = f[9]; int32_t g0 = g[0]; int32_t g1 = g[1]; int32_t g2 = g[2]; int32_t g3 = g[3]; int32_t g4 = g[4]; int32_t g5 = g[5]; int32_t g6 = g[6]; int32_t g7 = g[7]; int32_t g8 = g[8]; int32_t g9 = g[9]; int32_t x0 = f0 ^ g0; int32_t x1 = f1 ^ g1; int32_t x2 = f2 ^ g2; int32_t x3 = f3 ^ g3; int32_t x4 = f4 ^ g4; int32_t x5 = f5 ^ g5; int32_t x6 = f6 ^ g6; int32_t x7 = f7 ^ g7; int32_t x8 = f8 ^ g8; int32_t x9 = f9 ^ g9; b = (unsigned int) (- (int) b); /* silence warning */ x0 &= b; x1 &= b; x2 &= b; x3 &= b; x4 &= b; x5 &= b; x6 &= b; x7 &= b; x8 &= b; x9 &= b; f[0] = f0 ^ x0; f[1] = f1 ^ x1; f[2] = f2 ^ x2; f[3] = f3 ^ x3; f[4] = f4 ^ x4; f[5] = f5 ^ x5; f[6] = f6 ^ x6; f[7] = f7 ^ x7; f[8] = f8 ^ x8; f[9] = f9 ^ x9; g[0] = g0 ^ x0; g[1] = g1 ^ x1; g[2] = g2 ^ x2; g[3] = g3 ^ x3; g[4] = g4 ^ x4; g[5] = g5 ^ x5; g[6] = g6 ^ x6; g[7] = g7 ^ x7; g[8] = g8 ^ x8; g[9] = g9 ^ x9; } /* h = f */ void fe_copy(fe h, const fe f) { int32_t f0 = f[0]; int32_t f1 = f[1]; int32_t f2 = f[2]; int32_t f3 = f[3]; int32_t f4 = f[4]; int32_t f5 = f[5]; int32_t f6 = f[6]; int32_t f7 = f[7]; int32_t f8 = f[8]; int32_t f9 = f[9]; h[0] = f0; h[1] = f1; h[2] = f2; h[3] = f3; h[4] = f4; h[5] = f5; h[6] = f6; h[7] = f7; h[8] = f8; h[9] = f9; } /* Ignores top bit of h. */ void fe_frombytes(fe h, const unsigned char *s) { int64_t h0 = load_4(s); int64_t h1 = load_3(s + 4) << 6; int64_t h2 = load_3(s + 7) << 5; int64_t h3 = load_3(s + 10) << 3; int64_t h4 = load_3(s + 13) << 2; int64_t h5 = load_4(s + 16); int64_t h6 = load_3(s + 20) << 7; int64_t h7 = load_3(s + 23) << 5; int64_t h8 = load_3(s + 26) << 4; int64_t h9 = (load_3(s + 29) & 8388607) << 2; int64_t carry0; int64_t carry1; int64_t carry2; int64_t carry3; int64_t carry4; int64_t carry5; int64_t carry6; int64_t carry7; int64_t carry8; int64_t carry9; carry9 = (h9 + (int64_t) (1 << 24)) >> 25; h0 += carry9 * 19; h9 -= carry9 << 25; carry1 = (h1 + (int64_t) (1 << 24)) >> 25; h2 += carry1; h1 -= carry1 << 25; carry3 = (h3 + (int64_t) (1 << 24)) >> 25; h4 += carry3; h3 -= carry3 << 25; carry5 = (h5 + (int64_t) (1 << 24)) >> 25; h6 += carry5; h5 -= carry5 << 25; carry7 = (h7 + (int64_t) (1 << 24)) >> 25; h8 += carry7; h7 -= carry7 << 25; carry0 = (h0 + (int64_t) (1 << 25)) >> 26; h1 += carry0; h0 -= carry0 << 26; carry2 = (h2 + (int64_t) (1 << 25)) >> 26; h3 += carry2; h2 -= carry2 << 26; carry4 = (h4 + (int64_t) (1 << 25)) >> 26; h5 += carry4; h4 -= carry4 << 26; carry6 = (h6 + (int64_t) (1 << 25)) >> 26; h7 += carry6; h6 -= carry6 << 26; carry8 = (h8 + (int64_t) (1 << 25)) >> 26; h9 += carry8; h8 -= carry8 << 26; h[0] = (int32_t) h0; h[1] = (int32_t) h1; h[2] = (int32_t) h2; h[3] = (int32_t) h3; h[4] = (int32_t) h4; h[5] = (int32_t) h5; h[6] = (int32_t) h6; h[7] = (int32_t) h7; h[8] = (int32_t) h8; h[9] = (int32_t) h9; } void fe_invert(fe out, const fe z) { fe t0; fe t1; fe t2; fe t3; int i; fe_sq(t0, z); for (i = 1; i < 1; ++i) { fe_sq(t0, t0); } fe_sq(t1, t0); for (i = 1; i < 2; ++i) { fe_sq(t1, t1); } fe_mul(t1, z, t1); fe_mul(t0, t0, t1); fe_sq(t2, t0); for (i = 1; i < 1; ++i) { fe_sq(t2, t2); } fe_mul(t1, t1, t2); fe_sq(t2, t1); for (i = 1; i < 5; ++i) { fe_sq(t2, t2); } fe_mul(t1, t2, t1); fe_sq(t2, t1); for (i = 1; i < 10; ++i) { fe_sq(t2, t2); } fe_mul(t2, t2, t1); fe_sq(t3, t2); for (i = 1; i < 20; ++i) { fe_sq(t3, t3); } fe_mul(t2, t3, t2); fe_sq(t2, t2); for (i = 1; i < 10; ++i) { fe_sq(t2, t2); } fe_mul(t1, t2, t1); fe_sq(t2, t1); for (i = 1; i < 50; ++i) { fe_sq(t2, t2); } fe_mul(t2, t2, t1); fe_sq(t3, t2); for (i = 1; i < 100; ++i) { fe_sq(t3, t3); } fe_mul(t2, t3, t2); fe_sq(t2, t2); for (i = 1; i < 50; ++i) { fe_sq(t2, t2); } fe_mul(t1, t2, t1); fe_sq(t1, t1); for (i = 1; i < 5; ++i) { fe_sq(t1, t1); } fe_mul(out, t1, t0); } /* return 1 if f is in {1,3,5,...,q-2} return 0 if f is in {0,2,4,...,q-1} Preconditions: |f| bounded by 1.1*2^26,1.1*2^25,1.1*2^26,1.1*2^25,etc. */ int fe_isnegative(const fe f) { unsigned char s[32]; fe_tobytes(s, f); return s[0] & 1; } /* return 1 if f == 0 return 0 if f != 0 Preconditions: |f| bounded by 1.1*2^26,1.1*2^25,1.1*2^26,1.1*2^25,etc. */ int fe_isnonzero(const fe f) { unsigned char s[32]; unsigned char r; fe_tobytes(s, f); r = s[0]; #define F(i) r |= s[i] F(1); F(2); F(3); F(4); F(5); F(6); F(7); F(8); F(9); F(10); F(11); F(12); F(13); F(14); F(15); F(16); F(17); F(18); F(19); F(20); F(21); F(22); F(23); F(24); F(25); F(26); F(27); F(28); F(29); F(30); F(31); #undef F return r != 0; } /* h = f * g Can overlap h with f or g. Preconditions: |f| bounded by 1.65*2^26,1.65*2^25,1.65*2^26,1.65*2^25,etc. |g| bounded by 1.65*2^26,1.65*2^25,1.65*2^26,1.65*2^25,etc. Postconditions: |h| bounded by 1.01*2^25,1.01*2^24,1.01*2^25,1.01*2^24,etc. */ /* Notes on implementation strategy: Using schoolbook multiplication. Karatsuba would save a little in some cost models. Most multiplications by 2 and 19 are 32-bit precomputations; cheaper than 64-bit postcomputations. There is one remaining multiplication by 19 in the carry chain; one *19 precomputation can be merged into this, but the resulting data flow is considerably less clean. There are 12 carries below. 10 of them are 2-way parallelizable and vectorizable. Can get away with 11 carries, but then data flow is much deeper. With tighter constraints on inputs can squeeze carries into int32. */ void fe_mul(fe h, const fe f, const fe g) { int32_t f0 = f[0]; int32_t f1 = f[1]; int32_t f2 = f[2]; int32_t f3 = f[3]; int32_t f4 = f[4]; int32_t f5 = f[5]; int32_t f6 = f[6]; int32_t f7 = f[7]; int32_t f8 = f[8]; int32_t f9 = f[9]; int32_t g0 = g[0]; int32_t g1 = g[1]; int32_t g2 = g[2]; int32_t g3 = g[3]; int32_t g4 = g[4]; int32_t g5 = g[5]; int32_t g6 = g[6]; int32_t g7 = g[7]; int32_t g8 = g[8]; int32_t g9 = g[9]; int32_t g1_19 = 19 * g1; /* 1.959375*2^29 */ int32_t g2_19 = 19 * g2; /* 1.959375*2^30; still ok */ int32_t g3_19 = 19 * g3; int32_t g4_19 = 19 * g4; int32_t g5_19 = 19 * g5; int32_t g6_19 = 19 * g6; int32_t g7_19 = 19 * g7; int32_t g8_19 = 19 * g8; int32_t g9_19 = 19 * g9; int32_t f1_2 = 2 * f1; int32_t f3_2 = 2 * f3; int32_t f5_2 = 2 * f5; int32_t f7_2 = 2 * f7; int32_t f9_2 = 2 * f9; int64_t f0g0 = f0 * (int64_t) g0; int64_t f0g1 = f0 * (int64_t) g1; int64_t f0g2 = f0 * (int64_t) g2; int64_t f0g3 = f0 * (int64_t) g3; int64_t f0g4 = f0 * (int64_t) g4; int64_t f0g5 = f0 * (int64_t) g5; int64_t f0g6 = f0 * (int64_t) g6; int64_t f0g7 = f0 * (int64_t) g7; int64_t f0g8 = f0 * (int64_t) g8; int64_t f0g9 = f0 * (int64_t) g9; int64_t f1g0 = f1 * (int64_t) g0; int64_t f1g1_2 = f1_2 * (int64_t) g1; int64_t f1g2 = f1 * (int64_t) g2; int64_t f1g3_2 = f1_2 * (int64_t) g3; int64_t f1g4 = f1 * (int64_t) g4; int64_t f1g5_2 = f1_2 * (int64_t) g5; int64_t f1g6 = f1 * (int64_t) g6; int64_t f1g7_2 = f1_2 * (int64_t) g7; int64_t f1g8 = f1 * (int64_t) g8; int64_t f1g9_38 = f1_2 * (int64_t) g9_19; int64_t f2g0 = f2 * (int64_t) g0; int64_t f2g1 = f2 * (int64_t) g1; int64_t f2g2 = f2 * (int64_t) g2; int64_t f2g3 = f2 * (int64_t) g3; int64_t f2g4 = f2 * (int64_t) g4; int64_t f2g5 = f2 * (int64_t) g5; int64_t f2g6 = f2 * (int64_t) g6; int64_t f2g7 = f2 * (int64_t) g7; int64_t f2g8_19 = f2 * (int64_t) g8_19; int64_t f2g9_19 = f2 * (int64_t) g9_19; int64_t f3g0 = f3 * (int64_t) g0; int64_t f3g1_2 = f3_2 * (int64_t) g1; int64_t f3g2 = f3 * (int64_t) g2; int64_t f3g3_2 = f3_2 * (int64_t) g3; int64_t f3g4 = f3 * (int64_t) g4; int64_t f3g5_2 = f3_2 * (int64_t) g5; int64_t f3g6 = f3 * (int64_t) g6; int64_t f3g7_38 = f3_2 * (int64_t) g7_19; int64_t f3g8_19 = f3 * (int64_t) g8_19; int64_t f3g9_38 = f3_2 * (int64_t) g9_19; int64_t f4g0 = f4 * (int64_t) g0; int64_t f4g1 = f4 * (int64_t) g1; int64_t f4g2 = f4 * (int64_t) g2; int64_t f4g3 = f4 * (int64_t) g3; int64_t f4g4 = f4 * (int64_t) g4; int64_t f4g5 = f4 * (int64_t) g5; int64_t f4g6_19 = f4 * (int64_t) g6_19; int64_t f4g7_19 = f4 * (int64_t) g7_19; int64_t f4g8_19 = f4 * (int64_t) g8_19; int64_t f4g9_19 = f4 * (int64_t) g9_19; int64_t f5g0 = f5 * (int64_t) g0; int64_t f5g1_2 = f5_2 * (int64_t) g1; int64_t f5g2 = f5 * (int64_t) g2; int64_t f5g3_2 = f5_2 * (int64_t) g3; int64_t f5g4 = f5 * (int64_t) g4; int64_t f5g5_38 = f5_2 * (int64_t) g5_19; int64_t f5g6_19 = f5 * (int64_t) g6_19; int64_t f5g7_38 = f5_2 * (int64_t) g7_19; int64_t f5g8_19 = f5 * (int64_t) g8_19; int64_t f5g9_38 = f5_2 * (int64_t) g9_19; int64_t f6g0 = f6 * (int64_t) g0; int64_t f6g1 = f6 * (int64_t) g1; int64_t f6g2 = f6 * (int64_t) g2; int64_t f6g3 = f6 * (int64_t) g3; int64_t f6g4_19 = f6 * (int64_t) g4_19; int64_t f6g5_19 = f6 * (int64_t) g5_19; int64_t f6g6_19 = f6 * (int64_t) g6_19; int64_t f6g7_19 = f6 * (int64_t) g7_19; int64_t f6g8_19 = f6 * (int64_t) g8_19; int64_t f6g9_19 = f6 * (int64_t) g9_19; int64_t f7g0 = f7 * (int64_t) g0; int64_t f7g1_2 = f7_2 * (int64_t) g1; int64_t f7g2 = f7 * (int64_t) g2; int64_t f7g3_38 = f7_2 * (int64_t) g3_19; int64_t f7g4_19 = f7 * (int64_t) g4_19; int64_t f7g5_38 = f7_2 * (int64_t) g5_19; int64_t f7g6_19 = f7 * (int64_t) g6_19; int64_t f7g7_38 = f7_2 * (int64_t) g7_19; int64_t f7g8_19 = f7 * (int64_t) g8_19; int64_t f7g9_38 = f7_2 * (int64_t) g9_19; int64_t f8g0 = f8 * (int64_t) g0; int64_t f8g1 = f8 * (int64_t) g1; int64_t f8g2_19 = f8 * (int64_t) g2_19; int64_t f8g3_19 = f8 * (int64_t) g3_19; int64_t f8g4_19 = f8 * (int64_t) g4_19; int64_t f8g5_19 = f8 * (int64_t) g5_19; int64_t f8g6_19 = f8 * (int64_t) g6_19; int64_t f8g7_19 = f8 * (int64_t) g7_19; int64_t f8g8_19 = f8 * (int64_t) g8_19; int64_t f8g9_19 = f8 * (int64_t) g9_19; int64_t f9g0 = f9 * (int64_t) g0; int64_t f9g1_38 = f9_2 * (int64_t) g1_19; int64_t f9g2_19 = f9 * (int64_t) g2_19; int64_t f9g3_38 = f9_2 * (int64_t) g3_19; int64_t f9g4_19 = f9 * (int64_t) g4_19; int64_t f9g5_38 = f9_2 * (int64_t) g5_19; int64_t f9g6_19 = f9 * (int64_t) g6_19; int64_t f9g7_38 = f9_2 * (int64_t) g7_19; int64_t f9g8_19 = f9 * (int64_t) g8_19; int64_t f9g9_38 = f9_2 * (int64_t) g9_19; int64_t h0 = f0g0 + f1g9_38 + f2g8_19 + f3g7_38 + f4g6_19 + f5g5_38 + f6g4_19 + f7g3_38 + f8g2_19 + f9g1_38; int64_t h1 = f0g1 + f1g0 + f2g9_19 + f3g8_19 + f4g7_19 + f5g6_19 + f6g5_19 + f7g4_19 + f8g3_19 + f9g2_19; int64_t h2 = f0g2 + f1g1_2 + f2g0 + f3g9_38 + f4g8_19 + f5g7_38 + f6g6_19 + f7g5_38 + f8g4_19 + f9g3_38; int64_t h3 = f0g3 + f1g2 + f2g1 + f3g0 + f4g9_19 + f5g8_19 + f6g7_19 + f7g6_19 + f8g5_19 + f9g4_19; int64_t h4 = f0g4 + f1g3_2 + f2g2 + f3g1_2 + f4g0 + f5g9_38 + f6g8_19 + f7g7_38 + f8g6_19 + f9g5_38; int64_t h5 = f0g5 + f1g4 + f2g3 + f3g2 + f4g1 + f5g0 + f6g9_19 + f7g8_19 + f8g7_19 + f9g6_19; int64_t h6 = f0g6 + f1g5_2 + f2g4 + f3g3_2 + f4g2 + f5g1_2 + f6g0 + f7g9_38 + f8g8_19 + f9g7_38; int64_t h7 = f0g7 + f1g6 + f2g5 + f3g4 + f4g3 + f5g2 + f6g1 + f7g0 + f8g9_19 + f9g8_19; int64_t h8 = f0g8 + f1g7_2 + f2g6 + f3g5_2 + f4g4 + f5g3_2 + f6g2 + f7g1_2 + f8g0 + f9g9_38; int64_t h9 = f0g9 + f1g8 + f2g7 + f3g6 + f4g5 + f5g4 + f6g3 + f7g2 + f8g1 + f9g0 ; int64_t carry0; int64_t carry1; int64_t carry2; int64_t carry3; int64_t carry4; int64_t carry5; int64_t carry6; int64_t carry7; int64_t carry8; int64_t carry9; carry0 = (h0 + (int64_t) (1 << 25)) >> 26; h1 += carry0; h0 -= carry0 << 26; carry4 = (h4 + (int64_t) (1 << 25)) >> 26; h5 += carry4; h4 -= carry4 << 26; carry1 = (h1 + (int64_t) (1 << 24)) >> 25; h2 += carry1; h1 -= carry1 << 25; carry5 = (h5 + (int64_t) (1 << 24)) >> 25; h6 += carry5; h5 -= carry5 << 25; carry2 = (h2 + (int64_t) (1 << 25)) >> 26; h3 += carry2; h2 -= carry2 << 26; carry6 = (h6 + (int64_t) (1 << 25)) >> 26; h7 += carry6; h6 -= carry6 << 26; carry3 = (h3 + (int64_t) (1 << 24)) >> 25; h4 += carry3; h3 -= carry3 << 25; carry7 = (h7 + (int64_t) (1 << 24)) >> 25; h8 += carry7; h7 -= carry7 << 25; carry4 = (h4 + (int64_t) (1 << 25)) >> 26; h5 += carry4; h4 -= carry4 << 26; carry8 = (h8 + (int64_t) (1 << 25)) >> 26; h9 += carry8; h8 -= carry8 << 26; carry9 = (h9 + (int64_t) (1 << 24)) >> 25; h0 += carry9 * 19; h9 -= carry9 << 25; carry0 = (h0 + (int64_t) (1 << 25)) >> 26; h1 += carry0; h0 -= carry0 << 26; h[0] = (int32_t) h0; h[1] = (int32_t) h1; h[2] = (int32_t) h2; h[3] = (int32_t) h3; h[4] = (int32_t) h4; h[5] = (int32_t) h5; h[6] = (int32_t) h6; h[7] = (int32_t) h7; h[8] = (int32_t) h8; h[9] = (int32_t) h9; } /* h = f * 121666 Can overlap h with f. Preconditions: |f| bounded by 1.1*2^26,1.1*2^25,1.1*2^26,1.1*2^25,etc. Postconditions: |h| bounded by 1.1*2^25,1.1*2^24,1.1*2^25,1.1*2^24,etc. */ void fe_mul121666(fe h, fe f) { int32_t f0 = f[0]; int32_t f1 = f[1]; int32_t f2 = f[2]; int32_t f3 = f[3]; int32_t f4 = f[4]; int32_t f5 = f[5]; int32_t f6 = f[6]; int32_t f7 = f[7]; int32_t f8 = f[8]; int32_t f9 = f[9]; int64_t h0 = f0 * (int64_t) 121666; int64_t h1 = f1 * (int64_t) 121666; int64_t h2 = f2 * (int64_t) 121666; int64_t h3 = f3 * (int64_t) 121666; int64_t h4 = f4 * (int64_t) 121666; int64_t h5 = f5 * (int64_t) 121666; int64_t h6 = f6 * (int64_t) 121666; int64_t h7 = f7 * (int64_t) 121666; int64_t h8 = f8 * (int64_t) 121666; int64_t h9 = f9 * (int64_t) 121666; int64_t carry0; int64_t carry1; int64_t carry2; int64_t carry3; int64_t carry4; int64_t carry5; int64_t carry6; int64_t carry7; int64_t carry8; int64_t carry9; carry9 = (h9 + (int64_t) (1<<24)) >> 25; h0 += carry9 * 19; h9 -= carry9 << 25; carry1 = (h1 + (int64_t) (1<<24)) >> 25; h2 += carry1; h1 -= carry1 << 25; carry3 = (h3 + (int64_t) (1<<24)) >> 25; h4 += carry3; h3 -= carry3 << 25; carry5 = (h5 + (int64_t) (1<<24)) >> 25; h6 += carry5; h5 -= carry5 << 25; carry7 = (h7 + (int64_t) (1<<24)) >> 25; h8 += carry7; h7 -= carry7 << 25; carry0 = (h0 + (int64_t) (1<<25)) >> 26; h1 += carry0; h0 -= carry0 << 26; carry2 = (h2 + (int64_t) (1<<25)) >> 26; h3 += carry2; h2 -= carry2 << 26; carry4 = (h4 + (int64_t) (1<<25)) >> 26; h5 += carry4; h4 -= carry4 << 26; carry6 = (h6 + (int64_t) (1<<25)) >> 26; h7 += carry6; h6 -= carry6 << 26; carry8 = (h8 + (int64_t) (1<<25)) >> 26; h9 += carry8; h8 -= carry8 << 26; h[0] = (int32_t) h0; h[1] = (int32_t) h1; h[2] = (int32_t) h2; h[3] = (int32_t) h3; h[4] = (int32_t) h4; h[5] = (int32_t) h5; h[6] = (int32_t) h6; h[7] = (int32_t) h7; h[8] = (int32_t) h8; h[9] = (int32_t) h9; } /* h = -f Preconditions: |f| bounded by 1.1*2^25,1.1*2^24,1.1*2^25,1.1*2^24,etc. Postconditions: |h| bounded by 1.1*2^25,1.1*2^24,1.1*2^25,1.1*2^24,etc. */ void fe_neg(fe h, const fe f) { int32_t f0 = f[0]; int32_t f1 = f[1]; int32_t f2 = f[2]; int32_t f3 = f[3]; int32_t f4 = f[4]; int32_t f5 = f[5]; int32_t f6 = f[6]; int32_t f7 = f[7]; int32_t f8 = f[8]; int32_t f9 = f[9]; int32_t h0 = -f0; int32_t h1 = -f1; int32_t h2 = -f2; int32_t h3 = -f3; int32_t h4 = -f4; int32_t h5 = -f5; int32_t h6 = -f6; int32_t h7 = -f7; int32_t h8 = -f8; int32_t h9 = -f9; h[0] = h0; h[1] = h1; h[2] = h2; h[3] = h3; h[4] = h4; h[5] = h5; h[6] = h6; h[7] = h7; h[8] = h8; h[9] = h9; } void fe_pow22523(fe out, const fe z) { fe t0; fe t1; fe t2; int i; fe_sq(t0, z); for (i = 1; i < 1; ++i) { fe_sq(t0, t0); } fe_sq(t1, t0); for (i = 1; i < 2; ++i) { fe_sq(t1, t1); } fe_mul(t1, z, t1); fe_mul(t0, t0, t1); fe_sq(t0, t0); for (i = 1; i < 1; ++i) { fe_sq(t0, t0); } fe_mul(t0, t1, t0); fe_sq(t1, t0); for (i = 1; i < 5; ++i) { fe_sq(t1, t1); } fe_mul(t0, t1, t0); fe_sq(t1, t0); for (i = 1; i < 10; ++i) { fe_sq(t1, t1); } fe_mul(t1, t1, t0); fe_sq(t2, t1); for (i = 1; i < 20; ++i) { fe_sq(t2, t2); } fe_mul(t1, t2, t1); fe_sq(t1, t1); for (i = 1; i < 10; ++i) { fe_sq(t1, t1); } fe_mul(t0, t1, t0); fe_sq(t1, t0); for (i = 1; i < 50; ++i) { fe_sq(t1, t1); } fe_mul(t1, t1, t0); fe_sq(t2, t1); for (i = 1; i < 100; ++i) { fe_sq(t2, t2); } fe_mul(t1, t2, t1); fe_sq(t1, t1); for (i = 1; i < 50; ++i) { fe_sq(t1, t1); } fe_mul(t0, t1, t0); fe_sq(t0, t0); for (i = 1; i < 2; ++i) { fe_sq(t0, t0); } fe_mul(out, t0, z); return; } /* h = f * f Can overlap h with f. Preconditions: |f| bounded by 1.65*2^26,1.65*2^25,1.65*2^26,1.65*2^25,etc. Postconditions: |h| bounded by 1.01*2^25,1.01*2^24,1.01*2^25,1.01*2^24,etc. */ /* See fe_mul.c for discussion of implementation strategy. */ void fe_sq(fe h, const fe f) { int32_t f0 = f[0]; int32_t f1 = f[1]; int32_t f2 = f[2]; int32_t f3 = f[3]; int32_t f4 = f[4]; int32_t f5 = f[5]; int32_t f6 = f[6]; int32_t f7 = f[7]; int32_t f8 = f[8]; int32_t f9 = f[9]; int32_t f0_2 = 2 * f0; int32_t f1_2 = 2 * f1; int32_t f2_2 = 2 * f2; int32_t f3_2 = 2 * f3; int32_t f4_2 = 2 * f4; int32_t f5_2 = 2 * f5; int32_t f6_2 = 2 * f6; int32_t f7_2 = 2 * f7; int32_t f5_38 = 38 * f5; /* 1.959375*2^30 */ int32_t f6_19 = 19 * f6; /* 1.959375*2^30 */ int32_t f7_38 = 38 * f7; /* 1.959375*2^30 */ int32_t f8_19 = 19 * f8; /* 1.959375*2^30 */ int32_t f9_38 = 38 * f9; /* 1.959375*2^30 */ int64_t f0f0 = f0 * (int64_t) f0; int64_t f0f1_2 = f0_2 * (int64_t) f1; int64_t f0f2_2 = f0_2 * (int64_t) f2; int64_t f0f3_2 = f0_2 * (int64_t) f3; int64_t f0f4_2 = f0_2 * (int64_t) f4; int64_t f0f5_2 = f0_2 * (int64_t) f5; int64_t f0f6_2 = f0_2 * (int64_t) f6; int64_t f0f7_2 = f0_2 * (int64_t) f7; int64_t f0f8_2 = f0_2 * (int64_t) f8; int64_t f0f9_2 = f0_2 * (int64_t) f9; int64_t f1f1_2 = f1_2 * (int64_t) f1; int64_t f1f2_2 = f1_2 * (int64_t) f2; int64_t f1f3_4 = f1_2 * (int64_t) f3_2; int64_t f1f4_2 = f1_2 * (int64_t) f4; int64_t f1f5_4 = f1_2 * (int64_t) f5_2; int64_t f1f6_2 = f1_2 * (int64_t) f6; int64_t f1f7_4 = f1_2 * (int64_t) f7_2; int64_t f1f8_2 = f1_2 * (int64_t) f8; int64_t f1f9_76 = f1_2 * (int64_t) f9_38; int64_t f2f2 = f2 * (int64_t) f2; int64_t f2f3_2 = f2_2 * (int64_t) f3; int64_t f2f4_2 = f2_2 * (int64_t) f4; int64_t f2f5_2 = f2_2 * (int64_t) f5; int64_t f2f6_2 = f2_2 * (int64_t) f6; int64_t f2f7_2 = f2_2 * (int64_t) f7; int64_t f2f8_38 = f2_2 * (int64_t) f8_19; int64_t f2f9_38 = f2 * (int64_t) f9_38; int64_t f3f3_2 = f3_2 * (int64_t) f3; int64_t f3f4_2 = f3_2 * (int64_t) f4; int64_t f3f5_4 = f3_2 * (int64_t) f5_2; int64_t f3f6_2 = f3_2 * (int64_t) f6; int64_t f3f7_76 = f3_2 * (int64_t) f7_38; int64_t f3f8_38 = f3_2 * (int64_t) f8_19; int64_t f3f9_76 = f3_2 * (int64_t) f9_38; int64_t f4f4 = f4 * (int64_t) f4; int64_t f4f5_2 = f4_2 * (int64_t) f5; int64_t f4f6_38 = f4_2 * (int64_t) f6_19; int64_t f4f7_38 = f4 * (int64_t) f7_38; int64_t f4f8_38 = f4_2 * (int64_t) f8_19; int64_t f4f9_38 = f4 * (int64_t) f9_38; int64_t f5f5_38 = f5 * (int64_t) f5_38; int64_t f5f6_38 = f5_2 * (int64_t) f6_19; int64_t f5f7_76 = f5_2 * (int64_t) f7_38; int64_t f5f8_38 = f5_2 * (int64_t) f8_19; int64_t f5f9_76 = f5_2 * (int64_t) f9_38; int64_t f6f6_19 = f6 * (int64_t) f6_19; int64_t f6f7_38 = f6 * (int64_t) f7_38; int64_t f6f8_38 = f6_2 * (int64_t) f8_19; int64_t f6f9_38 = f6 * (int64_t) f9_38; int64_t f7f7_38 = f7 * (int64_t) f7_38; int64_t f7f8_38 = f7_2 * (int64_t) f8_19; int64_t f7f9_76 = f7_2 * (int64_t) f9_38; int64_t f8f8_19 = f8 * (int64_t) f8_19; int64_t f8f9_38 = f8 * (int64_t) f9_38; int64_t f9f9_38 = f9 * (int64_t) f9_38; int64_t h0 = f0f0 + f1f9_76 + f2f8_38 + f3f7_76 + f4f6_38 + f5f5_38; int64_t h1 = f0f1_2 + f2f9_38 + f3f8_38 + f4f7_38 + f5f6_38; int64_t h2 = f0f2_2 + f1f1_2 + f3f9_76 + f4f8_38 + f5f7_76 + f6f6_19; int64_t h3 = f0f3_2 + f1f2_2 + f4f9_38 + f5f8_38 + f6f7_38; int64_t h4 = f0f4_2 + f1f3_4 + f2f2 + f5f9_76 + f6f8_38 + f7f7_38; int64_t h5 = f0f5_2 + f1f4_2 + f2f3_2 + f6f9_38 + f7f8_38; int64_t h6 = f0f6_2 + f1f5_4 + f2f4_2 + f3f3_2 + f7f9_76 + f8f8_19; int64_t h7 = f0f7_2 + f1f6_2 + f2f5_2 + f3f4_2 + f8f9_38; int64_t h8 = f0f8_2 + f1f7_4 + f2f6_2 + f3f5_4 + f4f4 + f9f9_38; int64_t h9 = f0f9_2 + f1f8_2 + f2f7_2 + f3f6_2 + f4f5_2; int64_t carry0; int64_t carry1; int64_t carry2; int64_t carry3; int64_t carry4; int64_t carry5; int64_t carry6; int64_t carry7; int64_t carry8; int64_t carry9; carry0 = (h0 + (int64_t) (1 << 25)) >> 26; h1 += carry0; h0 -= carry0 << 26; carry4 = (h4 + (int64_t) (1 << 25)) >> 26; h5 += carry4; h4 -= carry4 << 26; carry1 = (h1 + (int64_t) (1 << 24)) >> 25; h2 += carry1; h1 -= carry1 << 25; carry5 = (h5 + (int64_t) (1 << 24)) >> 25; h6 += carry5; h5 -= carry5 << 25; carry2 = (h2 + (int64_t) (1 << 25)) >> 26; h3 += carry2; h2 -= carry2 << 26; carry6 = (h6 + (int64_t) (1 << 25)) >> 26; h7 += carry6; h6 -= carry6 << 26; carry3 = (h3 + (int64_t) (1 << 24)) >> 25; h4 += carry3; h3 -= carry3 << 25; carry7 = (h7 + (int64_t) (1 << 24)) >> 25; h8 += carry7; h7 -= carry7 << 25; carry4 = (h4 + (int64_t) (1 << 25)) >> 26; h5 += carry4; h4 -= carry4 << 26; carry8 = (h8 + (int64_t) (1 << 25)) >> 26; h9 += carry8; h8 -= carry8 << 26; carry9 = (h9 + (int64_t) (1 << 24)) >> 25; h0 += carry9 * 19; h9 -= carry9 << 25; carry0 = (h0 + (int64_t) (1 << 25)) >> 26; h1 += carry0; h0 -= carry0 << 26; h[0] = (int32_t) h0; h[1] = (int32_t) h1; h[2] = (int32_t) h2; h[3] = (int32_t) h3; h[4] = (int32_t) h4; h[5] = (int32_t) h5; h[6] = (int32_t) h6; h[7] = (int32_t) h7; h[8] = (int32_t) h8; h[9] = (int32_t) h9; } /* h = 2 * f * f Can overlap h with f. Preconditions: |f| bounded by 1.65*2^26,1.65*2^25,1.65*2^26,1.65*2^25,etc. Postconditions: |h| bounded by 1.01*2^25,1.01*2^24,1.01*2^25,1.01*2^24,etc. */ /* See fe_mul.c for discussion of implementation strategy. */ void fe_sq2(fe h, const fe f) { int32_t f0 = f[0]; int32_t f1 = f[1]; int32_t f2 = f[2]; int32_t f3 = f[3]; int32_t f4 = f[4]; int32_t f5 = f[5]; int32_t f6 = f[6]; int32_t f7 = f[7]; int32_t f8 = f[8]; int32_t f9 = f[9]; int32_t f0_2 = 2 * f0; int32_t f1_2 = 2 * f1; int32_t f2_2 = 2 * f2; int32_t f3_2 = 2 * f3; int32_t f4_2 = 2 * f4; int32_t f5_2 = 2 * f5; int32_t f6_2 = 2 * f6; int32_t f7_2 = 2 * f7; int32_t f5_38 = 38 * f5; /* 1.959375*2^30 */ int32_t f6_19 = 19 * f6; /* 1.959375*2^30 */ int32_t f7_38 = 38 * f7; /* 1.959375*2^30 */ int32_t f8_19 = 19 * f8; /* 1.959375*2^30 */ int32_t f9_38 = 38 * f9; /* 1.959375*2^30 */ int64_t f0f0 = f0 * (int64_t) f0; int64_t f0f1_2 = f0_2 * (int64_t) f1; int64_t f0f2_2 = f0_2 * (int64_t) f2; int64_t f0f3_2 = f0_2 * (int64_t) f3; int64_t f0f4_2 = f0_2 * (int64_t) f4; int64_t f0f5_2 = f0_2 * (int64_t) f5; int64_t f0f6_2 = f0_2 * (int64_t) f6; int64_t f0f7_2 = f0_2 * (int64_t) f7; int64_t f0f8_2 = f0_2 * (int64_t) f8; int64_t f0f9_2 = f0_2 * (int64_t) f9; int64_t f1f1_2 = f1_2 * (int64_t) f1; int64_t f1f2_2 = f1_2 * (int64_t) f2; int64_t f1f3_4 = f1_2 * (int64_t) f3_2; int64_t f1f4_2 = f1_2 * (int64_t) f4; int64_t f1f5_4 = f1_2 * (int64_t) f5_2; int64_t f1f6_2 = f1_2 * (int64_t) f6; int64_t f1f7_4 = f1_2 * (int64_t) f7_2; int64_t f1f8_2 = f1_2 * (int64_t) f8; int64_t f1f9_76 = f1_2 * (int64_t) f9_38; int64_t f2f2 = f2 * (int64_t) f2; int64_t f2f3_2 = f2_2 * (int64_t) f3; int64_t f2f4_2 = f2_2 * (int64_t) f4; int64_t f2f5_2 = f2_2 * (int64_t) f5; int64_t f2f6_2 = f2_2 * (int64_t) f6; int64_t f2f7_2 = f2_2 * (int64_t) f7; int64_t f2f8_38 = f2_2 * (int64_t) f8_19; int64_t f2f9_38 = f2 * (int64_t) f9_38; int64_t f3f3_2 = f3_2 * (int64_t) f3; int64_t f3f4_2 = f3_2 * (int64_t) f4; int64_t f3f5_4 = f3_2 * (int64_t) f5_2; int64_t f3f6_2 = f3_2 * (int64_t) f6; int64_t f3f7_76 = f3_2 * (int64_t) f7_38; int64_t f3f8_38 = f3_2 * (int64_t) f8_19; int64_t f3f9_76 = f3_2 * (int64_t) f9_38; int64_t f4f4 = f4 * (int64_t) f4; int64_t f4f5_2 = f4_2 * (int64_t) f5; int64_t f4f6_38 = f4_2 * (int64_t) f6_19; int64_t f4f7_38 = f4 * (int64_t) f7_38; int64_t f4f8_38 = f4_2 * (int64_t) f8_19; int64_t f4f9_38 = f4 * (int64_t) f9_38; int64_t f5f5_38 = f5 * (int64_t) f5_38; int64_t f5f6_38 = f5_2 * (int64_t) f6_19; int64_t f5f7_76 = f5_2 * (int64_t) f7_38; int64_t f5f8_38 = f5_2 * (int64_t) f8_19; int64_t f5f9_76 = f5_2 * (int64_t) f9_38; int64_t f6f6_19 = f6 * (int64_t) f6_19; int64_t f6f7_38 = f6 * (int64_t) f7_38; int64_t f6f8_38 = f6_2 * (int64_t) f8_19; int64_t f6f9_38 = f6 * (int64_t) f9_38; int64_t f7f7_38 = f7 * (int64_t) f7_38; int64_t f7f8_38 = f7_2 * (int64_t) f8_19; int64_t f7f9_76 = f7_2 * (int64_t) f9_38; int64_t f8f8_19 = f8 * (int64_t) f8_19; int64_t f8f9_38 = f8 * (int64_t) f9_38; int64_t f9f9_38 = f9 * (int64_t) f9_38; int64_t h0 = f0f0 + f1f9_76 + f2f8_38 + f3f7_76 + f4f6_38 + f5f5_38; int64_t h1 = f0f1_2 + f2f9_38 + f3f8_38 + f4f7_38 + f5f6_38; int64_t h2 = f0f2_2 + f1f1_2 + f3f9_76 + f4f8_38 + f5f7_76 + f6f6_19; int64_t h3 = f0f3_2 + f1f2_2 + f4f9_38 + f5f8_38 + f6f7_38; int64_t h4 = f0f4_2 + f1f3_4 + f2f2 + f5f9_76 + f6f8_38 + f7f7_38; int64_t h5 = f0f5_2 + f1f4_2 + f2f3_2 + f6f9_38 + f7f8_38; int64_t h6 = f0f6_2 + f1f5_4 + f2f4_2 + f3f3_2 + f7f9_76 + f8f8_19; int64_t h7 = f0f7_2 + f1f6_2 + f2f5_2 + f3f4_2 + f8f9_38; int64_t h8 = f0f8_2 + f1f7_4 + f2f6_2 + f3f5_4 + f4f4 + f9f9_38; int64_t h9 = f0f9_2 + f1f8_2 + f2f7_2 + f3f6_2 + f4f5_2; int64_t carry0; int64_t carry1; int64_t carry2; int64_t carry3; int64_t carry4; int64_t carry5; int64_t carry6; int64_t carry7; int64_t carry8; int64_t carry9; h0 += h0; h1 += h1; h2 += h2; h3 += h3; h4 += h4; h5 += h5; h6 += h6; h7 += h7; h8 += h8; h9 += h9; carry0 = (h0 + (int64_t) (1 << 25)) >> 26; h1 += carry0; h0 -= carry0 << 26; carry4 = (h4 + (int64_t) (1 << 25)) >> 26; h5 += carry4; h4 -= carry4 << 26; carry1 = (h1 + (int64_t) (1 << 24)) >> 25; h2 += carry1; h1 -= carry1 << 25; carry5 = (h5 + (int64_t) (1 << 24)) >> 25; h6 += carry5; h5 -= carry5 << 25; carry2 = (h2 + (int64_t) (1 << 25)) >> 26; h3 += carry2; h2 -= carry2 << 26; carry6 = (h6 + (int64_t) (1 << 25)) >> 26; h7 += carry6; h6 -= carry6 << 26; carry3 = (h3 + (int64_t) (1 << 24)) >> 25; h4 += carry3; h3 -= carry3 << 25; carry7 = (h7 + (int64_t) (1 << 24)) >> 25; h8 += carry7; h7 -= carry7 << 25; carry4 = (h4 + (int64_t) (1 << 25)) >> 26; h5 += carry4; h4 -= carry4 << 26; carry8 = (h8 + (int64_t) (1 << 25)) >> 26; h9 += carry8; h8 -= carry8 << 26; carry9 = (h9 + (int64_t) (1 << 24)) >> 25; h0 += carry9 * 19; h9 -= carry9 << 25; carry0 = (h0 + (int64_t) (1 << 25)) >> 26; h1 += carry0; h0 -= carry0 << 26; h[0] = (int32_t) h0; h[1] = (int32_t) h1; h[2] = (int32_t) h2; h[3] = (int32_t) h3; h[4] = (int32_t) h4; h[5] = (int32_t) h5; h[6] = (int32_t) h6; h[7] = (int32_t) h7; h[8] = (int32_t) h8; h[9] = (int32_t) h9; } /* h = f - g Can overlap h with f or g. Preconditions: |f| bounded by 1.1*2^25,1.1*2^24,1.1*2^25,1.1*2^24,etc. |g| bounded by 1.1*2^25,1.1*2^24,1.1*2^25,1.1*2^24,etc. Postconditions: |h| bounded by 1.1*2^26,1.1*2^25,1.1*2^26,1.1*2^25,etc. */ void fe_sub(fe h, const fe f, const fe g) { int32_t f0 = f[0]; int32_t f1 = f[1]; int32_t f2 = f[2]; int32_t f3 = f[3]; int32_t f4 = f[4]; int32_t f5 = f[5]; int32_t f6 = f[6]; int32_t f7 = f[7]; int32_t f8 = f[8]; int32_t f9 = f[9]; int32_t g0 = g[0]; int32_t g1 = g[1]; int32_t g2 = g[2]; int32_t g3 = g[3]; int32_t g4 = g[4]; int32_t g5 = g[5]; int32_t g6 = g[6]; int32_t g7 = g[7]; int32_t g8 = g[8]; int32_t g9 = g[9]; int32_t h0 = f0 - g0; int32_t h1 = f1 - g1; int32_t h2 = f2 - g2; int32_t h3 = f3 - g3; int32_t h4 = f4 - g4; int32_t h5 = f5 - g5; int32_t h6 = f6 - g6; int32_t h7 = f7 - g7; int32_t h8 = f8 - g8; int32_t h9 = f9 - g9; h[0] = h0; h[1] = h1; h[2] = h2; h[3] = h3; h[4] = h4; h[5] = h5; h[6] = h6; h[7] = h7; h[8] = h8; h[9] = h9; } /* Preconditions: |h| bounded by 1.1*2^26,1.1*2^25,1.1*2^26,1.1*2^25,etc. Write p=2^255-19; q=floor(h/p). Basic claim: q = floor(2^(-255)(h + 19 2^(-25)h9 + 2^(-1))). Proof: Have |h|<=p so |q|<=1 so |19^2 2^(-255) q|<1/4. Also have |h-2^230 h9|<2^231 so |19 2^(-255)(h-2^230 h9)|<1/4. Write y=2^(-1)-19^2 2^(-255)q-19 2^(-255)(h-2^230 h9). Then 0> 25; q = (h0 + q) >> 26; q = (h1 + q) >> 25; q = (h2 + q) >> 26; q = (h3 + q) >> 25; q = (h4 + q) >> 26; q = (h5 + q) >> 25; q = (h6 + q) >> 26; q = (h7 + q) >> 25; q = (h8 + q) >> 26; q = (h9 + q) >> 25; /* Goal: Output h-(2^255-19)q, which is between 0 and 2^255-20. */ h0 += 19 * q; /* Goal: Output h-2^255 q, which is between 0 and 2^255-20. */ carry0 = h0 >> 26; h1 += carry0; h0 -= carry0 << 26; carry1 = h1 >> 25; h2 += carry1; h1 -= carry1 << 25; carry2 = h2 >> 26; h3 += carry2; h2 -= carry2 << 26; carry3 = h3 >> 25; h4 += carry3; h3 -= carry3 << 25; carry4 = h4 >> 26; h5 += carry4; h4 -= carry4 << 26; carry5 = h5 >> 25; h6 += carry5; h5 -= carry5 << 25; carry6 = h6 >> 26; h7 += carry6; h6 -= carry6 << 26; carry7 = h7 >> 25; h8 += carry7; h7 -= carry7 << 25; carry8 = h8 >> 26; h9 += carry8; h8 -= carry8 << 26; carry9 = h9 >> 25; h9 -= carry9 << 25; /* h10 = carry9 */ /* Goal: Output h0+...+2^255 h10-2^255 q, which is between 0 and 2^255-20. Have h0+...+2^230 h9 between 0 and 2^255-1; evidently 2^255 h10-2^255 q = 0. Goal: Output h0+...+2^230 h9. */ s[0] = (unsigned char) (h0 >> 0); s[1] = (unsigned char) (h0 >> 8); s[2] = (unsigned char) (h0 >> 16); s[3] = (unsigned char) ((h0 >> 24) | (h1 << 2)); s[4] = (unsigned char) (h1 >> 6); s[5] = (unsigned char) (h1 >> 14); s[6] = (unsigned char) ((h1 >> 22) | (h2 << 3)); s[7] = (unsigned char) (h2 >> 5); s[8] = (unsigned char) (h2 >> 13); s[9] = (unsigned char) ((h2 >> 21) | (h3 << 5)); s[10] = (unsigned char) (h3 >> 3); s[11] = (unsigned char) (h3 >> 11); s[12] = (unsigned char) ((h3 >> 19) | (h4 << 6)); s[13] = (unsigned char) (h4 >> 2); s[14] = (unsigned char) (h4 >> 10); s[15] = (unsigned char) (h4 >> 18); s[16] = (unsigned char) (h5 >> 0); s[17] = (unsigned char) (h5 >> 8); s[18] = (unsigned char) (h5 >> 16); s[19] = (unsigned char) ((h5 >> 24) | (h6 << 1)); s[20] = (unsigned char) (h6 >> 7); s[21] = (unsigned char) (h6 >> 15); s[22] = (unsigned char) ((h6 >> 23) | (h7 << 3)); s[23] = (unsigned char) (h7 >> 5); s[24] = (unsigned char) (h7 >> 13); s[25] = (unsigned char) ((h7 >> 21) | (h8 << 4)); s[26] = (unsigned char) (h8 >> 4); s[27] = (unsigned char) (h8 >> 12); s[28] = (unsigned char) ((h8 >> 20) | (h9 << 6)); s[29] = (unsigned char) (h9 >> 2); s[30] = (unsigned char) (h9 >> 10); s[31] = (unsigned char) (h9 >> 18); } ================================================ FILE: Vendor/ed25519-sparkle/src/fe.h ================================================ #ifndef FE_H #define FE_H #include "fixedint.h" /* fe means field element. Here the field is \Z/(2^255-19). An element t, entries t[0]...t[9], represents the integer t[0]+2^26 t[1]+2^51 t[2]+2^77 t[3]+2^102 t[4]+...+2^230 t[9]. Bounds on each t[i] vary depending on context. */ typedef int32_t fe[10]; void fe_0(fe h); void fe_1(fe h); void fe_frombytes(fe h, const unsigned char *s); void fe_tobytes(unsigned char *s, const fe h); void fe_copy(fe h, const fe f); int fe_isnegative(const fe f); int fe_isnonzero(const fe f); void fe_cmov(fe f, const fe g, unsigned int b); void fe_cswap(fe f, fe g, unsigned int b); void fe_neg(fe h, const fe f); void fe_add(fe h, const fe f, const fe g); void fe_invert(fe out, const fe z); void fe_sq(fe h, const fe f); void fe_sq2(fe h, const fe f); void fe_mul(fe h, const fe f, const fe g); void fe_mul121666(fe h, fe f); void fe_pow22523(fe out, const fe z); void fe_sub(fe h, const fe f, const fe g); #endif ================================================ FILE: Vendor/ed25519-sparkle/src/fixedint.h ================================================ /* Portable header to provide the 32 and 64 bits type. Not a compatible replacement for , do not blindly use it as such. */ #if ((defined(__STDC__) && __STDC__ && __STDC_VERSION__ >= 199901L) || (defined(__WATCOMC__) && (defined(_STDINT_H_INCLUDED) || __WATCOMC__ >= 1250)) || (defined(__GNUC__) && (defined(_STDINT_H) || defined(_STDINT_H_) || defined(__UINT_FAST64_TYPE__)) )) && !defined(FIXEDINT_H_INCLUDED) #include #define FIXEDINT_H_INCLUDED #if defined(__WATCOMC__) && __WATCOMC__ >= 1250 && !defined(UINT64_C) #include #define UINT64_C(x) (x + (UINT64_MAX - UINT64_MAX)) #endif #endif #ifndef FIXEDINT_H_INCLUDED #define FIXEDINT_H_INCLUDED #include /* (u)int32_t */ #ifndef uint32_t #if (ULONG_MAX == 0xffffffffUL) typedef unsigned long uint32_t; #elif (UINT_MAX == 0xffffffffUL) typedef unsigned int uint32_t; #elif (USHRT_MAX == 0xffffffffUL) typedef unsigned short uint32_t; #endif #endif #ifndef int32_t #if (LONG_MAX == 0x7fffffffL) typedef signed long int32_t; #elif (INT_MAX == 0x7fffffffL) typedef signed int int32_t; #elif (SHRT_MAX == 0x7fffffffL) typedef signed short int32_t; #endif #endif /* (u)int64_t */ #if (defined(__STDC__) && defined(__STDC_VERSION__) && __STDC__ && __STDC_VERSION__ >= 199901L) typedef long long int64_t; typedef unsigned long long uint64_t; #define UINT64_C(v) v ##ULL #define INT64_C(v) v ##LL #elif defined(__GNUC__) __extension__ typedef long long int64_t; __extension__ typedef unsigned long long uint64_t; #define UINT64_C(v) v ##ULL #define INT64_C(v) v ##LL #elif defined(__MWERKS__) || defined(__SUNPRO_C) || defined(__SUNPRO_CC) || defined(__APPLE_CC__) || defined(_LONG_LONG) || defined(_CRAYC) typedef long long int64_t; typedef unsigned long long uint64_t; #define UINT64_C(v) v ##ULL #define INT64_C(v) v ##LL #elif (defined(__WATCOMC__) && defined(__WATCOM_INT64__)) || (defined(_MSC_VER) && _INTEGRAL_MAX_BITS >= 64) || (defined(__BORLANDC__) && __BORLANDC__ > 0x460) || defined(__alpha) || defined(__DECC) typedef __int64 int64_t; typedef unsigned __int64 uint64_t; #define UINT64_C(v) v ##UI64 #define INT64_C(v) v ##I64 #endif #endif ================================================ FILE: Vendor/ed25519-sparkle/src/ge.c ================================================ #include "ge.h" #include "precomp_data.h" /* r = p + q */ void ge_add(ge_p1p1 *r, const ge_p3 *p, const ge_cached *q) { fe t0; fe_add(r->X, p->Y, p->X); fe_sub(r->Y, p->Y, p->X); fe_mul(r->Z, r->X, q->YplusX); fe_mul(r->Y, r->Y, q->YminusX); fe_mul(r->T, q->T2d, p->T); fe_mul(r->X, p->Z, q->Z); fe_add(t0, r->X, r->X); fe_sub(r->X, r->Z, r->Y); fe_add(r->Y, r->Z, r->Y); fe_add(r->Z, t0, r->T); fe_sub(r->T, t0, r->T); } static void slide(signed char *r, const unsigned char *a) { int i; int b; int k; for (i = 0; i < 256; ++i) { r[i] = 1 & (a[i >> 3] >> (i & 7)); } for (i = 0; i < 256; ++i) if (r[i]) { for (b = 1; b <= 6 && i + b < 256; ++b) { if (r[i + b]) { if (r[i] + (r[i + b] << b) <= 15) { r[i] += r[i + b] << b; r[i + b] = 0; } else if (r[i] - (r[i + b] << b) >= -15) { r[i] -= r[i + b] << b; for (k = i + b; k < 256; ++k) { if (!r[k]) { r[k] = 1; break; } r[k] = 0; } } else { break; } } } } } /* r = a * A + b * B where a = a[0]+256*a[1]+...+256^31 a[31]. and b = b[0]+256*b[1]+...+256^31 b[31]. B is the Ed25519 base point (x,4/5) with x positive. */ void ge_double_scalarmult_vartime(ge_p2 *r, const unsigned char *a, const ge_p3 *A, const unsigned char *b) { signed char aslide[256]; signed char bslide[256]; ge_cached Ai[8]; /* A,3A,5A,7A,9A,11A,13A,15A */ ge_p1p1 t; ge_p3 u; ge_p3 A2; int i; slide(aslide, a); slide(bslide, b); ge_p3_to_cached(&Ai[0], A); ge_p3_dbl(&t, A); ge_p1p1_to_p3(&A2, &t); ge_add(&t, &A2, &Ai[0]); ge_p1p1_to_p3(&u, &t); ge_p3_to_cached(&Ai[1], &u); ge_add(&t, &A2, &Ai[1]); ge_p1p1_to_p3(&u, &t); ge_p3_to_cached(&Ai[2], &u); ge_add(&t, &A2, &Ai[2]); ge_p1p1_to_p3(&u, &t); ge_p3_to_cached(&Ai[3], &u); ge_add(&t, &A2, &Ai[3]); ge_p1p1_to_p3(&u, &t); ge_p3_to_cached(&Ai[4], &u); ge_add(&t, &A2, &Ai[4]); ge_p1p1_to_p3(&u, &t); ge_p3_to_cached(&Ai[5], &u); ge_add(&t, &A2, &Ai[5]); ge_p1p1_to_p3(&u, &t); ge_p3_to_cached(&Ai[6], &u); ge_add(&t, &A2, &Ai[6]); ge_p1p1_to_p3(&u, &t); ge_p3_to_cached(&Ai[7], &u); ge_p2_0(r); for (i = 255; i >= 0; --i) { if (aslide[i] || bslide[i]) { break; } } for (; i >= 0; --i) { ge_p2_dbl(&t, r); if (aslide[i] > 0) { ge_p1p1_to_p3(&u, &t); ge_add(&t, &u, &Ai[aslide[i] / 2]); } else if (aslide[i] < 0) { ge_p1p1_to_p3(&u, &t); ge_sub(&t, &u, &Ai[(-aslide[i]) / 2]); } if (bslide[i] > 0) { ge_p1p1_to_p3(&u, &t); ge_madd(&t, &u, &Bi[bslide[i] / 2]); } else if (bslide[i] < 0) { ge_p1p1_to_p3(&u, &t); ge_msub(&t, &u, &Bi[(-bslide[i]) / 2]); } ge_p1p1_to_p2(r, &t); } } static const fe d = { -10913610, 13857413, -15372611, 6949391, 114729, -8787816, -6275908, -3247719, -18696448, -12055116 }; static const fe sqrtm1 = { -32595792, -7943725, 9377950, 3500415, 12389472, -272473, -25146209, -2005654, 326686, 11406482 }; int ge_frombytes_negate_vartime(ge_p3 *h, const unsigned char *s) { fe u; fe v; fe v3; fe vxx; fe check; fe_frombytes(h->Y, s); fe_1(h->Z); fe_sq(u, h->Y); fe_mul(v, u, d); fe_sub(u, u, h->Z); /* u = y^2-1 */ fe_add(v, v, h->Z); /* v = dy^2+1 */ fe_sq(v3, v); fe_mul(v3, v3, v); /* v3 = v^3 */ fe_sq(h->X, v3); fe_mul(h->X, h->X, v); fe_mul(h->X, h->X, u); /* x = uv^7 */ fe_pow22523(h->X, h->X); /* x = (uv^7)^((q-5)/8) */ fe_mul(h->X, h->X, v3); fe_mul(h->X, h->X, u); /* x = uv^3(uv^7)^((q-5)/8) */ fe_sq(vxx, h->X); fe_mul(vxx, vxx, v); fe_sub(check, vxx, u); /* vx^2-u */ if (fe_isnonzero(check)) { fe_add(check, vxx, u); /* vx^2+u */ if (fe_isnonzero(check)) { return -1; } fe_mul(h->X, h->X, sqrtm1); } if (fe_isnegative(h->X) == (s[31] >> 7)) { fe_neg(h->X, h->X); } fe_mul(h->T, h->X, h->Y); return 0; } /* r = p + q */ void ge_madd(ge_p1p1 *r, const ge_p3 *p, const ge_precomp *q) { fe t0; fe_add(r->X, p->Y, p->X); fe_sub(r->Y, p->Y, p->X); fe_mul(r->Z, r->X, q->yplusx); fe_mul(r->Y, r->Y, q->yminusx); fe_mul(r->T, q->xy2d, p->T); fe_add(t0, p->Z, p->Z); fe_sub(r->X, r->Z, r->Y); fe_add(r->Y, r->Z, r->Y); fe_add(r->Z, t0, r->T); fe_sub(r->T, t0, r->T); } /* r = p - q */ void ge_msub(ge_p1p1 *r, const ge_p3 *p, const ge_precomp *q) { fe t0; fe_add(r->X, p->Y, p->X); fe_sub(r->Y, p->Y, p->X); fe_mul(r->Z, r->X, q->yminusx); fe_mul(r->Y, r->Y, q->yplusx); fe_mul(r->T, q->xy2d, p->T); fe_add(t0, p->Z, p->Z); fe_sub(r->X, r->Z, r->Y); fe_add(r->Y, r->Z, r->Y); fe_sub(r->Z, t0, r->T); fe_add(r->T, t0, r->T); } /* r = p */ void ge_p1p1_to_p2(ge_p2 *r, const ge_p1p1 *p) { fe_mul(r->X, p->X, p->T); fe_mul(r->Y, p->Y, p->Z); fe_mul(r->Z, p->Z, p->T); } /* r = p */ void ge_p1p1_to_p3(ge_p3 *r, const ge_p1p1 *p) { fe_mul(r->X, p->X, p->T); fe_mul(r->Y, p->Y, p->Z); fe_mul(r->Z, p->Z, p->T); fe_mul(r->T, p->X, p->Y); } void ge_p2_0(ge_p2 *h) { fe_0(h->X); fe_1(h->Y); fe_1(h->Z); } /* r = 2 * p */ void ge_p2_dbl(ge_p1p1 *r, const ge_p2 *p) { fe t0; fe_sq(r->X, p->X); fe_sq(r->Z, p->Y); fe_sq2(r->T, p->Z); fe_add(r->Y, p->X, p->Y); fe_sq(t0, r->Y); fe_add(r->Y, r->Z, r->X); fe_sub(r->Z, r->Z, r->X); fe_sub(r->X, t0, r->Y); fe_sub(r->T, r->T, r->Z); } void ge_p3_0(ge_p3 *h) { fe_0(h->X); fe_1(h->Y); fe_1(h->Z); fe_0(h->T); } /* r = 2 * p */ void ge_p3_dbl(ge_p1p1 *r, const ge_p3 *p) { ge_p2 q; ge_p3_to_p2(&q, p); ge_p2_dbl(r, &q); } /* r = p */ static const fe d2 = { -21827239, -5839606, -30745221, 13898782, 229458, 15978800, -12551817, -6495438, 29715968, 9444199 }; void ge_p3_to_cached(ge_cached *r, const ge_p3 *p) { fe_add(r->YplusX, p->Y, p->X); fe_sub(r->YminusX, p->Y, p->X); fe_copy(r->Z, p->Z); fe_mul(r->T2d, p->T, d2); } /* r = p */ void ge_p3_to_p2(ge_p2 *r, const ge_p3 *p) { fe_copy(r->X, p->X); fe_copy(r->Y, p->Y); fe_copy(r->Z, p->Z); } void ge_p3_tobytes(unsigned char *s, const ge_p3 *h) { fe recip; fe x; fe y; fe_invert(recip, h->Z); fe_mul(x, h->X, recip); fe_mul(y, h->Y, recip); fe_tobytes(s, y); s[31] ^= fe_isnegative(x) << 7; } static unsigned char equal(signed char b, signed char c) { unsigned char ub = b; unsigned char uc = c; unsigned char x = ub ^ uc; /* 0: yes; 1..255: no */ uint64_t y = x; /* 0: yes; 1..255: no */ y -= 1; /* large: yes; 0..254: no */ y >>= 63; /* 1: yes; 0: no */ return (unsigned char) y; } static unsigned char negative(signed char b) { uint64_t x = b; /* 18446744073709551361..18446744073709551615: yes; 0..255: no */ x >>= 63; /* 1: yes; 0: no */ return (unsigned char) x; } static void cmov(ge_precomp *t, const ge_precomp *u, unsigned char b) { fe_cmov(t->yplusx, u->yplusx, b); fe_cmov(t->yminusx, u->yminusx, b); fe_cmov(t->xy2d, u->xy2d, b); } static void select(ge_precomp *t, int pos, signed char b) { ge_precomp minust; unsigned char bnegative = negative(b); unsigned char babs = b - (((-bnegative) & b) << 1); fe_1(t->yplusx); fe_1(t->yminusx); fe_0(t->xy2d); cmov(t, &base[pos][0], equal(babs, 1)); cmov(t, &base[pos][1], equal(babs, 2)); cmov(t, &base[pos][2], equal(babs, 3)); cmov(t, &base[pos][3], equal(babs, 4)); cmov(t, &base[pos][4], equal(babs, 5)); cmov(t, &base[pos][5], equal(babs, 6)); cmov(t, &base[pos][6], equal(babs, 7)); cmov(t, &base[pos][7], equal(babs, 8)); fe_copy(minust.yplusx, t->yminusx); fe_copy(minust.yminusx, t->yplusx); fe_neg(minust.xy2d, t->xy2d); cmov(t, &minust, bnegative); } /* h = a * B where a = a[0]+256*a[1]+...+256^31 a[31] B is the Ed25519 base point (x,4/5) with x positive. Preconditions: a[31] <= 127 */ void ge_scalarmult_base(ge_p3 *h, const unsigned char *a) { signed char e[64]; signed char carry; ge_p1p1 r; ge_p2 s; ge_precomp t; int i; for (i = 0; i < 32; ++i) { e[2 * i + 0] = (a[i] >> 0) & 15; e[2 * i + 1] = (a[i] >> 4) & 15; } /* each e[i] is between 0 and 15 */ /* e[63] is between 0 and 7 */ carry = 0; for (i = 0; i < 63; ++i) { e[i] += carry; carry = e[i] + 8; carry >>= 4; e[i] -= carry << 4; } e[63] += carry; /* each e[i] is between -8 and 8 */ ge_p3_0(h); for (i = 1; i < 64; i += 2) { select(&t, i / 2, e[i]); ge_madd(&r, h, &t); ge_p1p1_to_p3(h, &r); } ge_p3_dbl(&r, h); ge_p1p1_to_p2(&s, &r); ge_p2_dbl(&r, &s); ge_p1p1_to_p2(&s, &r); ge_p2_dbl(&r, &s); ge_p1p1_to_p2(&s, &r); ge_p2_dbl(&r, &s); ge_p1p1_to_p3(h, &r); for (i = 0; i < 64; i += 2) { select(&t, i / 2, e[i]); ge_madd(&r, h, &t); ge_p1p1_to_p3(h, &r); } } /* r = p - q */ void ge_sub(ge_p1p1 *r, const ge_p3 *p, const ge_cached *q) { fe t0; fe_add(r->X, p->Y, p->X); fe_sub(r->Y, p->Y, p->X); fe_mul(r->Z, r->X, q->YminusX); fe_mul(r->Y, r->Y, q->YplusX); fe_mul(r->T, q->T2d, p->T); fe_mul(r->X, p->Z, q->Z); fe_add(t0, r->X, r->X); fe_sub(r->X, r->Z, r->Y); fe_add(r->Y, r->Z, r->Y); fe_sub(r->Z, t0, r->T); fe_add(r->T, t0, r->T); } void ge_tobytes(unsigned char *s, const ge_p2 *h) { fe recip; fe x; fe y; fe_invert(recip, h->Z); fe_mul(x, h->X, recip); fe_mul(y, h->Y, recip); fe_tobytes(s, y); s[31] ^= fe_isnegative(x) << 7; } ================================================ FILE: Vendor/ed25519-sparkle/src/ge.h ================================================ #ifndef GE_H #define GE_H #include "fe.h" /* ge means group element. Here the group is the set of pairs (x,y) of field elements (see fe.h) satisfying -x^2 + y^2 = 1 + d x^2y^2 where d = -121665/121666. Representations: ge_p2 (projective): (X:Y:Z) satisfying x=X/Z, y=Y/Z ge_p3 (extended): (X:Y:Z:T) satisfying x=X/Z, y=Y/Z, XY=ZT ge_p1p1 (completed): ((X:Z),(Y:T)) satisfying x=X/Z, y=Y/T ge_precomp (Duif): (y+x,y-x,2dxy) */ typedef struct { fe X; fe Y; fe Z; } ge_p2; typedef struct { fe X; fe Y; fe Z; fe T; } ge_p3; typedef struct { fe X; fe Y; fe Z; fe T; } ge_p1p1; typedef struct { fe yplusx; fe yminusx; fe xy2d; } ge_precomp; typedef struct { fe YplusX; fe YminusX; fe Z; fe T2d; } ge_cached; void ge_p3_tobytes(unsigned char *s, const ge_p3 *h); void ge_tobytes(unsigned char *s, const ge_p2 *h); int ge_frombytes_negate_vartime(ge_p3 *h, const unsigned char *s); void ge_add(ge_p1p1 *r, const ge_p3 *p, const ge_cached *q); void ge_sub(ge_p1p1 *r, const ge_p3 *p, const ge_cached *q); void ge_double_scalarmult_vartime(ge_p2 *r, const unsigned char *a, const ge_p3 *A, const unsigned char *b); void ge_madd(ge_p1p1 *r, const ge_p3 *p, const ge_precomp *q); void ge_msub(ge_p1p1 *r, const ge_p3 *p, const ge_precomp *q); void ge_scalarmult_base(ge_p3 *h, const unsigned char *a); void ge_p1p1_to_p2(ge_p2 *r, const ge_p1p1 *p); void ge_p1p1_to_p3(ge_p3 *r, const ge_p1p1 *p); void ge_p2_0(ge_p2 *h); void ge_p2_dbl(ge_p1p1 *r, const ge_p2 *p); void ge_p3_0(ge_p3 *h); void ge_p3_dbl(ge_p1p1 *r, const ge_p3 *p); void ge_p3_to_cached(ge_cached *r, const ge_p3 *p); void ge_p3_to_p2(ge_p2 *r, const ge_p3 *p); #endif ================================================ FILE: Vendor/ed25519-sparkle/src/key_exchange.c ================================================ #include "ed25519.h" #include "fe.h" void ed25519_key_exchange(unsigned char *shared_secret, const unsigned char *public_key, const unsigned char *private_key) { unsigned char e[32]; unsigned int i; fe x1; fe x2; fe z2; fe x3; fe z3; fe tmp0; fe tmp1; int pos; unsigned int swap; unsigned int b; /* copy the private key and make sure it's valid */ for (i = 0; i < 32; ++i) { e[i] = private_key[i]; } e[0] &= 248; e[31] &= 63; e[31] |= 64; /* unpack the public key and convert edwards to montgomery */ /* due to CodesInChaos: montgomeryX = (edwardsY + 1)*inverse(1 - edwardsY) mod p */ fe_frombytes(x1, public_key); fe_1(tmp1); fe_add(tmp0, x1, tmp1); fe_sub(tmp1, tmp1, x1); fe_invert(tmp1, tmp1); fe_mul(x1, tmp0, tmp1); fe_1(x2); fe_0(z2); fe_copy(x3, x1); fe_1(z3); swap = 0; for (pos = 254; pos >= 0; --pos) { b = e[pos / 8] >> (pos & 7); b &= 1; swap ^= b; fe_cswap(x2, x3, swap); fe_cswap(z2, z3, swap); swap = b; /* from montgomery.h */ fe_sub(tmp0, x3, z3); fe_sub(tmp1, x2, z2); fe_add(x2, x2, z2); fe_add(z2, x3, z3); fe_mul(z3, tmp0, x2); fe_mul(z2, z2, tmp1); fe_sq(tmp0, tmp1); fe_sq(tmp1, x2); fe_add(x3, z3, z2); fe_sub(z2, z3, z2); fe_mul(x2, tmp1, tmp0); fe_sub(tmp1, tmp1, tmp0); fe_sq(z2, z2); fe_mul121666(z3, tmp1); fe_sq(x3, x3); fe_add(tmp0, tmp0, z3); fe_mul(z3, x1, z2); fe_mul(z2, tmp1, tmp0); } fe_cswap(x2, x3, swap); fe_cswap(z2, z3, swap); fe_invert(z2, z2); fe_mul(x2, x2, z2); fe_tobytes(shared_secret, x2); } ================================================ FILE: Vendor/ed25519-sparkle/src/keypair.c ================================================ #include "ed25519.h" #include "sha512.h" #include "ge.h" void ed25519_create_keypair(unsigned char *public_key, unsigned char *private_key, const unsigned char *seed) { ge_p3 A; sha512(seed, 32, private_key); private_key[0] &= 248; private_key[31] &= 63; private_key[31] |= 64; ge_scalarmult_base(&A, private_key); ge_p3_tobytes(public_key, &A); } ================================================ FILE: Vendor/ed25519-sparkle/src/precomp_data.h ================================================ static const ge_precomp Bi[8] = { { { 25967493, -14356035, 29566456, 3660896, -12694345, 4014787, 27544626, -11754271, -6079156, 2047605 }, { -12545711, 934262, -2722910, 3049990, -727428, 9406986, 12720692, 5043384, 19500929, -15469378 }, { -8738181, 4489570, 9688441, -14785194, 10184609, -12363380, 29287919, 11864899, -24514362, -4438546 }, }, { { 15636291, -9688557, 24204773, -7912398, 616977, -16685262, 27787600, -14772189, 28944400, -1550024 }, { 16568933, 4717097, -11556148, -1102322, 15682896, -11807043, 16354577, -11775962, 7689662, 11199574 }, { 30464156, -5976125, -11779434, -15670865, 23220365, 15915852, 7512774, 10017326, -17749093, -9920357 }, }, { { 10861363, 11473154, 27284546, 1981175, -30064349, 12577861, 32867885, 14515107, -15438304, 10819380 }, { 4708026, 6336745, 20377586, 9066809, -11272109, 6594696, -25653668, 12483688, -12668491, 5581306 }, { 19563160, 16186464, -29386857, 4097519, 10237984, -4348115, 28542350, 13850243, -23678021, -15815942 }, }, { { 5153746, 9909285, 1723747, -2777874, 30523605, 5516873, 19480852, 5230134, -23952439, -15175766 }, { -30269007, -3463509, 7665486, 10083793, 28475525, 1649722, 20654025, 16520125, 30598449, 7715701 }, { 28881845, 14381568, 9657904, 3680757, -20181635, 7843316, -31400660, 1370708, 29794553, -1409300 }, }, { { -22518993, -6692182, 14201702, -8745502, -23510406, 8844726, 18474211, -1361450, -13062696, 13821877 }, { -6455177, -7839871, 3374702, -4740862, -27098617, -10571707, 31655028, -7212327, 18853322, -14220951 }, { 4566830, -12963868, -28974889, -12240689, -7602672, -2830569, -8514358, -10431137, 2207753, -3209784 }, }, { { -25154831, -4185821, 29681144, 7868801, -6854661, -9423865, -12437364, -663000, -31111463, -16132436 }, { 25576264, -2703214, 7349804, -11814844, 16472782, 9300885, 3844789, 15725684, 171356, 6466918 }, { 23103977, 13316479, 9739013, -16149481, 817875, -15038942, 8965339, -14088058, -30714912, 16193877 }, }, { { -33521811, 3180713, -2394130, 14003687, -16903474, -16270840, 17238398, 4729455, -18074513, 9256800 }, { -25182317, -4174131, 32336398, 5036987, -21236817, 11360617, 22616405, 9761698, -19827198, 630305 }, { -13720693, 2639453, -24237460, -7406481, 9494427, -5774029, -6554551, -15960994, -2449256, -14291300 }, }, { { -3151181, -5046075, 9282714, 6866145, -31907062, -863023, -18940575, 15033784, 25105118, -7894876 }, { -24326370, 15950226, -31801215, -14592823, -11662737, -5090925, 1573892, -2625887, 2198790, -15804619 }, { -3099351, 10324967, -2241613, 7453183, -5446979, -2735503, -13812022, -16236442, -32461234, -12290683 }, }, }; /* base[i][j] = (j+1)*256^i*B */ static const ge_precomp base[32][8] = { { { { 25967493, -14356035, 29566456, 3660896, -12694345, 4014787, 27544626, -11754271, -6079156, 2047605 }, { -12545711, 934262, -2722910, 3049990, -727428, 9406986, 12720692, 5043384, 19500929, -15469378 }, { -8738181, 4489570, 9688441, -14785194, 10184609, -12363380, 29287919, 11864899, -24514362, -4438546 }, }, { { -12815894, -12976347, -21581243, 11784320, -25355658, -2750717, -11717903, -3814571, -358445, -10211303 }, { -21703237, 6903825, 27185491, 6451973, -29577724, -9554005, -15616551, 11189268, -26829678, -5319081 }, { 26966642, 11152617, 32442495, 15396054, 14353839, -12752335, -3128826, -9541118, -15472047, -4166697 }, }, { { 15636291, -9688557, 24204773, -7912398, 616977, -16685262, 27787600, -14772189, 28944400, -1550024 }, { 16568933, 4717097, -11556148, -1102322, 15682896, -11807043, 16354577, -11775962, 7689662, 11199574 }, { 30464156, -5976125, -11779434, -15670865, 23220365, 15915852, 7512774, 10017326, -17749093, -9920357 }, }, { { -17036878, 13921892, 10945806, -6033431, 27105052, -16084379, -28926210, 15006023, 3284568, -6276540 }, { 23599295, -8306047, -11193664, -7687416, 13236774, 10506355, 7464579, 9656445, 13059162, 10374397 }, { 7798556, 16710257, 3033922, 2874086, 28997861, 2835604, 32406664, -3839045, -641708, -101325 }, }, { { 10861363, 11473154, 27284546, 1981175, -30064349, 12577861, 32867885, 14515107, -15438304, 10819380 }, { 4708026, 6336745, 20377586, 9066809, -11272109, 6594696, -25653668, 12483688, -12668491, 5581306 }, { 19563160, 16186464, -29386857, 4097519, 10237984, -4348115, 28542350, 13850243, -23678021, -15815942 }, }, { { -15371964, -12862754, 32573250, 4720197, -26436522, 5875511, -19188627, -15224819, -9818940, -12085777 }, { -8549212, 109983, 15149363, 2178705, 22900618, 4543417, 3044240, -15689887, 1762328, 14866737 }, { -18199695, -15951423, -10473290, 1707278, -17185920, 3916101, -28236412, 3959421, 27914454, 4383652 }, }, { { 5153746, 9909285, 1723747, -2777874, 30523605, 5516873, 19480852, 5230134, -23952439, -15175766 }, { -30269007, -3463509, 7665486, 10083793, 28475525, 1649722, 20654025, 16520125, 30598449, 7715701 }, { 28881845, 14381568, 9657904, 3680757, -20181635, 7843316, -31400660, 1370708, 29794553, -1409300 }, }, { { 14499471, -2729599, -33191113, -4254652, 28494862, 14271267, 30290735, 10876454, -33154098, 2381726 }, { -7195431, -2655363, -14730155, 462251, -27724326, 3941372, -6236617, 3696005, -32300832, 15351955 }, { 27431194, 8222322, 16448760, -3907995, -18707002, 11938355, -32961401, -2970515, 29551813, 10109425 }, }, }, { { { -13657040, -13155431, -31283750, 11777098, 21447386, 6519384, -2378284, -1627556, 10092783, -4764171 }, { 27939166, 14210322, 4677035, 16277044, -22964462, -12398139, -32508754, 12005538, -17810127, 12803510 }, { 17228999, -15661624, -1233527, 300140, -1224870, -11714777, 30364213, -9038194, 18016357, 4397660 }, }, { { -10958843, -7690207, 4776341, -14954238, 27850028, -15602212, -26619106, 14544525, -17477504, 982639 }, { 29253598, 15796703, -2863982, -9908884, 10057023, 3163536, 7332899, -4120128, -21047696, 9934963 }, { 5793303, 16271923, -24131614, -10116404, 29188560, 1206517, -14747930, 4559895, -30123922, -10897950 }, }, { { -27643952, -11493006, 16282657, -11036493, 28414021, -15012264, 24191034, 4541697, -13338309, 5500568 }, { 12650548, -1497113, 9052871, 11355358, -17680037, -8400164, -17430592, 12264343, 10874051, 13524335 }, { 25556948, -3045990, 714651, 2510400, 23394682, -10415330, 33119038, 5080568, -22528059, 5376628 }, }, { { -26088264, -4011052, -17013699, -3537628, -6726793, 1920897, -22321305, -9447443, 4535768, 1569007 }, { -2255422, 14606630, -21692440, -8039818, 28430649, 8775819, -30494562, 3044290, 31848280, 12543772 }, { -22028579, 2943893, -31857513, 6777306, 13784462, -4292203, -27377195, -2062731, 7718482, 14474653 }, }, { { 2385315, 2454213, -22631320, 46603, -4437935, -15680415, 656965, -7236665, 24316168, -5253567 }, { 13741529, 10911568, -33233417, -8603737, -20177830, -1033297, 33040651, -13424532, -20729456, 8321686 }, { 21060490, -2212744, 15712757, -4336099, 1639040, 10656336, 23845965, -11874838, -9984458, 608372 }, }, { { -13672732, -15087586, -10889693, -7557059, -6036909, 11305547, 1123968, -6780577, 27229399, 23887 }, { -23244140, -294205, -11744728, 14712571, -29465699, -2029617, 12797024, -6440308, -1633405, 16678954 }, { -29500620, 4770662, -16054387, 14001338, 7830047, 9564805, -1508144, -4795045, -17169265, 4904953 }, }, { { 24059557, 14617003, 19037157, -15039908, 19766093, -14906429, 5169211, 16191880, 2128236, -4326833 }, { -16981152, 4124966, -8540610, -10653797, 30336522, -14105247, -29806336, 916033, -6882542, -2986532 }, { -22630907, 12419372, -7134229, -7473371, -16478904, 16739175, 285431, 2763829, 15736322, 4143876 }, }, { { 2379352, 11839345, -4110402, -5988665, 11274298, 794957, 212801, -14594663, 23527084, -16458268 }, { 33431127, -11130478, -17838966, -15626900, 8909499, 8376530, -32625340, 4087881, -15188911, -14416214 }, { 1767683, 7197987, -13205226, -2022635, -13091350, 448826, 5799055, 4357868, -4774191, -16323038 }, }, }, { { { 6721966, 13833823, -23523388, -1551314, 26354293, -11863321, 23365147, -3949732, 7390890, 2759800 }, { 4409041, 2052381, 23373853, 10530217, 7676779, -12885954, 21302353, -4264057, 1244380, -12919645 }, { -4421239, 7169619, 4982368, -2957590, 30256825, -2777540, 14086413, 9208236, 15886429, 16489664 }, }, { { 1996075, 10375649, 14346367, 13311202, -6874135, -16438411, -13693198, 398369, -30606455, -712933 }, { -25307465, 9795880, -2777414, 14878809, -33531835, 14780363, 13348553, 12076947, -30836462, 5113182 }, { -17770784, 11797796, 31950843, 13929123, -25888302, 12288344, -30341101, -7336386, 13847711, 5387222 }, }, { { -18582163, -3416217, 17824843, -2340966, 22744343, -10442611, 8763061, 3617786, -19600662, 10370991 }, { 20246567, -14369378, 22358229, -543712, 18507283, -10413996, 14554437, -8746092, 32232924, 16763880 }, { 9648505, 10094563, 26416693, 14745928, -30374318, -6472621, 11094161, 15689506, 3140038, -16510092 }, }, { { -16160072, 5472695, 31895588, 4744994, 8823515, 10365685, -27224800, 9448613, -28774454, 366295 }, { 19153450, 11523972, -11096490, -6503142, -24647631, 5420647, 28344573, 8041113, 719605, 11671788 }, { 8678025, 2694440, -6808014, 2517372, 4964326, 11152271, -15432916, -15266516, 27000813, -10195553 }, }, { { -15157904, 7134312, 8639287, -2814877, -7235688, 10421742, 564065, 5336097, 6750977, -14521026 }, { 11836410, -3979488, 26297894, 16080799, 23455045, 15735944, 1695823, -8819122, 8169720, 16220347 }, { -18115838, 8653647, 17578566, -6092619, -8025777, -16012763, -11144307, -2627664, -5990708, -14166033 }, }, { { -23308498, -10968312, 15213228, -10081214, -30853605, -11050004, 27884329, 2847284, 2655861, 1738395 }, { -27537433, -14253021, -25336301, -8002780, -9370762, 8129821, 21651608, -3239336, -19087449, -11005278 }, { 1533110, 3437855, 23735889, 459276, 29970501, 11335377, 26030092, 5821408, 10478196, 8544890 }, }, { { 32173121, -16129311, 24896207, 3921497, 22579056, -3410854, 19270449, 12217473, 17789017, -3395995 }, { -30552961, -2228401, -15578829, -10147201, 13243889, 517024, 15479401, -3853233, 30460520, 1052596 }, { -11614875, 13323618, 32618793, 8175907, -15230173, 12596687, 27491595, -4612359, 3179268, -9478891 }, }, { { 31947069, -14366651, -4640583, -15339921, -15125977, -6039709, -14756777, -16411740, 19072640, -9511060 }, { 11685058, 11822410, 3158003, -13952594, 33402194, -4165066, 5977896, -5215017, 473099, 5040608 }, { -20290863, 8198642, -27410132, 11602123, 1290375, -2799760, 28326862, 1721092, -19558642, -3131606 }, }, }, { { { 7881532, 10687937, 7578723, 7738378, -18951012, -2553952, 21820786, 8076149, -27868496, 11538389 }, { -19935666, 3899861, 18283497, -6801568, -15728660, -11249211, 8754525, 7446702, -5676054, 5797016 }, { -11295600, -3793569, -15782110, -7964573, 12708869, -8456199, 2014099, -9050574, -2369172, -5877341 }, }, { { -22472376, -11568741, -27682020, 1146375, 18956691, 16640559, 1192730, -3714199, 15123619, 10811505 }, { 14352098, -3419715, -18942044, 10822655, 32750596, 4699007, -70363, 15776356, -28886779, -11974553 }, { -28241164, -8072475, -4978962, -5315317, 29416931, 1847569, -20654173, -16484855, 4714547, -9600655 }, }, { { 15200332, 8368572, 19679101, 15970074, -31872674, 1959451, 24611599, -4543832, -11745876, 12340220 }, { 12876937, -10480056, 33134381, 6590940, -6307776, 14872440, 9613953, 8241152, 15370987, 9608631 }, { -4143277, -12014408, 8446281, -391603, 4407738, 13629032, -7724868, 15866074, -28210621, -8814099 }, }, { { 26660628, -15677655, 8393734, 358047, -7401291, 992988, -23904233, 858697, 20571223, 8420556 }, { 14620715, 13067227, -15447274, 8264467, 14106269, 15080814, 33531827, 12516406, -21574435, -12476749 }, { 236881, 10476226, 57258, -14677024, 6472998, 2466984, 17258519, 7256740, 8791136, 15069930 }, }, { { 1276410, -9371918, 22949635, -16322807, -23493039, -5702186, 14711875, 4874229, -30663140, -2331391 }, { 5855666, 4990204, -13711848, 7294284, -7804282, 1924647, -1423175, -7912378, -33069337, 9234253 }, { 20590503, -9018988, 31529744, -7352666, -2706834, 10650548, 31559055, -11609587, 18979186, 13396066 }, }, { { 24474287, 4968103, 22267082, 4407354, 24063882, -8325180, -18816887, 13594782, 33514650, 7021958 }, { -11566906, -6565505, -21365085, 15928892, -26158305, 4315421, -25948728, -3916677, -21480480, 12868082 }, { -28635013, 13504661, 19988037, -2132761, 21078225, 6443208, -21446107, 2244500, -12455797, -8089383 }, }, { { -30595528, 13793479, -5852820, 319136, -25723172, -6263899, 33086546, 8957937, -15233648, 5540521 }, { -11630176, -11503902, -8119500, -7643073, 2620056, 1022908, -23710744, -1568984, -16128528, -14962807 }, { 23152971, 775386, 27395463, 14006635, -9701118, 4649512, 1689819, 892185, -11513277, -15205948 }, }, { { 9770129, 9586738, 26496094, 4324120, 1556511, -3550024, 27453819, 4763127, -19179614, 5867134 }, { -32765025, 1927590, 31726409, -4753295, 23962434, -16019500, 27846559, 5931263, -29749703, -16108455 }, { 27461885, -2977536, 22380810, 1815854, -23033753, -3031938, 7283490, -15148073, -19526700, 7734629 }, }, }, { { { -8010264, -9590817, -11120403, 6196038, 29344158, -13430885, 7585295, -3176626, 18549497, 15302069 }, { -32658337, -6171222, -7672793, -11051681, 6258878, 13504381, 10458790, -6418461, -8872242, 8424746 }, { 24687205, 8613276, -30667046, -3233545, 1863892, -1830544, 19206234, 7134917, -11284482, -828919 }, }, { { 11334899, -9218022, 8025293, 12707519, 17523892, -10476071, 10243738, -14685461, -5066034, 16498837 }, { 8911542, 6887158, -9584260, -6958590, 11145641, -9543680, 17303925, -14124238, 6536641, 10543906 }, { -28946384, 15479763, -17466835, 568876, -1497683, 11223454, -2669190, -16625574, -27235709, 8876771 }, }, { { -25742899, -12566864, -15649966, -846607, -33026686, -796288, -33481822, 15824474, -604426, -9039817 }, { 10330056, 70051, 7957388, -9002667, 9764902, 15609756, 27698697, -4890037, 1657394, 3084098 }, { 10477963, -7470260, 12119566, -13250805, 29016247, -5365589, 31280319, 14396151, -30233575, 15272409 }, }, { { -12288309, 3169463, 28813183, 16658753, 25116432, -5630466, -25173957, -12636138, -25014757, 1950504 }, { -26180358, 9489187, 11053416, -14746161, -31053720, 5825630, -8384306, -8767532, 15341279, 8373727 }, { 28685821, 7759505, -14378516, -12002860, -31971820, 4079242, 298136, -10232602, -2878207, 15190420 }, }, { { -32932876, 13806336, -14337485, -15794431, -24004620, 10940928, 8669718, 2742393, -26033313, -6875003 }, { -1580388, -11729417, -25979658, -11445023, -17411874, -10912854, 9291594, -16247779, -12154742, 6048605 }, { -30305315, 14843444, 1539301, 11864366, 20201677, 1900163, 13934231, 5128323, 11213262, 9168384 }, }, { { -26280513, 11007847, 19408960, -940758, -18592965, -4328580, -5088060, -11105150, 20470157, -16398701 }, { -23136053, 9282192, 14855179, -15390078, -7362815, -14408560, -22783952, 14461608, 14042978, 5230683 }, { 29969567, -2741594, -16711867, -8552442, 9175486, -2468974, 21556951, 3506042, -5933891, -12449708 }, }, { { -3144746, 8744661, 19704003, 4581278, -20430686, 6830683, -21284170, 8971513, -28539189, 15326563 }, { -19464629, 10110288, -17262528, -3503892, -23500387, 1355669, -15523050, 15300988, -20514118, 9168260 }, { -5353335, 4488613, -23803248, 16314347, 7780487, -15638939, -28948358, 9601605, 33087103, -9011387 }, }, { { -19443170, -15512900, -20797467, -12445323, -29824447, 10229461, -27444329, -15000531, -5996870, 15664672 }, { 23294591, -16632613, -22650781, -8470978, 27844204, 11461195, 13099750, -2460356, 18151676, 13417686 }, { -24722913, -4176517, -31150679, 5988919, -26858785, 6685065, 1661597, -12551441, 15271676, -15452665 }, }, }, { { { 11433042, -13228665, 8239631, -5279517, -1985436, -725718, -18698764, 2167544, -6921301, -13440182 }, { -31436171, 15575146, 30436815, 12192228, -22463353, 9395379, -9917708, -8638997, 12215110, 12028277 }, { 14098400, 6555944, 23007258, 5757252, -15427832, -12950502, 30123440, 4617780, -16900089, -655628 }, }, { { -4026201, -15240835, 11893168, 13718664, -14809462, 1847385, -15819999, 10154009, 23973261, -12684474 }, { -26531820, -3695990, -1908898, 2534301, -31870557, -16550355, 18341390, -11419951, 32013174, -10103539 }, { -25479301, 10876443, -11771086, -14625140, -12369567, 1838104, 21911214, 6354752, 4425632, -837822 }, }, { { -10433389, -14612966, 22229858, -3091047, -13191166, 776729, -17415375, -12020462, 4725005, 14044970 }, { 19268650, -7304421, 1555349, 8692754, -21474059, -9910664, 6347390, -1411784, -19522291, -16109756 }, { -24864089, 12986008, -10898878, -5558584, -11312371, -148526, 19541418, 8180106, 9282262, 10282508 }, }, { { -26205082, 4428547, -8661196, -13194263, 4098402, -14165257, 15522535, 8372215, 5542595, -10702683 }, { -10562541, 14895633, 26814552, -16673850, -17480754, -2489360, -2781891, 6993761, -18093885, 10114655 }, { -20107055, -929418, 31422704, 10427861, -7110749, 6150669, -29091755, -11529146, 25953725, -106158 }, }, { { -4234397, -8039292, -9119125, 3046000, 2101609, -12607294, 19390020, 6094296, -3315279, 12831125 }, { -15998678, 7578152, 5310217, 14408357, -33548620, -224739, 31575954, 6326196, 7381791, -2421839 }, { -20902779, 3296811, 24736065, -16328389, 18374254, 7318640, 6295303, 8082724, -15362489, 12339664 }, }, { { 27724736, 2291157, 6088201, -14184798, 1792727, 5857634, 13848414, 15768922, 25091167, 14856294 }, { -18866652, 8331043, 24373479, 8541013, -701998, -9269457, 12927300, -12695493, -22182473, -9012899 }, { -11423429, -5421590, 11632845, 3405020, 30536730, -11674039, -27260765, 13866390, 30146206, 9142070 }, }, { { 3924129, -15307516, -13817122, -10054960, 12291820, -668366, -27702774, 9326384, -8237858, 4171294 }, { -15921940, 16037937, 6713787, 16606682, -21612135, 2790944, 26396185, 3731949, 345228, -5462949 }, { -21327538, 13448259, 25284571, 1143661, 20614966, -8849387, 2031539, -12391231, -16253183, -13582083 }, }, { { 31016211, -16722429, 26371392, -14451233, -5027349, 14854137, 17477601, 3842657, 28012650, -16405420 }, { -5075835, 9368966, -8562079, -4600902, -15249953, 6970560, -9189873, 16292057, -8867157, 3507940 }, { 29439664, 3537914, 23333589, 6997794, -17555561, -11018068, -15209202, -15051267, -9164929, 6580396 }, }, }, { { { -12185861, -7679788, 16438269, 10826160, -8696817, -6235611, 17860444, -9273846, -2095802, 9304567 }, { 20714564, -4336911, 29088195, 7406487, 11426967, -5095705, 14792667, -14608617, 5289421, -477127 }, { -16665533, -10650790, -6160345, -13305760, 9192020, -1802462, 17271490, 12349094, 26939669, -3752294 }, }, { { -12889898, 9373458, 31595848, 16374215, 21471720, 13221525, -27283495, -12348559, -3698806, 117887 }, { 22263325, -6560050, 3984570, -11174646, -15114008, -566785, 28311253, 5358056, -23319780, 541964 }, { 16259219, 3261970, 2309254, -15534474, -16885711, -4581916, 24134070, -16705829, -13337066, -13552195 }, }, { { 9378160, -13140186, -22845982, -12745264, 28198281, -7244098, -2399684, -717351, 690426, 14876244 }, { 24977353, -314384, -8223969, -13465086, 28432343, -1176353, -13068804, -12297348, -22380984, 6618999 }, { -1538174, 11685646, 12944378, 13682314, -24389511, -14413193, 8044829, -13817328, 32239829, -5652762 }, }, { { -18603066, 4762990, -926250, 8885304, -28412480, -3187315, 9781647, -10350059, 32779359, 5095274 }, { -33008130, -5214506, -32264887, -3685216, 9460461, -9327423, -24601656, 14506724, 21639561, -2630236 }, { -16400943, -13112215, 25239338, 15531969, 3987758, -4499318, -1289502, -6863535, 17874574, 558605 }, }, { { -13600129, 10240081, 9171883, 16131053, -20869254, 9599700, 33499487, 5080151, 2085892, 5119761 }, { -22205145, -2519528, -16381601, 414691, -25019550, 2170430, 30634760, -8363614, -31999993, -5759884 }, { -6845704, 15791202, 8550074, -1312654, 29928809, -12092256, 27534430, -7192145, -22351378, 12961482 }, }, { { -24492060, -9570771, 10368194, 11582341, -23397293, -2245287, 16533930, 8206996, -30194652, -5159638 }, { -11121496, -3382234, 2307366, 6362031, -135455, 8868177, -16835630, 7031275, 7589640, 8945490 }, { -32152748, 8917967, 6661220, -11677616, -1192060, -15793393, 7251489, -11182180, 24099109, -14456170 }, }, { { 5019558, -7907470, 4244127, -14714356, -26933272, 6453165, -19118182, -13289025, -6231896, -10280736 }, { 10853594, 10721687, 26480089, 5861829, -22995819, 1972175, -1866647, -10557898, -3363451, -6441124 }, { -17002408, 5906790, 221599, -6563147, 7828208, -13248918, 24362661, -2008168, -13866408, 7421392 }, }, { { 8139927, -6546497, 32257646, -5890546, 30375719, 1886181, -21175108, 15441252, 28826358, -4123029 }, { 6267086, 9695052, 7709135, -16603597, -32869068, -1886135, 14795160, -7840124, 13746021, -1742048 }, { 28584902, 7787108, -6732942, -15050729, 22846041, -7571236, -3181936, -363524, 4771362, -8419958 }, }, }, { { { 24949256, 6376279, -27466481, -8174608, -18646154, -9930606, 33543569, -12141695, 3569627, 11342593 }, { 26514989, 4740088, 27912651, 3697550, 19331575, -11472339, 6809886, 4608608, 7325975, -14801071 }, { -11618399, -14554430, -24321212, 7655128, -1369274, 5214312, -27400540, 10258390, -17646694, -8186692 }, }, { { 11431204, 15823007, 26570245, 14329124, 18029990, 4796082, -31446179, 15580664, 9280358, -3973687 }, { -160783, -10326257, -22855316, -4304997, -20861367, -13621002, -32810901, -11181622, -15545091, 4387441 }, { -20799378, 12194512, 3937617, -5805892, -27154820, 9340370, -24513992, 8548137, 20617071, -7482001 }, }, { { -938825, -3930586, -8714311, 16124718, 24603125, -6225393, -13775352, -11875822, 24345683, 10325460 }, { -19855277, -1568885, -22202708, 8714034, 14007766, 6928528, 16318175, -1010689, 4766743, 3552007 }, { -21751364, -16730916, 1351763, -803421, -4009670, 3950935, 3217514, 14481909, 10988822, -3994762 }, }, { { 15564307, -14311570, 3101243, 5684148, 30446780, -8051356, 12677127, -6505343, -8295852, 13296005 }, { -9442290, 6624296, -30298964, -11913677, -4670981, -2057379, 31521204, 9614054, -30000824, 12074674 }, { 4771191, -135239, 14290749, -13089852, 27992298, 14998318, -1413936, -1556716, 29832613, -16391035 }, }, { { 7064884, -7541174, -19161962, -5067537, -18891269, -2912736, 25825242, 5293297, -27122660, 13101590 }, { -2298563, 2439670, -7466610, 1719965, -27267541, -16328445, 32512469, -5317593, -30356070, -4190957 }, { -30006540, 10162316, -33180176, 3981723, -16482138, -13070044, 14413974, 9515896, 19568978, 9628812 }, }, { { 33053803, 199357, 15894591, 1583059, 27380243, -4580435, -17838894, -6106839, -6291786, 3437740 }, { -18978877, 3884493, 19469877, 12726490, 15913552, 13614290, -22961733, 70104, 7463304, 4176122 }, { -27124001, 10659917, 11482427, -16070381, 12771467, -6635117, -32719404, -5322751, 24216882, 5944158 }, }, { { 8894125, 7450974, -2664149, -9765752, -28080517, -12389115, 19345746, 14680796, 11632993, 5847885 }, { 26942781, -2315317, 9129564, -4906607, 26024105, 11769399, -11518837, 6367194, -9727230, 4782140 }, { 19916461, -4828410, -22910704, -11414391, 25606324, -5972441, 33253853, 8220911, 6358847, -1873857 }, }, { { 801428, -2081702, 16569428, 11065167, 29875704, 96627, 7908388, -4480480, -13538503, 1387155 }, { 19646058, 5720633, -11416706, 12814209, 11607948, 12749789, 14147075, 15156355, -21866831, 11835260 }, { 19299512, 1155910, 28703737, 14890794, 2925026, 7269399, 26121523, 15467869, -26560550, 5052483 }, }, }, { { { -3017432, 10058206, 1980837, 3964243, 22160966, 12322533, -6431123, -12618185, 12228557, -7003677 }, { 32944382, 14922211, -22844894, 5188528, 21913450, -8719943, 4001465, 13238564, -6114803, 8653815 }, { 22865569, -4652735, 27603668, -12545395, 14348958, 8234005, 24808405, 5719875, 28483275, 2841751 }, }, { { -16420968, -1113305, -327719, -12107856, 21886282, -15552774, -1887966, -315658, 19932058, -12739203 }, { -11656086, 10087521, -8864888, -5536143, -19278573, -3055912, 3999228, 13239134, -4777469, -13910208 }, { 1382174, -11694719, 17266790, 9194690, -13324356, 9720081, 20403944, 11284705, -14013818, 3093230 }, }, { { 16650921, -11037932, -1064178, 1570629, -8329746, 7352753, -302424, 16271225, -24049421, -6691850 }, { -21911077, -5927941, -4611316, -5560156, -31744103, -10785293, 24123614, 15193618, -21652117, -16739389 }, { -9935934, -4289447, -25279823, 4372842, 2087473, 10399484, 31870908, 14690798, 17361620, 11864968 }, }, { { -11307610, 6210372, 13206574, 5806320, -29017692, -13967200, -12331205, -7486601, -25578460, -16240689 }, { 14668462, -12270235, 26039039, 15305210, 25515617, 4542480, 10453892, 6577524, 9145645, -6443880 }, { 5974874, 3053895, -9433049, -10385191, -31865124, 3225009, -7972642, 3936128, -5652273, -3050304 }, }, { { 30625386, -4729400, -25555961, -12792866, -20484575, 7695099, 17097188, -16303496, -27999779, 1803632 }, { -3553091, 9865099, -5228566, 4272701, -5673832, -16689700, 14911344, 12196514, -21405489, 7047412 }, { 20093277, 9920966, -11138194, -5343857, 13161587, 12044805, -32856851, 4124601, -32343828, -10257566 }, }, { { -20788824, 14084654, -13531713, 7842147, 19119038, -13822605, 4752377, -8714640, -21679658, 2288038 }, { -26819236, -3283715, 29965059, 3039786, -14473765, 2540457, 29457502, 14625692, -24819617, 12570232 }, { -1063558, -11551823, 16920318, 12494842, 1278292, -5869109, -21159943, -3498680, -11974704, 4724943 }, }, { { 17960970, -11775534, -4140968, -9702530, -8876562, -1410617, -12907383, -8659932, -29576300, 1903856 }, { 23134274, -14279132, -10681997, -1611936, 20684485, 15770816, -12989750, 3190296, 26955097, 14109738 }, { 15308788, 5320727, -30113809, -14318877, 22902008, 7767164, 29425325, -11277562, 31960942, 11934971 }, }, { { -27395711, 8435796, 4109644, 12222639, -24627868, 14818669, 20638173, 4875028, 10491392, 1379718 }, { -13159415, 9197841, 3875503, -8936108, -1383712, -5879801, 33518459, 16176658, 21432314, 12180697 }, { -11787308, 11500838, 13787581, -13832590, -22430679, 10140205, 1465425, 12689540, -10301319, -13872883 }, }, }, { { { 5414091, -15386041, -21007664, 9643570, 12834970, 1186149, -2622916, -1342231, 26128231, 6032912 }, { -26337395, -13766162, 32496025, -13653919, 17847801, -12669156, 3604025, 8316894, -25875034, -10437358 }, { 3296484, 6223048, 24680646, -12246460, -23052020, 5903205, -8862297, -4639164, 12376617, 3188849 }, }, { { 29190488, -14659046, 27549113, -1183516, 3520066, -10697301, 32049515, -7309113, -16109234, -9852307 }, { -14744486, -9309156, 735818, -598978, -20407687, -5057904, 25246078, -15795669, 18640741, -960977 }, { -6928835, -16430795, 10361374, 5642961, 4910474, 12345252, -31638386, -494430, 10530747, 1053335 }, }, { { -29265967, -14186805, -13538216, -12117373, -19457059, -10655384, -31462369, -2948985, 24018831, 15026644 }, { -22592535, -3145277, -2289276, 5953843, -13440189, 9425631, 25310643, 13003497, -2314791, -15145616 }, { -27419985, -603321, -8043984, -1669117, -26092265, 13987819, -27297622, 187899, -23166419, -2531735 }, }, { { -21744398, -13810475, 1844840, 5021428, -10434399, -15911473, 9716667, 16266922, -5070217, 726099 }, { 29370922, -6053998, 7334071, -15342259, 9385287, 2247707, -13661962, -4839461, 30007388, -15823341 }, { -936379, 16086691, 23751945, -543318, -1167538, -5189036, 9137109, 730663, 9835848, 4555336 }, }, { { -23376435, 1410446, -22253753, -12899614, 30867635, 15826977, 17693930, 544696, -11985298, 12422646 }, { 31117226, -12215734, -13502838, 6561947, -9876867, -12757670, -5118685, -4096706, 29120153, 13924425 }, { -17400879, -14233209, 19675799, -2734756, -11006962, -5858820, -9383939, -11317700, 7240931, -237388 }, }, { { -31361739, -11346780, -15007447, -5856218, -22453340, -12152771, 1222336, 4389483, 3293637, -15551743 }, { -16684801, -14444245, 11038544, 11054958, -13801175, -3338533, -24319580, 7733547, 12796905, -6335822 }, { -8759414, -10817836, -25418864, 10783769, -30615557, -9746811, -28253339, 3647836, 3222231, -11160462 }, }, { { 18606113, 1693100, -25448386, -15170272, 4112353, 10045021, 23603893, -2048234, -7550776, 2484985 }, { 9255317, -3131197, -12156162, -1004256, 13098013, -9214866, 16377220, -2102812, -19802075, -3034702 }, { -22729289, 7496160, -5742199, 11329249, 19991973, -3347502, -31718148, 9936966, -30097688, -10618797 }, }, { { 21878590, -5001297, 4338336, 13643897, -3036865, 13160960, 19708896, 5415497, -7360503, -4109293 }, { 27736861, 10103576, 12500508, 8502413, -3413016, -9633558, 10436918, -1550276, -23659143, -8132100 }, { 19492550, -12104365, -29681976, -852630, -3208171, 12403437, 30066266, 8367329, 13243957, 8709688 }, }, }, { { { 12015105, 2801261, 28198131, 10151021, 24818120, -4743133, -11194191, -5645734, 5150968, 7274186 }, { 2831366, -12492146, 1478975, 6122054, 23825128, -12733586, 31097299, 6083058, 31021603, -9793610 }, { -2529932, -2229646, 445613, 10720828, -13849527, -11505937, -23507731, 16354465, 15067285, -14147707 }, }, { { 7840942, 14037873, -33364863, 15934016, -728213, -3642706, 21403988, 1057586, -19379462, -12403220 }, { 915865, -16469274, 15608285, -8789130, -24357026, 6060030, -17371319, 8410997, -7220461, 16527025 }, { 32922597, -556987, 20336074, -16184568, 10903705, -5384487, 16957574, 52992, 23834301, 6588044 }, }, { { 32752030, 11232950, 3381995, -8714866, 22652988, -10744103, 17159699, 16689107, -20314580, -1305992 }, { -4689649, 9166776, -25710296, -10847306, 11576752, 12733943, 7924251, -2752281, 1976123, -7249027 }, { 21251222, 16309901, -2983015, -6783122, 30810597, 12967303, 156041, -3371252, 12331345, -8237197 }, }, { { 8651614, -4477032, -16085636, -4996994, 13002507, 2950805, 29054427, -5106970, 10008136, -4667901 }, { 31486080, 15114593, -14261250, 12951354, 14369431, -7387845, 16347321, -13662089, 8684155, -10532952 }, { 19443825, 11385320, 24468943, -9659068, -23919258, 2187569, -26263207, -6086921, 31316348, 14219878 }, }, { { -28594490, 1193785, 32245219, 11392485, 31092169, 15722801, 27146014, 6992409, 29126555, 9207390 }, { 32382935, 1110093, 18477781, 11028262, -27411763, -7548111, -4980517, 10843782, -7957600, -14435730 }, { 2814918, 7836403, 27519878, -7868156, -20894015, -11553689, -21494559, 8550130, 28346258, 1994730 }, }, { { -19578299, 8085545, -14000519, -3948622, 2785838, -16231307, -19516951, 7174894, 22628102, 8115180 }, { -30405132, 955511, -11133838, -15078069, -32447087, -13278079, -25651578, 3317160, -9943017, 930272 }, { -15303681, -6833769, 28856490, 1357446, 23421993, 1057177, 24091212, -1388970, -22765376, -10650715 }, }, { { -22751231, -5303997, -12907607, -12768866, -15811511, -7797053, -14839018, -16554220, -1867018, 8398970 }, { -31969310, 2106403, -4736360, 1362501, 12813763, 16200670, 22981545, -6291273, 18009408, -15772772 }, { -17220923, -9545221, -27784654, 14166835, 29815394, 7444469, 29551787, -3727419, 19288549, 1325865 }, }, { { 15100157, -15835752, -23923978, -1005098, -26450192, 15509408, 12376730, -3479146, 33166107, -8042750 }, { 20909231, 13023121, -9209752, 16251778, -5778415, -8094914, 12412151, 10018715, 2213263, -13878373 }, { 32529814, -11074689, 30361439, -16689753, -9135940, 1513226, 22922121, 6382134, -5766928, 8371348 }, }, }, { { { 9923462, 11271500, 12616794, 3544722, -29998368, -1721626, 12891687, -8193132, -26442943, 10486144 }, { -22597207, -7012665, 8587003, -8257861, 4084309, -12970062, 361726, 2610596, -23921530, -11455195 }, { 5408411, -1136691, -4969122, 10561668, 24145918, 14240566, 31319731, -4235541, 19985175, -3436086 }, }, { { -13994457, 16616821, 14549246, 3341099, 32155958, 13648976, -17577068, 8849297, 65030, 8370684 }, { -8320926, -12049626, 31204563, 5839400, -20627288, -1057277, -19442942, 6922164, 12743482, -9800518 }, { -2361371, 12678785, 28815050, 4759974, -23893047, 4884717, 23783145, 11038569, 18800704, 255233 }, }, { { -5269658, -1773886, 13957886, 7990715, 23132995, 728773, 13393847, 9066957, 19258688, -14753793 }, { -2936654, -10827535, -10432089, 14516793, -3640786, 4372541, -31934921, 2209390, -1524053, 2055794 }, { 580882, 16705327, 5468415, -2683018, -30926419, -14696000, -7203346, -8994389, -30021019, 7394435 }, }, { { 23838809, 1822728, -15738443, 15242727, 8318092, -3733104, -21672180, -3492205, -4821741, 14799921 }, { 13345610, 9759151, 3371034, -16137791, 16353039, 8577942, 31129804, 13496856, -9056018, 7402518 }, { 2286874, -4435931, -20042458, -2008336, -13696227, 5038122, 11006906, -15760352, 8205061, 1607563 }, }, { { 14414086, -8002132, 3331830, -3208217, 22249151, -5594188, 18364661, -2906958, 30019587, -9029278 }, { -27688051, 1585953, -10775053, 931069, -29120221, -11002319, -14410829, 12029093, 9944378, 8024 }, { 4368715, -3709630, 29874200, -15022983, -20230386, -11410704, -16114594, -999085, -8142388, 5640030 }, }, { { 10299610, 13746483, 11661824, 16234854, 7630238, 5998374, 9809887, -16694564, 15219798, -14327783 }, { 27425505, -5719081, 3055006, 10660664, 23458024, 595578, -15398605, -1173195, -18342183, 9742717 }, { 6744077, 2427284, 26042789, 2720740, -847906, 1118974, 32324614, 7406442, 12420155, 1994844 }, }, { { 14012521, -5024720, -18384453, -9578469, -26485342, -3936439, -13033478, -10909803, 24319929, -6446333 }, { 16412690, -4507367, 10772641, 15929391, -17068788, -4658621, 10555945, -10484049, -30102368, -4739048 }, { 22397382, -7767684, -9293161, -12792868, 17166287, -9755136, -27333065, 6199366, 21880021, -12250760 }, }, { { -4283307, 5368523, -31117018, 8163389, -30323063, 3209128, 16557151, 8890729, 8840445, 4957760 }, { -15447727, 709327, -6919446, -10870178, -29777922, 6522332, -21720181, 12130072, -14796503, 5005757 }, { -2114751, -14308128, 23019042, 15765735, -25269683, 6002752, 10183197, -13239326, -16395286, -2176112 }, }, }, { { { -19025756, 1632005, 13466291, -7995100, -23640451, 16573537, -32013908, -3057104, 22208662, 2000468 }, { 3065073, -1412761, -25598674, -361432, -17683065, -5703415, -8164212, 11248527, -3691214, -7414184 }, { 10379208, -6045554, 8877319, 1473647, -29291284, -12507580, 16690915, 2553332, -3132688, 16400289 }, }, { { 15716668, 1254266, -18472690, 7446274, -8448918, 6344164, -22097271, -7285580, 26894937, 9132066 }, { 24158887, 12938817, 11085297, -8177598, -28063478, -4457083, -30576463, 64452, -6817084, -2692882 }, { 13488534, 7794716, 22236231, 5989356, 25426474, -12578208, 2350710, -3418511, -4688006, 2364226 }, }, { { 16335052, 9132434, 25640582, 6678888, 1725628, 8517937, -11807024, -11697457, 15445875, -7798101 }, { 29004207, -7867081, 28661402, -640412, -12794003, -7943086, 31863255, -4135540, -278050, -15759279 }, { -6122061, -14866665, -28614905, 14569919, -10857999, -3591829, 10343412, -6976290, -29828287, -10815811 }, }, { { 27081650, 3463984, 14099042, -4517604, 1616303, -6205604, 29542636, 15372179, 17293797, 960709 }, { 20263915, 11434237, -5765435, 11236810, 13505955, -10857102, -16111345, 6493122, -19384511, 7639714 }, { -2830798, -14839232, 25403038, -8215196, -8317012, -16173699, 18006287, -16043750, 29994677, -15808121 }, }, { { 9769828, 5202651, -24157398, -13631392, -28051003, -11561624, -24613141, -13860782, -31184575, 709464 }, { 12286395, 13076066, -21775189, -1176622, -25003198, 4057652, -32018128, -8890874, 16102007, 13205847 }, { 13733362, 5599946, 10557076, 3195751, -5557991, 8536970, -25540170, 8525972, 10151379, 10394400 }, }, { { 4024660, -16137551, 22436262, 12276534, -9099015, -2686099, 19698229, 11743039, -33302334, 8934414 }, { -15879800, -4525240, -8580747, -2934061, 14634845, -698278, -9449077, 3137094, -11536886, 11721158 }, { 17555939, -5013938, 8268606, 2331751, -22738815, 9761013, 9319229, 8835153, -9205489, -1280045 }, }, { { -461409, -7830014, 20614118, 16688288, -7514766, -4807119, 22300304, 505429, 6108462, -6183415 }, { -5070281, 12367917, -30663534, 3234473, 32617080, -8422642, 29880583, -13483331, -26898490, -7867459 }, { -31975283, 5726539, 26934134, 10237677, -3173717, -605053, 24199304, 3795095, 7592688, -14992079 }, }, { { 21594432, -14964228, 17466408, -4077222, 32537084, 2739898, 6407723, 12018833, -28256052, 4298412 }, { -20650503, -11961496, -27236275, 570498, 3767144, -1717540, 13891942, -1569194, 13717174, 10805743 }, { -14676630, -15644296, 15287174, 11927123, 24177847, -8175568, -796431, 14860609, -26938930, -5863836 }, }, }, { { { 12962541, 5311799, -10060768, 11658280, 18855286, -7954201, 13286263, -12808704, -4381056, 9882022 }, { 18512079, 11319350, -20123124, 15090309, 18818594, 5271736, -22727904, 3666879, -23967430, -3299429 }, { -6789020, -3146043, 16192429, 13241070, 15898607, -14206114, -10084880, -6661110, -2403099, 5276065 }, }, { { 30169808, -5317648, 26306206, -11750859, 27814964, 7069267, 7152851, 3684982, 1449224, 13082861 }, { 10342826, 3098505, 2119311, 193222, 25702612, 12233820, 23697382, 15056736, -21016438, -8202000 }, { -33150110, 3261608, 22745853, 7948688, 19370557, -15177665, -26171976, 6482814, -10300080, -11060101 }, }, { { 32869458, -5408545, 25609743, 15678670, -10687769, -15471071, 26112421, 2521008, -22664288, 6904815 }, { 29506923, 4457497, 3377935, -9796444, -30510046, 12935080, 1561737, 3841096, -29003639, -6657642 }, { 10340844, -6630377, -18656632, -2278430, 12621151, -13339055, 30878497, -11824370, -25584551, 5181966 }, }, { { 25940115, -12658025, 17324188, -10307374, -8671468, 15029094, 24396252, -16450922, -2322852, -12388574 }, { -21765684, 9916823, -1300409, 4079498, -1028346, 11909559, 1782390, 12641087, 20603771, -6561742 }, { -18882287, -11673380, 24849422, 11501709, 13161720, -4768874, 1925523, 11914390, 4662781, 7820689 }, }, { { 12241050, -425982, 8132691, 9393934, 32846760, -1599620, 29749456, 12172924, 16136752, 15264020 }, { -10349955, -14680563, -8211979, 2330220, -17662549, -14545780, 10658213, 6671822, 19012087, 3772772 }, { 3753511, -3421066, 10617074, 2028709, 14841030, -6721664, 28718732, -15762884, 20527771, 12988982 }, }, { { -14822485, -5797269, -3707987, 12689773, -898983, -10914866, -24183046, -10564943, 3299665, -12424953 }, { -16777703, -15253301, -9642417, 4978983, 3308785, 8755439, 6943197, 6461331, -25583147, 8991218 }, { -17226263, 1816362, -1673288, -6086439, 31783888, -8175991, -32948145, 7417950, -30242287, 1507265 }, }, { { 29692663, 6829891, -10498800, 4334896, 20945975, -11906496, -28887608, 8209391, 14606362, -10647073 }, { -3481570, 8707081, 32188102, 5672294, 22096700, 1711240, -33020695, 9761487, 4170404, -2085325 }, { -11587470, 14855945, -4127778, -1531857, -26649089, 15084046, 22186522, 16002000, -14276837, -8400798 }, }, { { -4811456, 13761029, -31703877, -2483919, -3312471, 7869047, -7113572, -9620092, 13240845, 10965870 }, { -7742563, -8256762, -14768334, -13656260, -23232383, 12387166, 4498947, 14147411, 29514390, 4302863 }, { -13413405, -12407859, 20757302, -13801832, 14785143, 8976368, -5061276, -2144373, 17846988, -13971927 }, }, }, { { { -2244452, -754728, -4597030, -1066309, -6247172, 1455299, -21647728, -9214789, -5222701, 12650267 }, { -9906797, -16070310, 21134160, 12198166, -27064575, 708126, 387813, 13770293, -19134326, 10958663 }, { 22470984, 12369526, 23446014, -5441109, -21520802, -9698723, -11772496, -11574455, -25083830, 4271862 }, }, { { -25169565, -10053642, -19909332, 15361595, -5984358, 2159192, 75375, -4278529, -32526221, 8469673 }, { 15854970, 4148314, -8893890, 7259002, 11666551, 13824734, -30531198, 2697372, 24154791, -9460943 }, { 15446137, -15806644, 29759747, 14019369, 30811221, -9610191, -31582008, 12840104, 24913809, 9815020 }, }, { { -4709286, -5614269, -31841498, -12288893, -14443537, 10799414, -9103676, 13438769, 18735128, 9466238 }, { 11933045, 9281483, 5081055, -5183824, -2628162, -4905629, -7727821, -10896103, -22728655, 16199064 }, { 14576810, 379472, -26786533, -8317236, -29426508, -10812974, -102766, 1876699, 30801119, 2164795 }, }, { { 15995086, 3199873, 13672555, 13712240, -19378835, -4647646, -13081610, -15496269, -13492807, 1268052 }, { -10290614, -3659039, -3286592, 10948818, 23037027, 3794475, -3470338, -12600221, -17055369, 3565904 }, { 29210088, -9419337, -5919792, -4952785, 10834811, -13327726, -16512102, -10820713, -27162222, -14030531 }, }, { { -13161890, 15508588, 16663704, -8156150, -28349942, 9019123, -29183421, -3769423, 2244111, -14001979 }, { -5152875, -3800936, -9306475, -6071583, 16243069, 14684434, -25673088, -16180800, 13491506, 4641841 }, { 10813417, 643330, -19188515, -728916, 30292062, -16600078, 27548447, -7721242, 14476989, -12767431 }, }, { { 10292079, 9984945, 6481436, 8279905, -7251514, 7032743, 27282937, -1644259, -27912810, 12651324 }, { -31185513, -813383, 22271204, 11835308, 10201545, 15351028, 17099662, 3988035, 21721536, -3148940 }, { 10202177, -6545839, -31373232, -9574638, -32150642, -8119683, -12906320, 3852694, 13216206, 14842320 }, }, { { -15815640, -10601066, -6538952, -7258995, -6984659, -6581778, -31500847, 13765824, -27434397, 9900184 }, { 14465505, -13833331, -32133984, -14738873, -27443187, 12990492, 33046193, 15796406, -7051866, -8040114 }, { 30924417, -8279620, 6359016, -12816335, 16508377, 9071735, -25488601, 15413635, 9524356, -7018878 }, }, { { 12274201, -13175547, 32627641, -1785326, 6736625, 13267305, 5237659, -5109483, 15663516, 4035784 }, { -2951309, 8903985, 17349946, 601635, -16432815, -4612556, -13732739, -15889334, -22258478, 4659091 }, { -16916263, -4952973, -30393711, -15158821, 20774812, 15897498, 5736189, 15026997, -2178256, -13455585 }, }, }, { { { -8858980, -2219056, 28571666, -10155518, -474467, -10105698, -3801496, 278095, 23440562, -290208 }, { 10226241, -5928702, 15139956, 120818, -14867693, 5218603, 32937275, 11551483, -16571960, -7442864 }, { 17932739, -12437276, -24039557, 10749060, 11316803, 7535897, 22503767, 5561594, -3646624, 3898661 }, }, { { 7749907, -969567, -16339731, -16464, -25018111, 15122143, -1573531, 7152530, 21831162, 1245233 }, { 26958459, -14658026, 4314586, 8346991, -5677764, 11960072, -32589295, -620035, -30402091, -16716212 }, { -12165896, 9166947, 33491384, 13673479, 29787085, 13096535, 6280834, 14587357, -22338025, 13987525 }, }, { { -24349909, 7778775, 21116000, 15572597, -4833266, -5357778, -4300898, -5124639, -7469781, -2858068 }, { 9681908, -6737123, -31951644, 13591838, -6883821, 386950, 31622781, 6439245, -14581012, 4091397 }, { -8426427, 1470727, -28109679, -1596990, 3978627, -5123623, -19622683, 12092163, 29077877, -14741988 }, }, { { 5269168, -6859726, -13230211, -8020715, 25932563, 1763552, -5606110, -5505881, -20017847, 2357889 }, { 32264008, -15407652, -5387735, -1160093, -2091322, -3946900, 23104804, -12869908, 5727338, 189038 }, { 14609123, -8954470, -6000566, -16622781, -14577387, -7743898, -26745169, 10942115, -25888931, -14884697 }, }, { { 20513500, 5557931, -15604613, 7829531, 26413943, -2019404, -21378968, 7471781, 13913677, -5137875 }, { -25574376, 11967826, 29233242, 12948236, -6754465, 4713227, -8940970, 14059180, 12878652, 8511905 }, { -25656801, 3393631, -2955415, -7075526, -2250709, 9366908, -30223418, 6812974, 5568676, -3127656 }, }, { { 11630004, 12144454, 2116339, 13606037, 27378885, 15676917, -17408753, -13504373, -14395196, 8070818 }, { 27117696, -10007378, -31282771, -5570088, 1127282, 12772488, -29845906, 10483306, -11552749, -1028714 }, { 10637467, -5688064, 5674781, 1072708, -26343588, -6982302, -1683975, 9177853, -27493162, 15431203 }, }, { { 20525145, 10892566, -12742472, 12779443, -29493034, 16150075, -28240519, 14943142, -15056790, -7935931 }, { -30024462, 5626926, -551567, -9981087, 753598, 11981191, 25244767, -3239766, -3356550, 9594024 }, { -23752644, 2636870, -5163910, -10103818, 585134, 7877383, 11345683, -6492290, 13352335, -10977084 }, }, { { -1931799, -5407458, 3304649, -12884869, 17015806, -4877091, -29783850, -7752482, -13215537, -319204 }, { 20239939, 6607058, 6203985, 3483793, -18386976, -779229, -20723742, 15077870, -22750759, 14523817 }, { 27406042, -6041657, 27423596, -4497394, 4996214, 10002360, -28842031, -4545494, -30172742, -4805667 }, }, }, { { { 11374242, 12660715, 17861383, -12540833, 10935568, 1099227, -13886076, -9091740, -27727044, 11358504 }, { -12730809, 10311867, 1510375, 10778093, -2119455, -9145702, 32676003, 11149336, -26123651, 4985768 }, { -19096303, 341147, -6197485, -239033, 15756973, -8796662, -983043, 13794114, -19414307, -15621255 }, }, { { 6490081, 11940286, 25495923, -7726360, 8668373, -8751316, 3367603, 6970005, -1691065, -9004790 }, { 1656497, 13457317, 15370807, 6364910, 13605745, 8362338, -19174622, -5475723, -16796596, -5031438 }, { -22273315, -13524424, -64685, -4334223, -18605636, -10921968, -20571065, -7007978, -99853, -10237333 }, }, { { 17747465, 10039260, 19368299, -4050591, -20630635, -16041286, 31992683, -15857976, -29260363, -5511971 }, { 31932027, -4986141, -19612382, 16366580, 22023614, 88450, 11371999, -3744247, 4882242, -10626905 }, { 29796507, 37186, 19818052, 10115756, -11829032, 3352736, 18551198, 3272828, -5190932, -4162409 }, }, { { 12501286, 4044383, -8612957, -13392385, -32430052, 5136599, -19230378, -3529697, 330070, -3659409 }, { 6384877, 2899513, 17807477, 7663917, -2358888, 12363165, 25366522, -8573892, -271295, 12071499 }, { -8365515, -4042521, 25133448, -4517355, -6211027, 2265927, -32769618, 1936675, -5159697, 3829363 }, }, { { 28425966, -5835433, -577090, -4697198, -14217555, 6870930, 7921550, -6567787, 26333140, 14267664 }, { -11067219, 11871231, 27385719, -10559544, -4585914, -11189312, 10004786, -8709488, -21761224, 8930324 }, { -21197785, -16396035, 25654216, -1725397, 12282012, 11008919, 1541940, 4757911, -26491501, -16408940 }, }, { { 13537262, -7759490, -20604840, 10961927, -5922820, -13218065, -13156584, 6217254, -15943699, 13814990 }, { -17422573, 15157790, 18705543, 29619, 24409717, -260476, 27361681, 9257833, -1956526, -1776914 }, { -25045300, -10191966, 15366585, 15166509, -13105086, 8423556, -29171540, 12361135, -18685978, 4578290 }, }, { { 24579768, 3711570, 1342322, -11180126, -27005135, 14124956, -22544529, 14074919, 21964432, 8235257 }, { -6528613, -2411497, 9442966, -5925588, 12025640, -1487420, -2981514, -1669206, 13006806, 2355433 }, { -16304899, -13605259, -6632427, -5142349, 16974359, -10911083, 27202044, 1719366, 1141648, -12796236 }, }, { { -12863944, -13219986, -8318266, -11018091, -6810145, -4843894, 13475066, -3133972, 32674895, 13715045 }, { 11423335, -5468059, 32344216, 8962751, 24989809, 9241752, -13265253, 16086212, -28740881, -15642093 }, { -1409668, 12530728, -6368726, 10847387, 19531186, -14132160, -11709148, 7791794, -27245943, 4383347 }, }, }, { { { -28970898, 5271447, -1266009, -9736989, -12455236, 16732599, -4862407, -4906449, 27193557, 6245191 }, { -15193956, 5362278, -1783893, 2695834, 4960227, 12840725, 23061898, 3260492, 22510453, 8577507 }, { -12632451, 11257346, -32692994, 13548177, -721004, 10879011, 31168030, 13952092, -29571492, -3635906 }, }, { { 3877321, -9572739, 32416692, 5405324, -11004407, -13656635, 3759769, 11935320, 5611860, 8164018 }, { -16275802, 14667797, 15906460, 12155291, -22111149, -9039718, 32003002, -8832289, 5773085, -8422109 }, { -23788118, -8254300, 1950875, 8937633, 18686727, 16459170, -905725, 12376320, 31632953, 190926 }, }, { { -24593607, -16138885, -8423991, 13378746, 14162407, 6901328, -8288749, 4508564, -25341555, -3627528 }, { 8884438, -5884009, 6023974, 10104341, -6881569, -4941533, 18722941, -14786005, -1672488, 827625 }, { -32720583, -16289296, -32503547, 7101210, 13354605, 2659080, -1800575, -14108036, -24878478, 1541286 }, }, { { 2901347, -1117687, 3880376, -10059388, -17620940, -3612781, -21802117, -3567481, 20456845, -1885033 }, { 27019610, 12299467, -13658288, -1603234, -12861660, -4861471, -19540150, -5016058, 29439641, 15138866 }, { 21536104, -6626420, -32447818, -10690208, -22408077, 5175814, -5420040, -16361163, 7779328, 109896 }, }, { { 30279744, 14648750, -8044871, 6425558, 13639621, -743509, 28698390, 12180118, 23177719, -554075 }, { 26572847, 3405927, -31701700, 12890905, -19265668, 5335866, -6493768, 2378492, 4439158, -13279347 }, { -22716706, 3489070, -9225266, -332753, 18875722, -1140095, 14819434, -12731527, -17717757, -5461437 }, }, { { -5056483, 16566551, 15953661, 3767752, -10436499, 15627060, -820954, 2177225, 8550082, -15114165 }, { -18473302, 16596775, -381660, 15663611, 22860960, 15585581, -27844109, -3582739, -23260460, -8428588 }, { -32480551, 15707275, -8205912, -5652081, 29464558, 2713815, -22725137, 15860482, -21902570, 1494193 }, }, { { -19562091, -14087393, -25583872, -9299552, 13127842, 759709, 21923482, 16529112, 8742704, 12967017 }, { -28464899, 1553205, 32536856, -10473729, -24691605, -406174, -8914625, -2933896, -29903758, 15553883 }, { 21877909, 3230008, 9881174, 10539357, -4797115, 2841332, 11543572, 14513274, 19375923, -12647961 }, }, { { 8832269, -14495485, 13253511, 5137575, 5037871, 4078777, 24880818, -6222716, 2862653, 9455043 }, { 29306751, 5123106, 20245049, -14149889, 9592566, 8447059, -2077124, -2990080, 15511449, 4789663 }, { -20679756, 7004547, 8824831, -9434977, -4045704, -3750736, -5754762, 108893, 23513200, 16652362 }, }, }, { { { -33256173, 4144782, -4476029, -6579123, 10770039, -7155542, -6650416, -12936300, -18319198, 10212860 }, { 2756081, 8598110, 7383731, -6859892, 22312759, -1105012, 21179801, 2600940, -9988298, -12506466 }, { -24645692, 13317462, -30449259, -15653928, 21365574, -10869657, 11344424, 864440, -2499677, -16710063 }, }, { { -26432803, 6148329, -17184412, -14474154, 18782929, -275997, -22561534, 211300, 2719757, 4940997 }, { -1323882, 3911313, -6948744, 14759765, -30027150, 7851207, 21690126, 8518463, 26699843, 5276295 }, { -13149873, -6429067, 9396249, 365013, 24703301, -10488939, 1321586, 149635, -15452774, 7159369 }, }, { { 9987780, -3404759, 17507962, 9505530, 9731535, -2165514, 22356009, 8312176, 22477218, -8403385 }, { 18155857, -16504990, 19744716, 9006923, 15154154, -10538976, 24256460, -4864995, -22548173, 9334109 }, { 2986088, -4911893, 10776628, -3473844, 10620590, -7083203, -21413845, 14253545, -22587149, 536906 }, }, { { 4377756, 8115836, 24567078, 15495314, 11625074, 13064599, 7390551, 10589625, 10838060, -15420424 }, { -19342404, 867880, 9277171, -3218459, -14431572, -1986443, 19295826, -15796950, 6378260, 699185 }, { 7895026, 4057113, -7081772, -13077756, -17886831, -323126, -716039, 15693155, -5045064, -13373962 }, }, { { -7737563, -5869402, -14566319, -7406919, 11385654, 13201616, 31730678, -10962840, -3918636, -9669325 }, { 10188286, -15770834, -7336361, 13427543, 22223443, 14896287, 30743455, 7116568, -21786507, 5427593 }, { 696102, 13206899, 27047647, -10632082, 15285305, -9853179, 10798490, -4578720, 19236243, 12477404 }, }, { { -11229439, 11243796, -17054270, -8040865, -788228, -8167967, -3897669, 11180504, -23169516, 7733644 }, { 17800790, -14036179, -27000429, -11766671, 23887827, 3149671, 23466177, -10538171, 10322027, 15313801 }, { 26246234, 11968874, 32263343, -5468728, 6830755, -13323031, -15794704, -101982, -24449242, 10890804 }, }, { { -31365647, 10271363, -12660625, -6267268, 16690207, -13062544, -14982212, 16484931, 25180797, -5334884 }, { -586574, 10376444, -32586414, -11286356, 19801893, 10997610, 2276632, 9482883, 316878, 13820577 }, { -9882808, -4510367, -2115506, 16457136, -11100081, 11674996, 30756178, -7515054, 30696930, -3712849 }, }, { { 32988917, -9603412, 12499366, 7910787, -10617257, -11931514, -7342816, -9985397, -32349517, 7392473 }, { -8855661, 15927861, 9866406, -3649411, -2396914, -16655781, -30409476, -9134995, 25112947, -2926644 }, { -2504044, -436966, 25621774, -5678772, 15085042, -5479877, -24884878, -13526194, 5537438, -13914319 }, }, }, { { { -11225584, 2320285, -9584280, 10149187, -33444663, 5808648, -14876251, -1729667, 31234590, 6090599 }, { -9633316, 116426, 26083934, 2897444, -6364437, -2688086, 609721, 15878753, -6970405, -9034768 }, { -27757857, 247744, -15194774, -9002551, 23288161, -10011936, -23869595, 6503646, 20650474, 1804084 }, }, { { -27589786, 15456424, 8972517, 8469608, 15640622, 4439847, 3121995, -10329713, 27842616, -202328 }, { -15306973, 2839644, 22530074, 10026331, 4602058, 5048462, 28248656, 5031932, -11375082, 12714369 }, { 20807691, -7270825, 29286141, 11421711, -27876523, -13868230, -21227475, 1035546, -19733229, 12796920 }, }, { { 12076899, -14301286, -8785001, -11848922, -25012791, 16400684, -17591495, -12899438, 3480665, -15182815 }, { -32361549, 5457597, 28548107, 7833186, 7303070, -11953545, -24363064, -15921875, -33374054, 2771025 }, { -21389266, 421932, 26597266, 6860826, 22486084, -6737172, -17137485, -4210226, -24552282, 15673397 }, }, { { -20184622, 2338216, 19788685, -9620956, -4001265, -8740893, -20271184, 4733254, 3727144, -12934448 }, { 6120119, 814863, -11794402, -622716, 6812205, -15747771, 2019594, 7975683, 31123697, -10958981 }, { 30069250, -11435332, 30434654, 2958439, 18399564, -976289, 12296869, 9204260, -16432438, 9648165 }, }, { { 32705432, -1550977, 30705658, 7451065, -11805606, 9631813, 3305266, 5248604, -26008332, -11377501 }, { 17219865, 2375039, -31570947, -5575615, -19459679, 9219903, 294711, 15298639, 2662509, -16297073 }, { -1172927, -7558695, -4366770, -4287744, -21346413, -8434326, 32087529, -1222777, 32247248, -14389861 }, }, { { 14312628, 1221556, 17395390, -8700143, -4945741, -8684635, -28197744, -9637817, -16027623, -13378845 }, { -1428825, -9678990, -9235681, 6549687, -7383069, -468664, 23046502, 9803137, 17597934, 2346211 }, { 18510800, 15337574, 26171504, 981392, -22241552, 7827556, -23491134, -11323352, 3059833, -11782870 }, }, { { 10141598, 6082907, 17829293, -1947643, 9830092, 13613136, -25556636, -5544586, -33502212, 3592096 }, { 33114168, -15889352, -26525686, -13343397, 33076705, 8716171, 1151462, 1521897, -982665, -6837803 }, { -32939165, -4255815, 23947181, -324178, -33072974, -12305637, -16637686, 3891704, 26353178, 693168 }, }, { { 30374239, 1595580, -16884039, 13186931, 4600344, 406904, 9585294, -400668, 31375464, 14369965 }, { -14370654, -7772529, 1510301, 6434173, -18784789, -6262728, 32732230, -13108839, 17901441, 16011505 }, { 18171223, -11934626, -12500402, 15197122, -11038147, -15230035, -19172240, -16046376, 8764035, 12309598 }, }, }, { { { 5975908, -5243188, -19459362, -9681747, -11541277, 14015782, -23665757, 1228319, 17544096, -10593782 }, { 5811932, -1715293, 3442887, -2269310, -18367348, -8359541, -18044043, -15410127, -5565381, 12348900 }, { -31399660, 11407555, 25755363, 6891399, -3256938, 14872274, -24849353, 8141295, -10632534, -585479 }, }, { { -12675304, 694026, -5076145, 13300344, 14015258, -14451394, -9698672, -11329050, 30944593, 1130208 }, { 8247766, -6710942, -26562381, -7709309, -14401939, -14648910, 4652152, 2488540, 23550156, -271232 }, { 17294316, -3788438, 7026748, 15626851, 22990044, 113481, 2267737, -5908146, -408818, -137719 }, }, { { 16091085, -16253926, 18599252, 7340678, 2137637, -1221657, -3364161, 14550936, 3260525, -7166271 }, { -4910104, -13332887, 18550887, 10864893, -16459325, -7291596, -23028869, -13204905, -12748722, 2701326 }, { -8574695, 16099415, 4629974, -16340524, -20786213, -6005432, -10018363, 9276971, 11329923, 1862132 }, }, { { 14763076, -15903608, -30918270, 3689867, 3511892, 10313526, -21951088, 12219231, -9037963, -940300 }, { 8894987, -3446094, 6150753, 3013931, 301220, 15693451, -31981216, -2909717, -15438168, 11595570 }, { 15214962, 3537601, -26238722, -14058872, 4418657, -15230761, 13947276, 10730794, -13489462, -4363670 }, }, { { -2538306, 7682793, 32759013, 263109, -29984731, -7955452, -22332124, -10188635, 977108, 699994 }, { -12466472, 4195084, -9211532, 550904, -15565337, 12917920, 19118110, -439841, -30534533, -14337913 }, { 31788461, -14507657, 4799989, 7372237, 8808585, -14747943, 9408237, -10051775, 12493932, -5409317 }, }, { { -25680606, 5260744, -19235809, -6284470, -3695942, 16566087, 27218280, 2607121, 29375955, 6024730 }, { 842132, -2794693, -4763381, -8722815, 26332018, -12405641, 11831880, 6985184, -9940361, 2854096 }, { -4847262, -7969331, 2516242, -5847713, 9695691, -7221186, 16512645, 960770, 12121869, 16648078 }, }, { { -15218652, 14667096, -13336229, 2013717, 30598287, -464137, -31504922, -7882064, 20237806, 2838411 }, { -19288047, 4453152, 15298546, -16178388, 22115043, -15972604, 12544294, -13470457, 1068881, -12499905 }, { -9558883, -16518835, 33238498, 13506958, 30505848, -1114596, -8486907, -2630053, 12521378, 4845654 }, }, { { -28198521, 10744108, -2958380, 10199664, 7759311, -13088600, 3409348, -873400, -6482306, -12885870 }, { -23561822, 6230156, -20382013, 10655314, -24040585, -11621172, 10477734, -1240216, -3113227, 13974498 }, { 12966261, 15550616, -32038948, -1615346, 21025980, -629444, 5642325, 7188737, 18895762, 12629579 }, }, }, { { { 14741879, -14946887, 22177208, -11721237, 1279741, 8058600, 11758140, 789443, 32195181, 3895677 }, { 10758205, 15755439, -4509950, 9243698, -4879422, 6879879, -2204575, -3566119, -8982069, 4429647 }, { -2453894, 15725973, -20436342, -10410672, -5803908, -11040220, -7135870, -11642895, 18047436, -15281743 }, }, { { -25173001, -11307165, 29759956, 11776784, -22262383, -15820455, 10993114, -12850837, -17620701, -9408468 }, { 21987233, 700364, -24505048, 14972008, -7774265, -5718395, 32155026, 2581431, -29958985, 8773375 }, { -25568350, 454463, -13211935, 16126715, 25240068, 8594567, 20656846, 12017935, -7874389, -13920155 }, }, { { 6028182, 6263078, -31011806, -11301710, -818919, 2461772, -31841174, -5468042, -1721788, -2776725 }, { -12278994, 16624277, 987579, -5922598, 32908203, 1248608, 7719845, -4166698, 28408820, 6816612 }, { -10358094, -8237829, 19549651, -12169222, 22082623, 16147817, 20613181, 13982702, -10339570, 5067943 }, }, { { -30505967, -3821767, 12074681, 13582412, -19877972, 2443951, -19719286, 12746132, 5331210, -10105944 }, { 30528811, 3601899, -1957090, 4619785, -27361822, -15436388, 24180793, -12570394, 27679908, -1648928 }, { 9402404, -13957065, 32834043, 10838634, -26580150, -13237195, 26653274, -8685565, 22611444, -12715406 }, }, { { 22190590, 1118029, 22736441, 15130463, -30460692, -5991321, 19189625, -4648942, 4854859, 6622139 }, { -8310738, -2953450, -8262579, -3388049, -10401731, -271929, 13424426, -3567227, 26404409, 13001963 }, { -31241838, -15415700, -2994250, 8939346, 11562230, -12840670, -26064365, -11621720, -15405155, 11020693 }, }, { { 1866042, -7949489, -7898649, -10301010, 12483315, 13477547, 3175636, -12424163, 28761762, 1406734 }, { -448555, -1777666, 13018551, 3194501, -9580420, -11161737, 24760585, -4347088, 25577411, -13378680 }, { -24290378, 4759345, -690653, -1852816, 2066747, 10693769, -29595790, 9884936, -9368926, 4745410 }, }, { { -9141284, 6049714, -19531061, -4341411, -31260798, 9944276, -15462008, -11311852, 10931924, -11931931 }, { -16561513, 14112680, -8012645, 4817318, -8040464, -11414606, -22853429, 10856641, -20470770, 13434654 }, { 22759489, -10073434, -16766264, -1871422, 13637442, -10168091, 1765144, -12654326, 28445307, -5364710 }, }, { { 29875063, 12493613, 2795536, -3786330, 1710620, 15181182, -10195717, -8788675, 9074234, 1167180 }, { -26205683, 11014233, -9842651, -2635485, -26908120, 7532294, -18716888, -9535498, 3843903, 9367684 }, { -10969595, -6403711, 9591134, 9582310, 11349256, 108879, 16235123, 8601684, -139197, 4242895 }, }, }, { { { 22092954, -13191123, -2042793, -11968512, 32186753, -11517388, -6574341, 2470660, -27417366, 16625501 }, { -11057722, 3042016, 13770083, -9257922, 584236, -544855, -7770857, 2602725, -27351616, 14247413 }, { 6314175, -10264892, -32772502, 15957557, -10157730, 168750, -8618807, 14290061, 27108877, -1180880 }, }, { { -8586597, -7170966, 13241782, 10960156, -32991015, -13794596, 33547976, -11058889, -27148451, 981874 }, { 22833440, 9293594, -32649448, -13618667, -9136966, 14756819, -22928859, -13970780, -10479804, -16197962 }, { -7768587, 3326786, -28111797, 10783824, 19178761, 14905060, 22680049, 13906969, -15933690, 3797899 }, }, { { 21721356, -4212746, -12206123, 9310182, -3882239, -13653110, 23740224, -2709232, 20491983, -8042152 }, { 9209270, -15135055, -13256557, -6167798, -731016, 15289673, 25947805, 15286587, 30997318, -6703063 }, { 7392032, 16618386, 23946583, -8039892, -13265164, -1533858, -14197445, -2321576, 17649998, -250080 }, }, { { -9301088, -14193827, 30609526, -3049543, -25175069, -1283752, -15241566, -9525724, -2233253, 7662146 }, { -17558673, 1763594, -33114336, 15908610, -30040870, -12174295, 7335080, -8472199, -3174674, 3440183 }, { -19889700, -5977008, -24111293, -9688870, 10799743, -16571957, 40450, -4431835, 4862400, 1133 }, }, { { -32856209, -7873957, -5422389, 14860950, -16319031, 7956142, 7258061, 311861, -30594991, -7379421 }, { -3773428, -1565936, 28985340, 7499440, 24445838, 9325937, 29727763, 16527196, 18278453, 15405622 }, { -4381906, 8508652, -19898366, -3674424, -5984453, 15149970, -13313598, 843523, -21875062, 13626197 }, }, { { 2281448, -13487055, -10915418, -2609910, 1879358, 16164207, -10783882, 3953792, 13340839, 15928663 }, { 31727126, -7179855, -18437503, -8283652, 2875793, -16390330, -25269894, -7014826, -23452306, 5964753 }, { 4100420, -5959452, -17179337, 6017714, -18705837, 12227141, -26684835, 11344144, 2538215, -7570755 }, }, { { -9433605, 6123113, 11159803, -2156608, 30016280, 14966241, -20474983, 1485421, -629256, -15958862 }, { -26804558, 4260919, 11851389, 9658551, -32017107, 16367492, -20205425, -13191288, 11659922, -11115118 }, { 26180396, 10015009, -30844224, -8581293, 5418197, 9480663, 2231568, -10170080, 33100372, -1306171 }, }, { { 15121113, -5201871, -10389905, 15427821, -27509937, -15992507, 21670947, 4486675, -5931810, -14466380 }, { 16166486, -9483733, -11104130, 6023908, -31926798, -1364923, 2340060, -16254968, -10735770, -10039824 }, { 28042865, -3557089, -12126526, 12259706, -3717498, -6945899, 6766453, -8689599, 18036436, 5803270 }, }, }, { { { -817581, 6763912, 11803561, 1585585, 10958447, -2671165, 23855391, 4598332, -6159431, -14117438 }, { -31031306, -14256194, 17332029, -2383520, 31312682, -5967183, 696309, 50292, -20095739, 11763584 }, { -594563, -2514283, -32234153, 12643980, 12650761, 14811489, 665117, -12613632, -19773211, -10713562 }, }, { { 30464590, -11262872, -4127476, -12734478, 19835327, -7105613, -24396175, 2075773, -17020157, 992471 }, { 18357185, -6994433, 7766382, 16342475, -29324918, 411174, 14578841, 8080033, -11574335, -10601610 }, { 19598397, 10334610, 12555054, 2555664, 18821899, -10339780, 21873263, 16014234, 26224780, 16452269 }, }, { { -30223925, 5145196, 5944548, 16385966, 3976735, 2009897, -11377804, -7618186, -20533829, 3698650 }, { 14187449, 3448569, -10636236, -10810935, -22663880, -3433596, 7268410, -10890444, 27394301, 12015369 }, { 19695761, 16087646, 28032085, 12999827, 6817792, 11427614, 20244189, -1312777, -13259127, -3402461 }, }, { { 30860103, 12735208, -1888245, -4699734, -16974906, 2256940, -8166013, 12298312, -8550524, -10393462 }, { -5719826, -11245325, -1910649, 15569035, 26642876, -7587760, -5789354, -15118654, -4976164, 12651793 }, { -2848395, 9953421, 11531313, -5282879, 26895123, -12697089, -13118820, -16517902, 9768698, -2533218 }, }, { { -24719459, 1894651, -287698, -4704085, 15348719, -8156530, 32767513, 12765450, 4940095, 10678226 }, { 18860224, 15980149, -18987240, -1562570, -26233012, -11071856, -7843882, 13944024, -24372348, 16582019 }, { -15504260, 4970268, -29893044, 4175593, -20993212, -2199756, -11704054, 15444560, -11003761, 7989037 }, }, { { 31490452, 5568061, -2412803, 2182383, -32336847, 4531686, -32078269, 6200206, -19686113, -14800171 }, { -17308668, -15879940, -31522777, -2831, -32887382, 16375549, 8680158, -16371713, 28550068, -6857132 }, { -28126887, -5688091, 16837845, -1820458, -6850681, 12700016, -30039981, 4364038, 1155602, 5988841 }, }, { { 21890435, -13272907, -12624011, 12154349, -7831873, 15300496, 23148983, -4470481, 24618407, 8283181 }, { -33136107, -10512751, 9975416, 6841041, -31559793, 16356536, 3070187, -7025928, 1466169, 10740210 }, { -1509399, -15488185, -13503385, -10655916, 32799044, 909394, -13938903, -5779719, -32164649, -15327040 }, }, { { 3960823, -14267803, -28026090, -15918051, -19404858, 13146868, 15567327, 951507, -3260321, -573935 }, { 24740841, 5052253, -30094131, 8961361, 25877428, 6165135, -24368180, 14397372, -7380369, -6144105 }, { -28888365, 3510803, -28103278, -1158478, -11238128, -10631454, -15441463, -14453128, -1625486, -6494814 }, }, }, { { { 793299, -9230478, 8836302, -6235707, -27360908, -2369593, 33152843, -4885251, -9906200, -621852 }, { 5666233, 525582, 20782575, -8038419, -24538499, 14657740, 16099374, 1468826, -6171428, -15186581 }, { -4859255, -3779343, -2917758, -6748019, 7778750, 11688288, -30404353, -9871238, -1558923, -9863646 }, }, { { 10896332, -7719704, 824275, 472601, -19460308, 3009587, 25248958, 14783338, -30581476, -15757844 }, { 10566929, 12612572, -31944212, 11118703, -12633376, 12362879, 21752402, 8822496, 24003793, 14264025 }, { 27713862, -7355973, -11008240, 9227530, 27050101, 2504721, 23886875, -13117525, 13958495, -5732453 }, }, { { -23481610, 4867226, -27247128, 3900521, 29838369, -8212291, -31889399, -10041781, 7340521, -15410068 }, { 4646514, -8011124, -22766023, -11532654, 23184553, 8566613, 31366726, -1381061, -15066784, -10375192 }, { -17270517, 12723032, -16993061, 14878794, 21619651, -6197576, 27584817, 3093888, -8843694, 3849921 }, }, { { -9064912, 2103172, 25561640, -15125738, -5239824, 9582958, 32477045, -9017955, 5002294, -15550259 }, { -12057553, -11177906, 21115585, -13365155, 8808712, -12030708, 16489530, 13378448, -25845716, 12741426 }, { -5946367, 10645103, -30911586, 15390284, -3286982, -7118677, 24306472, 15852464, 28834118, -7646072 }, }, { { -17335748, -9107057, -24531279, 9434953, -8472084, -583362, -13090771, 455841, 20461858, 5491305 }, { 13669248, -16095482, -12481974, -10203039, -14569770, -11893198, -24995986, 11293807, -28588204, -9421832 }, { 28497928, 6272777, -33022994, 14470570, 8906179, -1225630, 18504674, -14165166, 29867745, -8795943 }, }, { { -16207023, 13517196, -27799630, -13697798, 24009064, -6373891, -6367600, -13175392, 22853429, -4012011 }, { 24191378, 16712145, -13931797, 15217831, 14542237, 1646131, 18603514, -11037887, 12876623, -2112447 }, { 17902668, 4518229, -411702, -2829247, 26878217, 5258055, -12860753, 608397, 16031844, 3723494 }, }, { { -28632773, 12763728, -20446446, 7577504, 33001348, -13017745, 17558842, -7872890, 23896954, -4314245 }, { -20005381, -12011952, 31520464, 605201, 2543521, 5991821, -2945064, 7229064, -9919646, -8826859 }, { 28816045, 298879, -28165016, -15920938, 19000928, -1665890, -12680833, -2949325, -18051778, -2082915 }, }, { { 16000882, -344896, 3493092, -11447198, -29504595, -13159789, 12577740, 16041268, -19715240, 7847707 }, { 10151868, 10572098, 27312476, 7922682, 14825339, 4723128, -32855931, -6519018, -10020567, 3852848 }, { -11430470, 15697596, -21121557, -4420647, 5386314, 15063598, 16514493, -15932110, 29330899, -15076224 }, }, }, { { { -25499735, -4378794, -15222908, -6901211, 16615731, 2051784, 3303702, 15490, -27548796, 12314391 }, { 15683520, -6003043, 18109120, -9980648, 15337968, -5997823, -16717435, 15921866, 16103996, -3731215 }, { -23169824, -10781249, 13588192, -1628807, -3798557, -1074929, -19273607, 5402699, -29815713, -9841101 }, }, { { 23190676, 2384583, -32714340, 3462154, -29903655, -1529132, -11266856, 8911517, -25205859, 2739713 }, { 21374101, -3554250, -33524649, 9874411, 15377179, 11831242, -33529904, 6134907, 4931255, 11987849 }, { -7732, -2978858, -16223486, 7277597, 105524, -322051, -31480539, 13861388, -30076310, 10117930 }, }, { { -29501170, -10744872, -26163768, 13051539, -25625564, 5089643, -6325503, 6704079, 12890019, 15728940 }, { -21972360, -11771379, -951059, -4418840, 14704840, 2695116, 903376, -10428139, 12885167, 8311031 }, { -17516482, 5352194, 10384213, -13811658, 7506451, 13453191, 26423267, 4384730, 1888765, -5435404 }, }, { { -25817338, -3107312, -13494599, -3182506, 30896459, -13921729, -32251644, -12707869, -19464434, -3340243 }, { -23607977, -2665774, -526091, 4651136, 5765089, 4618330, 6092245, 14845197, 17151279, -9854116 }, { -24830458, -12733720, -15165978, 10367250, -29530908, -265356, 22825805, -7087279, -16866484, 16176525 }, }, { { -23583256, 6564961, 20063689, 3798228, -4740178, 7359225, 2006182, -10363426, -28746253, -10197509 }, { -10626600, -4486402, -13320562, -5125317, 3432136, -6393229, 23632037, -1940610, 32808310, 1099883 }, { 15030977, 5768825, -27451236, -2887299, -6427378, -15361371, -15277896, -6809350, 2051441, -15225865 }, }, { { -3362323, -7239372, 7517890, 9824992, 23555850, 295369, 5148398, -14154188, -22686354, 16633660 }, { 4577086, -16752288, 13249841, -15304328, 19958763, -14537274, 18559670, -10759549, 8402478, -9864273 }, { -28406330, -1051581, -26790155, -907698, -17212414, -11030789, 9453451, -14980072, 17983010, 9967138 }, }, { { -25762494, 6524722, 26585488, 9969270, 24709298, 1220360, -1677990, 7806337, 17507396, 3651560 }, { -10420457, -4118111, 14584639, 15971087, -15768321, 8861010, 26556809, -5574557, -18553322, -11357135 }, { 2839101, 14284142, 4029895, 3472686, 14402957, 12689363, -26642121, 8459447, -5605463, -7621941 }, }, { { -4839289, -3535444, 9744961, 2871048, 25113978, 3187018, -25110813, -849066, 17258084, -7977739 }, { 18164541, -10595176, -17154882, -1542417, 19237078, -9745295, 23357533, -15217008, 26908270, 12150756 }, { -30264870, -7647865, 5112249, -7036672, -1499807, -6974257, 43168, -5537701, -32302074, 16215819 }, }, }, { { { -6898905, 9824394, -12304779, -4401089, -31397141, -6276835, 32574489, 12532905, -7503072, -8675347 }, { -27343522, -16515468, -27151524, -10722951, 946346, 16291093, 254968, 7168080, 21676107, -1943028 }, { 21260961, -8424752, -16831886, -11920822, -23677961, 3968121, -3651949, -6215466, -3556191, -7913075 }, }, { { 16544754, 13250366, -16804428, 15546242, -4583003, 12757258, -2462308, -8680336, -18907032, -9662799 }, { -2415239, -15577728, 18312303, 4964443, -15272530, -12653564, 26820651, 16690659, 25459437, -4564609 }, { -25144690, 11425020, 28423002, -11020557, -6144921, -15826224, 9142795, -2391602, -6432418, -1644817 }, }, { { -23104652, 6253476, 16964147, -3768872, -25113972, -12296437, -27457225, -16344658, 6335692, 7249989 }, { -30333227, 13979675, 7503222, -12368314, -11956721, -4621693, -30272269, 2682242, 25993170, -12478523 }, { 4364628, 5930691, 32304656, -10044554, -8054781, 15091131, 22857016, -10598955, 31820368, 15075278 }, }, { { 31879134, -8918693, 17258761, 90626, -8041836, -4917709, 24162788, -9650886, -17970238, 12833045 }, { 19073683, 14851414, -24403169, -11860168, 7625278, 11091125, -19619190, 2074449, -9413939, 14905377 }, { 24483667, -11935567, -2518866, -11547418, -1553130, 15355506, -25282080, 9253129, 27628530, -7555480 }, }, { { 17597607, 8340603, 19355617, 552187, 26198470, -3176583, 4593324, -9157582, -14110875, 15297016 }, { 510886, 14337390, -31785257, 16638632, 6328095, 2713355, -20217417, -11864220, 8683221, 2921426 }, { 18606791, 11874196, 27155355, -5281482, -24031742, 6265446, -25178240, -1278924, 4674690, 13890525 }, }, { { 13609624, 13069022, -27372361, -13055908, 24360586, 9592974, 14977157, 9835105, 4389687, 288396 }, { 9922506, -519394, 13613107, 5883594, -18758345, -434263, -12304062, 8317628, 23388070, 16052080 }, { 12720016, 11937594, -31970060, -5028689, 26900120, 8561328, -20155687, -11632979, -14754271, -10812892 }, }, { { 15961858, 14150409, 26716931, -665832, -22794328, 13603569, 11829573, 7467844, -28822128, 929275 }, { 11038231, -11582396, -27310482, -7316562, -10498527, -16307831, -23479533, -9371869, -21393143, 2465074 }, { 20017163, -4323226, 27915242, 1529148, 12396362, 15675764, 13817261, -9658066, 2463391, -4622140 }, }, { { -16358878, -12663911, -12065183, 4996454, -1256422, 1073572, 9583558, 12851107, 4003896, 12673717 }, { -1731589, -15155870, -3262930, 16143082, 19294135, 13385325, 14741514, -9103726, 7903886, 2348101 }, { 24536016, -16515207, 12715592, -3862155, 1511293, 10047386, -3842346, -7129159, -28377538, 10048127 }, }, }, { { { -12622226, -6204820, 30718825, 2591312, -10617028, 12192840, 18873298, -7297090, -32297756, 15221632 }, { -26478122, -11103864, 11546244, -1852483, 9180880, 7656409, -21343950, 2095755, 29769758, 6593415 }, { -31994208, -2907461, 4176912, 3264766, 12538965, -868111, 26312345, -6118678, 30958054, 8292160 }, }, { { 31429822, -13959116, 29173532, 15632448, 12174511, -2760094, 32808831, 3977186, 26143136, -3148876 }, { 22648901, 1402143, -22799984, 13746059, 7936347, 365344, -8668633, -1674433, -3758243, -2304625 }, { -15491917, 8012313, -2514730, -12702462, -23965846, -10254029, -1612713, -1535569, -16664475, 8194478 }, }, { { 27338066, -7507420, -7414224, 10140405, -19026427, -6589889, 27277191, 8855376, 28572286, 3005164 }, { 26287124, 4821776, 25476601, -4145903, -3764513, -15788984, -18008582, 1182479, -26094821, -13079595 }, { -7171154, 3178080, 23970071, 6201893, -17195577, -4489192, -21876275, -13982627, 32208683, -1198248 }, }, { { -16657702, 2817643, -10286362, 14811298, 6024667, 13349505, -27315504, -10497842, -27672585, -11539858 }, { 15941029, -9405932, -21367050, 8062055, 31876073, -238629, -15278393, -1444429, 15397331, -4130193 }, { 8934485, -13485467, -23286397, -13423241, -32446090, 14047986, 31170398, -1441021, -27505566, 15087184 }, }, { { -18357243, -2156491, 24524913, -16677868, 15520427, -6360776, -15502406, 11461896, 16788528, -5868942 }, { -1947386, 16013773, 21750665, 3714552, -17401782, -16055433, -3770287, -10323320, 31322514, -11615635 }, { 21426655, -5650218, -13648287, -5347537, -28812189, -4920970, -18275391, -14621414, 13040862, -12112948 }, }, { { 11293895, 12478086, -27136401, 15083750, -29307421, 14748872, 14555558, -13417103, 1613711, 4896935 }, { -25894883, 15323294, -8489791, -8057900, 25967126, -13425460, 2825960, -4897045, -23971776, -11267415 }, { -15924766, -5229880, -17443532, 6410664, 3622847, 10243618, 20615400, 12405433, -23753030, -8436416 }, }, { { -7091295, 12556208, -20191352, 9025187, -17072479, 4333801, 4378436, 2432030, 23097949, -566018 }, { 4565804, -16025654, 20084412, -7842817, 1724999, 189254, 24767264, 10103221, -18512313, 2424778 }, { 366633, -11976806, 8173090, -6890119, 30788634, 5745705, -7168678, 1344109, -3642553, 12412659 }, }, { { -24001791, 7690286, 14929416, -168257, -32210835, -13412986, 24162697, -15326504, -3141501, 11179385 }, { 18289522, -14724954, 8056945, 16430056, -21729724, 7842514, -6001441, -1486897, -18684645, -11443503 }, { 476239, 6601091, -6152790, -9723375, 17503545, -4863900, 27672959, 13403813, 11052904, 5219329 }, }, }, { { { 20678546, -8375738, -32671898, 8849123, -5009758, 14574752, 31186971, -3973730, 9014762, -8579056 }, { -13644050, -10350239, -15962508, 5075808, -1514661, -11534600, -33102500, 9160280, 8473550, -3256838 }, { 24900749, 14435722, 17209120, -15292541, -22592275, 9878983, -7689309, -16335821, -24568481, 11788948 }, }, { { -3118155, -11395194, -13802089, 14797441, 9652448, -6845904, -20037437, 10410733, -24568470, -1458691 }, { -15659161, 16736706, -22467150, 10215878, -9097177, 7563911, 11871841, -12505194, -18513325, 8464118 }, { -23400612, 8348507, -14585951, -861714, -3950205, -6373419, 14325289, 8628612, 33313881, -8370517 }, }, { { -20186973, -4967935, 22367356, 5271547, -1097117, -4788838, -24805667, -10236854, -8940735, -5818269 }, { -6948785, -1795212, -32625683, -16021179, 32635414, -7374245, 15989197, -12838188, 28358192, -4253904 }, { -23561781, -2799059, -32351682, -1661963, -9147719, 10429267, -16637684, 4072016, -5351664, 5596589 }, }, { { -28236598, -3390048, 12312896, 6213178, 3117142, 16078565, 29266239, 2557221, 1768301, 15373193 }, { -7243358, -3246960, -4593467, -7553353, -127927, -912245, -1090902, -4504991, -24660491, 3442910 }, { -30210571, 5124043, 14181784, 8197961, 18964734, -11939093, 22597931, 7176455, -18585478, 13365930 }, }, { { -7877390, -1499958, 8324673, 4690079, 6261860, 890446, 24538107, -8570186, -9689599, -3031667 }, { 25008904, -10771599, -4305031, -9638010, 16265036, 15721635, 683793, -11823784, 15723479, -15163481 }, { -9660625, 12374379, -27006999, -7026148, -7724114, -12314514, 11879682, 5400171, 519526, -1235876 }, }, { { 22258397, -16332233, -7869817, 14613016, -22520255, -2950923, -20353881, 7315967, 16648397, 7605640 }, { -8081308, -8464597, -8223311, 9719710, 19259459, -15348212, 23994942, -5281555, -9468848, 4763278 }, { -21699244, 9220969, -15730624, 1084137, -25476107, -2852390, 31088447, -7764523, -11356529, 728112 }, }, { { 26047220, -11751471, -6900323, -16521798, 24092068, 9158119, -4273545, -12555558, -29365436, -5498272 }, { 17510331, -322857, 5854289, 8403524, 17133918, -3112612, -28111007, 12327945, 10750447, 10014012 }, { -10312768, 3936952, 9156313, -8897683, 16498692, -994647, -27481051, -666732, 3424691, 7540221 }, }, { { 30322361, -6964110, 11361005, -4143317, 7433304, 4989748, -7071422, -16317219, -9244265, 15258046 }, { 13054562, -2779497, 19155474, 469045, -12482797, 4566042, 5631406, 2711395, 1062915, -5136345 }, { -19240248, -11254599, -29509029, -7499965, -5835763, 13005411, -6066489, 12194497, 32960380, 1459310 }, }, }, { { { 19852034, 7027924, 23669353, 10020366, 8586503, -6657907, 394197, -6101885, 18638003, -11174937 }, { 31395534, 15098109, 26581030, 8030562, -16527914, -5007134, 9012486, -7584354, -6643087, -5442636 }, { -9192165, -2347377, -1997099, 4529534, 25766844, 607986, -13222, 9677543, -32294889, -6456008 }, }, { { -2444496, -149937, 29348902, 8186665, 1873760, 12489863, -30934579, -7839692, -7852844, -8138429 }, { -15236356, -15433509, 7766470, 746860, 26346930, -10221762, -27333451, 10754588, -9431476, 5203576 }, { 31834314, 14135496, -770007, 5159118, 20917671, -16768096, -7467973, -7337524, 31809243, 7347066 }, }, { { -9606723, -11874240, 20414459, 13033986, 13716524, -11691881, 19797970, -12211255, 15192876, -2087490 }, { -12663563, -2181719, 1168162, -3804809, 26747877, -14138091, 10609330, 12694420, 33473243, -13382104 }, { 33184999, 11180355, 15832085, -11385430, -1633671, 225884, 15089336, -11023903, -6135662, 14480053 }, }, { { 31308717, -5619998, 31030840, -1897099, 15674547, -6582883, 5496208, 13685227, 27595050, 8737275 }, { -20318852, -15150239, 10933843, -16178022, 8335352, -7546022, -31008351, -12610604, 26498114, 66511 }, { 22644454, -8761729, -16671776, 4884562, -3105614, -13559366, 30540766, -4286747, -13327787, -7515095 }, }, { { -28017847, 9834845, 18617207, -2681312, -3401956, -13307506, 8205540, 13585437, -17127465, 15115439 }, { 23711543, -672915, 31206561, -8362711, 6164647, -9709987, -33535882, -1426096, 8236921, 16492939 }, { -23910559, -13515526, -26299483, -4503841, 25005590, -7687270, 19574902, 10071562, 6708380, -6222424 }, }, { { 2101391, -4930054, 19702731, 2367575, -15427167, 1047675, 5301017, 9328700, 29955601, -11678310 }, { 3096359, 9271816, -21620864, -15521844, -14847996, -7592937, -25892142, -12635595, -9917575, 6216608 }, { -32615849, 338663, -25195611, 2510422, -29213566, -13820213, 24822830, -6146567, -26767480, 7525079 }, }, { { -23066649, -13985623, 16133487, -7896178, -3389565, 778788, -910336, -2782495, -19386633, 11994101 }, { 21691500, -13624626, -641331, -14367021, 3285881, -3483596, -25064666, 9718258, -7477437, 13381418 }, { 18445390, -4202236, 14979846, 11622458, -1727110, -3582980, 23111648, -6375247, 28535282, 15779576 }, }, { { 30098053, 3089662, -9234387, 16662135, -21306940, 11308411, -14068454, 12021730, 9955285, -16303356 }, { 9734894, -14576830, -7473633, -9138735, 2060392, 11313496, -18426029, 9924399, 20194861, 13380996 }, { -26378102, -7965207, -22167821, 15789297, -18055342, -6168792, -1984914, 15707771, 26342023, 10146099 }, }, }, { { { -26016874, -219943, 21339191, -41388, 19745256, -2878700, -29637280, 2227040, 21612326, -545728 }, { -13077387, 1184228, 23562814, -5970442, -20351244, -6348714, 25764461, 12243797, -20856566, 11649658 }, { -10031494, 11262626, 27384172, 2271902, 26947504, -15997771, 39944, 6114064, 33514190, 2333242 }, }, { { -21433588, -12421821, 8119782, 7219913, -21830522, -9016134, -6679750, -12670638, 24350578, -13450001 }, { -4116307, -11271533, -23886186, 4843615, -30088339, 690623, -31536088, -10406836, 8317860, 12352766 }, { 18200138, -14475911, -33087759, -2696619, -23702521, -9102511, -23552096, -2287550, 20712163, 6719373 }, }, { { 26656208, 6075253, -7858556, 1886072, -28344043, 4262326, 11117530, -3763210, 26224235, -3297458 }, { -17168938, -14854097, -3395676, -16369877, -19954045, 14050420, 21728352, 9493610, 18620611, -16428628 }, { -13323321, 13325349, 11432106, 5964811, 18609221, 6062965, -5269471, -9725556, -30701573, -16479657 }, }, { { -23860538, -11233159, 26961357, 1640861, -32413112, -16737940, 12248509, -5240639, 13735342, 1934062 }, { 25089769, 6742589, 17081145, -13406266, 21909293, -16067981, -15136294, -3765346, -21277997, 5473616 }, { 31883677, -7961101, 1083432, -11572403, 22828471, 13290673, -7125085, 12469656, 29111212, -5451014 }, }, { { 24244947, -15050407, -26262976, 2791540, -14997599, 16666678, 24367466, 6388839, -10295587, 452383 }, { -25640782, -3417841, 5217916, 16224624, 19987036, -4082269, -24236251, -5915248, 15766062, 8407814 }, { -20406999, 13990231, 15495425, 16395525, 5377168, 15166495, -8917023, -4388953, -8067909, 2276718 }, }, { { 30157918, 12924066, -17712050, 9245753, 19895028, 3368142, -23827587, 5096219, 22740376, -7303417 }, { 2041139, -14256350, 7783687, 13876377, -25946985, -13352459, 24051124, 13742383, -15637599, 13295222 }, { 33338237, -8505733, 12532113, 7977527, 9106186, -1715251, -17720195, -4612972, -4451357, -14669444 }, }, { { -20045281, 5454097, -14346548, 6447146, 28862071, 1883651, -2469266, -4141880, 7770569, 9620597 }, { 23208068, 7979712, 33071466, 8149229, 1758231, -10834995, 30945528, -1694323, -33502340, -14767970 }, { 1439958, -16270480, -1079989, -793782, 4625402, 10647766, -5043801, 1220118, 30494170, -11440799 }, }, { { -5037580, -13028295, -2970559, -3061767, 15640974, -6701666, -26739026, 926050, -1684339, -13333647 }, { 13908495, -3549272, 30919928, -6273825, -21521863, 7989039, 9021034, 9078865, 3353509, 4033511 }, { -29663431, -15113610, 32259991, -344482, 24295849, -12912123, 23161163, 8839127, 27485041, 7356032 }, }, }, { { { 9661027, 705443, 11980065, -5370154, -1628543, 14661173, -6346142, 2625015, 28431036, -16771834 }, { -23839233, -8311415, -25945511, 7480958, -17681669, -8354183, -22545972, 14150565, 15970762, 4099461 }, { 29262576, 16756590, 26350592, -8793563, 8529671, -11208050, 13617293, -9937143, 11465739, 8317062 }, }, { { -25493081, -6962928, 32500200, -9419051, -23038724, -2302222, 14898637, 3848455, 20969334, -5157516 }, { -20384450, -14347713, -18336405, 13884722, -33039454, 2842114, -21610826, -3649888, 11177095, 14989547 }, { -24496721, -11716016, 16959896, 2278463, 12066309, 10137771, 13515641, 2581286, -28487508, 9930240 }, }, { { -17751622, -2097826, 16544300, -13009300, -15914807, -14949081, 18345767, -13403753, 16291481, -5314038 }, { -33229194, 2553288, 32678213, 9875984, 8534129, 6889387, -9676774, 6957617, 4368891, 9788741 }, { 16660756, 7281060, -10830758, 12911820, 20108584, -8101676, -21722536, -8613148, 16250552, -11111103 }, }, { { -19765507, 2390526, -16551031, 14161980, 1905286, 6414907, 4689584, 10604807, -30190403, 4782747 }, { -1354539, 14736941, -7367442, -13292886, 7710542, -14155590, -9981571, 4383045, 22546403, 437323 }, { 31665577, -12180464, -16186830, 1491339, -18368625, 3294682, 27343084, 2786261, -30633590, -14097016 }, }, { { -14467279, -683715, -33374107, 7448552, 19294360, 14334329, -19690631, 2355319, -19284671, -6114373 }, { 15121312, -15796162, 6377020, -6031361, -10798111, -12957845, 18952177, 15496498, -29380133, 11754228 }, { -2637277, -13483075, 8488727, -14303896, 12728761, -1622493, 7141596, 11724556, 22761615, -10134141 }, }, { { 16918416, 11729663, -18083579, 3022987, -31015732, -13339659, -28741185, -12227393, 32851222, 11717399 }, { 11166634, 7338049, -6722523, 4531520, -29468672, -7302055, 31474879, 3483633, -1193175, -4030831 }, { -185635, 9921305, 31456609, -13536438, -12013818, 13348923, 33142652, 6546660, -19985279, -3948376 }, }, { { -32460596, 11266712, -11197107, -7899103, 31703694, 3855903, -8537131, -12833048, -30772034, -15486313 }, { -18006477, 12709068, 3991746, -6479188, -21491523, -10550425, -31135347, -16049879, 10928917, 3011958 }, { -6957757, -15594337, 31696059, 334240, 29576716, 14796075, -30831056, -12805180, 18008031, 10258577 }, }, { { -22448644, 15655569, 7018479, -4410003, -30314266, -1201591, -1853465, 1367120, 25127874, 6671743 }, { 29701166, -14373934, -10878120, 9279288, -17568, 13127210, 21382910, 11042292, 25838796, 4642684 }, { -20430234, 14955537, -24126347, 8124619, -5369288, -5990470, 30468147, -13900640, 18423289, 4177476 }, }, }, }; ================================================ FILE: Vendor/ed25519-sparkle/src/sc.c ================================================ #include "fixedint.h" #include "sc.h" static uint64_t load_3(const unsigned char *in) { uint64_t result; result = (uint64_t) in[0]; result |= ((uint64_t) in[1]) << 8; result |= ((uint64_t) in[2]) << 16; return result; } static uint64_t load_4(const unsigned char *in) { uint64_t result; result = (uint64_t) in[0]; result |= ((uint64_t) in[1]) << 8; result |= ((uint64_t) in[2]) << 16; result |= ((uint64_t) in[3]) << 24; return result; } /* Input: s[0]+256*s[1]+...+256^63*s[63] = s Output: s[0]+256*s[1]+...+256^31*s[31] = s mod l where l = 2^252 + 27742317777372353535851937790883648493. Overwrites s in place. */ void sc_reduce(unsigned char *s) { int64_t s0 = 2097151 & load_3(s); int64_t s1 = 2097151 & (load_4(s + 2) >> 5); int64_t s2 = 2097151 & (load_3(s + 5) >> 2); int64_t s3 = 2097151 & (load_4(s + 7) >> 7); int64_t s4 = 2097151 & (load_4(s + 10) >> 4); int64_t s5 = 2097151 & (load_3(s + 13) >> 1); int64_t s6 = 2097151 & (load_4(s + 15) >> 6); int64_t s7 = 2097151 & (load_3(s + 18) >> 3); int64_t s8 = 2097151 & load_3(s + 21); int64_t s9 = 2097151 & (load_4(s + 23) >> 5); int64_t s10 = 2097151 & (load_3(s + 26) >> 2); int64_t s11 = 2097151 & (load_4(s + 28) >> 7); int64_t s12 = 2097151 & (load_4(s + 31) >> 4); int64_t s13 = 2097151 & (load_3(s + 34) >> 1); int64_t s14 = 2097151 & (load_4(s + 36) >> 6); int64_t s15 = 2097151 & (load_3(s + 39) >> 3); int64_t s16 = 2097151 & load_3(s + 42); int64_t s17 = 2097151 & (load_4(s + 44) >> 5); int64_t s18 = 2097151 & (load_3(s + 47) >> 2); int64_t s19 = 2097151 & (load_4(s + 49) >> 7); int64_t s20 = 2097151 & (load_4(s + 52) >> 4); int64_t s21 = 2097151 & (load_3(s + 55) >> 1); int64_t s22 = 2097151 & (load_4(s + 57) >> 6); int64_t s23 = (load_4(s + 60) >> 3); int64_t carry0; int64_t carry1; int64_t carry2; int64_t carry3; int64_t carry4; int64_t carry5; int64_t carry6; int64_t carry7; int64_t carry8; int64_t carry9; int64_t carry10; int64_t carry11; int64_t carry12; int64_t carry13; int64_t carry14; int64_t carry15; int64_t carry16; s11 += s23 * 666643; s12 += s23 * 470296; s13 += s23 * 654183; s14 -= s23 * 997805; s15 += s23 * 136657; s16 -= s23 * 683901; s23 = 0; s10 += s22 * 666643; s11 += s22 * 470296; s12 += s22 * 654183; s13 -= s22 * 997805; s14 += s22 * 136657; s15 -= s22 * 683901; s22 = 0; s9 += s21 * 666643; s10 += s21 * 470296; s11 += s21 * 654183; s12 -= s21 * 997805; s13 += s21 * 136657; s14 -= s21 * 683901; s21 = 0; s8 += s20 * 666643; s9 += s20 * 470296; s10 += s20 * 654183; s11 -= s20 * 997805; s12 += s20 * 136657; s13 -= s20 * 683901; s20 = 0; s7 += s19 * 666643; s8 += s19 * 470296; s9 += s19 * 654183; s10 -= s19 * 997805; s11 += s19 * 136657; s12 -= s19 * 683901; s19 = 0; s6 += s18 * 666643; s7 += s18 * 470296; s8 += s18 * 654183; s9 -= s18 * 997805; s10 += s18 * 136657; s11 -= s18 * 683901; s18 = 0; carry6 = (s6 + (1 << 20)) >> 21; s7 += carry6; s6 -= carry6 << 21; carry8 = (s8 + (1 << 20)) >> 21; s9 += carry8; s8 -= carry8 << 21; carry10 = (s10 + (1 << 20)) >> 21; s11 += carry10; s10 -= carry10 << 21; carry12 = (s12 + (1 << 20)) >> 21; s13 += carry12; s12 -= carry12 << 21; carry14 = (s14 + (1 << 20)) >> 21; s15 += carry14; s14 -= carry14 << 21; carry16 = (s16 + (1 << 20)) >> 21; s17 += carry16; s16 -= carry16 << 21; carry7 = (s7 + (1 << 20)) >> 21; s8 += carry7; s7 -= carry7 << 21; carry9 = (s9 + (1 << 20)) >> 21; s10 += carry9; s9 -= carry9 << 21; carry11 = (s11 + (1 << 20)) >> 21; s12 += carry11; s11 -= carry11 << 21; carry13 = (s13 + (1 << 20)) >> 21; s14 += carry13; s13 -= carry13 << 21; carry15 = (s15 + (1 << 20)) >> 21; s16 += carry15; s15 -= carry15 << 21; s5 += s17 * 666643; s6 += s17 * 470296; s7 += s17 * 654183; s8 -= s17 * 997805; s9 += s17 * 136657; s10 -= s17 * 683901; s17 = 0; s4 += s16 * 666643; s5 += s16 * 470296; s6 += s16 * 654183; s7 -= s16 * 997805; s8 += s16 * 136657; s9 -= s16 * 683901; s16 = 0; s3 += s15 * 666643; s4 += s15 * 470296; s5 += s15 * 654183; s6 -= s15 * 997805; s7 += s15 * 136657; s8 -= s15 * 683901; s15 = 0; s2 += s14 * 666643; s3 += s14 * 470296; s4 += s14 * 654183; s5 -= s14 * 997805; s6 += s14 * 136657; s7 -= s14 * 683901; s14 = 0; s1 += s13 * 666643; s2 += s13 * 470296; s3 += s13 * 654183; s4 -= s13 * 997805; s5 += s13 * 136657; s6 -= s13 * 683901; s13 = 0; s0 += s12 * 666643; s1 += s12 * 470296; s2 += s12 * 654183; s3 -= s12 * 997805; s4 += s12 * 136657; s5 -= s12 * 683901; s12 = 0; carry0 = (s0 + (1 << 20)) >> 21; s1 += carry0; s0 -= carry0 << 21; carry2 = (s2 + (1 << 20)) >> 21; s3 += carry2; s2 -= carry2 << 21; carry4 = (s4 + (1 << 20)) >> 21; s5 += carry4; s4 -= carry4 << 21; carry6 = (s6 + (1 << 20)) >> 21; s7 += carry6; s6 -= carry6 << 21; carry8 = (s8 + (1 << 20)) >> 21; s9 += carry8; s8 -= carry8 << 21; carry10 = (s10 + (1 << 20)) >> 21; s11 += carry10; s10 -= carry10 << 21; carry1 = (s1 + (1 << 20)) >> 21; s2 += carry1; s1 -= carry1 << 21; carry3 = (s3 + (1 << 20)) >> 21; s4 += carry3; s3 -= carry3 << 21; carry5 = (s5 + (1 << 20)) >> 21; s6 += carry5; s5 -= carry5 << 21; carry7 = (s7 + (1 << 20)) >> 21; s8 += carry7; s7 -= carry7 << 21; carry9 = (s9 + (1 << 20)) >> 21; s10 += carry9; s9 -= carry9 << 21; carry11 = (s11 + (1 << 20)) >> 21; s12 += carry11; s11 -= carry11 << 21; s0 += s12 * 666643; s1 += s12 * 470296; s2 += s12 * 654183; s3 -= s12 * 997805; s4 += s12 * 136657; s5 -= s12 * 683901; s12 = 0; carry0 = s0 >> 21; s1 += carry0; s0 -= carry0 << 21; carry1 = s1 >> 21; s2 += carry1; s1 -= carry1 << 21; carry2 = s2 >> 21; s3 += carry2; s2 -= carry2 << 21; carry3 = s3 >> 21; s4 += carry3; s3 -= carry3 << 21; carry4 = s4 >> 21; s5 += carry4; s4 -= carry4 << 21; carry5 = s5 >> 21; s6 += carry5; s5 -= carry5 << 21; carry6 = s6 >> 21; s7 += carry6; s6 -= carry6 << 21; carry7 = s7 >> 21; s8 += carry7; s7 -= carry7 << 21; carry8 = s8 >> 21; s9 += carry8; s8 -= carry8 << 21; carry9 = s9 >> 21; s10 += carry9; s9 -= carry9 << 21; carry10 = s10 >> 21; s11 += carry10; s10 -= carry10 << 21; carry11 = s11 >> 21; s12 += carry11; s11 -= carry11 << 21; s0 += s12 * 666643; s1 += s12 * 470296; s2 += s12 * 654183; s3 -= s12 * 997805; s4 += s12 * 136657; s5 -= s12 * 683901; s12 = 0; carry0 = s0 >> 21; s1 += carry0; s0 -= carry0 << 21; carry1 = s1 >> 21; s2 += carry1; s1 -= carry1 << 21; carry2 = s2 >> 21; s3 += carry2; s2 -= carry2 << 21; carry3 = s3 >> 21; s4 += carry3; s3 -= carry3 << 21; carry4 = s4 >> 21; s5 += carry4; s4 -= carry4 << 21; carry5 = s5 >> 21; s6 += carry5; s5 -= carry5 << 21; carry6 = s6 >> 21; s7 += carry6; s6 -= carry6 << 21; carry7 = s7 >> 21; s8 += carry7; s7 -= carry7 << 21; carry8 = s8 >> 21; s9 += carry8; s8 -= carry8 << 21; carry9 = s9 >> 21; s10 += carry9; s9 -= carry9 << 21; carry10 = s10 >> 21; s11 += carry10; s10 -= carry10 << 21; s[0] = (unsigned char) (s0 >> 0); s[1] = (unsigned char) (s0 >> 8); s[2] = (unsigned char) ((s0 >> 16) | (s1 << 5)); s[3] = (unsigned char) (s1 >> 3); s[4] = (unsigned char) (s1 >> 11); s[5] = (unsigned char) ((s1 >> 19) | (s2 << 2)); s[6] = (unsigned char) (s2 >> 6); s[7] = (unsigned char) ((s2 >> 14) | (s3 << 7)); s[8] = (unsigned char) (s3 >> 1); s[9] = (unsigned char) (s3 >> 9); s[10] = (unsigned char) ((s3 >> 17) | (s4 << 4)); s[11] = (unsigned char) (s4 >> 4); s[12] = (unsigned char) (s4 >> 12); s[13] = (unsigned char) ((s4 >> 20) | (s5 << 1)); s[14] = (unsigned char) (s5 >> 7); s[15] = (unsigned char) ((s5 >> 15) | (s6 << 6)); s[16] = (unsigned char) (s6 >> 2); s[17] = (unsigned char) (s6 >> 10); s[18] = (unsigned char) ((s6 >> 18) | (s7 << 3)); s[19] = (unsigned char) (s7 >> 5); s[20] = (unsigned char) (s7 >> 13); s[21] = (unsigned char) (s8 >> 0); s[22] = (unsigned char) (s8 >> 8); s[23] = (unsigned char) ((s8 >> 16) | (s9 << 5)); s[24] = (unsigned char) (s9 >> 3); s[25] = (unsigned char) (s9 >> 11); s[26] = (unsigned char) ((s9 >> 19) | (s10 << 2)); s[27] = (unsigned char) (s10 >> 6); s[28] = (unsigned char) ((s10 >> 14) | (s11 << 7)); s[29] = (unsigned char) (s11 >> 1); s[30] = (unsigned char) (s11 >> 9); s[31] = (unsigned char) (s11 >> 17); } /* Input: a[0]+256*a[1]+...+256^31*a[31] = a b[0]+256*b[1]+...+256^31*b[31] = b c[0]+256*c[1]+...+256^31*c[31] = c Output: s[0]+256*s[1]+...+256^31*s[31] = (ab+c) mod l where l = 2^252 + 27742317777372353535851937790883648493. */ void sc_muladd(unsigned char *s, const unsigned char *a, const unsigned char *b, const unsigned char *c) { int64_t a0 = 2097151 & load_3(a); int64_t a1 = 2097151 & (load_4(a + 2) >> 5); int64_t a2 = 2097151 & (load_3(a + 5) >> 2); int64_t a3 = 2097151 & (load_4(a + 7) >> 7); int64_t a4 = 2097151 & (load_4(a + 10) >> 4); int64_t a5 = 2097151 & (load_3(a + 13) >> 1); int64_t a6 = 2097151 & (load_4(a + 15) >> 6); int64_t a7 = 2097151 & (load_3(a + 18) >> 3); int64_t a8 = 2097151 & load_3(a + 21); int64_t a9 = 2097151 & (load_4(a + 23) >> 5); int64_t a10 = 2097151 & (load_3(a + 26) >> 2); int64_t a11 = (load_4(a + 28) >> 7); int64_t b0 = 2097151 & load_3(b); int64_t b1 = 2097151 & (load_4(b + 2) >> 5); int64_t b2 = 2097151 & (load_3(b + 5) >> 2); int64_t b3 = 2097151 & (load_4(b + 7) >> 7); int64_t b4 = 2097151 & (load_4(b + 10) >> 4); int64_t b5 = 2097151 & (load_3(b + 13) >> 1); int64_t b6 = 2097151 & (load_4(b + 15) >> 6); int64_t b7 = 2097151 & (load_3(b + 18) >> 3); int64_t b8 = 2097151 & load_3(b + 21); int64_t b9 = 2097151 & (load_4(b + 23) >> 5); int64_t b10 = 2097151 & (load_3(b + 26) >> 2); int64_t b11 = (load_4(b + 28) >> 7); int64_t c0 = 2097151 & load_3(c); int64_t c1 = 2097151 & (load_4(c + 2) >> 5); int64_t c2 = 2097151 & (load_3(c + 5) >> 2); int64_t c3 = 2097151 & (load_4(c + 7) >> 7); int64_t c4 = 2097151 & (load_4(c + 10) >> 4); int64_t c5 = 2097151 & (load_3(c + 13) >> 1); int64_t c6 = 2097151 & (load_4(c + 15) >> 6); int64_t c7 = 2097151 & (load_3(c + 18) >> 3); int64_t c8 = 2097151 & load_3(c + 21); int64_t c9 = 2097151 & (load_4(c + 23) >> 5); int64_t c10 = 2097151 & (load_3(c + 26) >> 2); int64_t c11 = (load_4(c + 28) >> 7); int64_t s0; int64_t s1; int64_t s2; int64_t s3; int64_t s4; int64_t s5; int64_t s6; int64_t s7; int64_t s8; int64_t s9; int64_t s10; int64_t s11; int64_t s12; int64_t s13; int64_t s14; int64_t s15; int64_t s16; int64_t s17; int64_t s18; int64_t s19; int64_t s20; int64_t s21; int64_t s22; int64_t s23; int64_t carry0; int64_t carry1; int64_t carry2; int64_t carry3; int64_t carry4; int64_t carry5; int64_t carry6; int64_t carry7; int64_t carry8; int64_t carry9; int64_t carry10; int64_t carry11; int64_t carry12; int64_t carry13; int64_t carry14; int64_t carry15; int64_t carry16; int64_t carry17; int64_t carry18; int64_t carry19; int64_t carry20; int64_t carry21; int64_t carry22; s0 = c0 + a0 * b0; s1 = c1 + a0 * b1 + a1 * b0; s2 = c2 + a0 * b2 + a1 * b1 + a2 * b0; s3 = c3 + a0 * b3 + a1 * b2 + a2 * b1 + a3 * b0; s4 = c4 + a0 * b4 + a1 * b3 + a2 * b2 + a3 * b1 + a4 * b0; s5 = c5 + a0 * b5 + a1 * b4 + a2 * b3 + a3 * b2 + a4 * b1 + a5 * b0; s6 = c6 + a0 * b6 + a1 * b5 + a2 * b4 + a3 * b3 + a4 * b2 + a5 * b1 + a6 * b0; s7 = c7 + a0 * b7 + a1 * b6 + a2 * b5 + a3 * b4 + a4 * b3 + a5 * b2 + a6 * b1 + a7 * b0; s8 = c8 + a0 * b8 + a1 * b7 + a2 * b6 + a3 * b5 + a4 * b4 + a5 * b3 + a6 * b2 + a7 * b1 + a8 * b0; s9 = c9 + a0 * b9 + a1 * b8 + a2 * b7 + a3 * b6 + a4 * b5 + a5 * b4 + a6 * b3 + a7 * b2 + a8 * b1 + a9 * b0; s10 = c10 + a0 * b10 + a1 * b9 + a2 * b8 + a3 * b7 + a4 * b6 + a5 * b5 + a6 * b4 + a7 * b3 + a8 * b2 + a9 * b1 + a10 * b0; s11 = c11 + a0 * b11 + a1 * b10 + a2 * b9 + a3 * b8 + a4 * b7 + a5 * b6 + a6 * b5 + a7 * b4 + a8 * b3 + a9 * b2 + a10 * b1 + a11 * b0; s12 = a1 * b11 + a2 * b10 + a3 * b9 + a4 * b8 + a5 * b7 + a6 * b6 + a7 * b5 + a8 * b4 + a9 * b3 + a10 * b2 + a11 * b1; s13 = a2 * b11 + a3 * b10 + a4 * b9 + a5 * b8 + a6 * b7 + a7 * b6 + a8 * b5 + a9 * b4 + a10 * b3 + a11 * b2; s14 = a3 * b11 + a4 * b10 + a5 * b9 + a6 * b8 + a7 * b7 + a8 * b6 + a9 * b5 + a10 * b4 + a11 * b3; s15 = a4 * b11 + a5 * b10 + a6 * b9 + a7 * b8 + a8 * b7 + a9 * b6 + a10 * b5 + a11 * b4; s16 = a5 * b11 + a6 * b10 + a7 * b9 + a8 * b8 + a9 * b7 + a10 * b6 + a11 * b5; s17 = a6 * b11 + a7 * b10 + a8 * b9 + a9 * b8 + a10 * b7 + a11 * b6; s18 = a7 * b11 + a8 * b10 + a9 * b9 + a10 * b8 + a11 * b7; s19 = a8 * b11 + a9 * b10 + a10 * b9 + a11 * b8; s20 = a9 * b11 + a10 * b10 + a11 * b9; s21 = a10 * b11 + a11 * b10; s22 = a11 * b11; s23 = 0; carry0 = (s0 + (1 << 20)) >> 21; s1 += carry0; s0 -= carry0 << 21; carry2 = (s2 + (1 << 20)) >> 21; s3 += carry2; s2 -= carry2 << 21; carry4 = (s4 + (1 << 20)) >> 21; s5 += carry4; s4 -= carry4 << 21; carry6 = (s6 + (1 << 20)) >> 21; s7 += carry6; s6 -= carry6 << 21; carry8 = (s8 + (1 << 20)) >> 21; s9 += carry8; s8 -= carry8 << 21; carry10 = (s10 + (1 << 20)) >> 21; s11 += carry10; s10 -= carry10 << 21; carry12 = (s12 + (1 << 20)) >> 21; s13 += carry12; s12 -= carry12 << 21; carry14 = (s14 + (1 << 20)) >> 21; s15 += carry14; s14 -= carry14 << 21; carry16 = (s16 + (1 << 20)) >> 21; s17 += carry16; s16 -= carry16 << 21; carry18 = (s18 + (1 << 20)) >> 21; s19 += carry18; s18 -= carry18 << 21; carry20 = (s20 + (1 << 20)) >> 21; s21 += carry20; s20 -= carry20 << 21; carry22 = (s22 + (1 << 20)) >> 21; s23 += carry22; s22 -= carry22 << 21; carry1 = (s1 + (1 << 20)) >> 21; s2 += carry1; s1 -= carry1 << 21; carry3 = (s3 + (1 << 20)) >> 21; s4 += carry3; s3 -= carry3 << 21; carry5 = (s5 + (1 << 20)) >> 21; s6 += carry5; s5 -= carry5 << 21; carry7 = (s7 + (1 << 20)) >> 21; s8 += carry7; s7 -= carry7 << 21; carry9 = (s9 + (1 << 20)) >> 21; s10 += carry9; s9 -= carry9 << 21; carry11 = (s11 + (1 << 20)) >> 21; s12 += carry11; s11 -= carry11 << 21; carry13 = (s13 + (1 << 20)) >> 21; s14 += carry13; s13 -= carry13 << 21; carry15 = (s15 + (1 << 20)) >> 21; s16 += carry15; s15 -= carry15 << 21; carry17 = (s17 + (1 << 20)) >> 21; s18 += carry17; s17 -= carry17 << 21; carry19 = (s19 + (1 << 20)) >> 21; s20 += carry19; s19 -= carry19 << 21; carry21 = (s21 + (1 << 20)) >> 21; s22 += carry21; s21 -= carry21 << 21; s11 += s23 * 666643; s12 += s23 * 470296; s13 += s23 * 654183; s14 -= s23 * 997805; s15 += s23 * 136657; s16 -= s23 * 683901; s23 = 0; s10 += s22 * 666643; s11 += s22 * 470296; s12 += s22 * 654183; s13 -= s22 * 997805; s14 += s22 * 136657; s15 -= s22 * 683901; s22 = 0; s9 += s21 * 666643; s10 += s21 * 470296; s11 += s21 * 654183; s12 -= s21 * 997805; s13 += s21 * 136657; s14 -= s21 * 683901; s21 = 0; s8 += s20 * 666643; s9 += s20 * 470296; s10 += s20 * 654183; s11 -= s20 * 997805; s12 += s20 * 136657; s13 -= s20 * 683901; s20 = 0; s7 += s19 * 666643; s8 += s19 * 470296; s9 += s19 * 654183; s10 -= s19 * 997805; s11 += s19 * 136657; s12 -= s19 * 683901; s19 = 0; s6 += s18 * 666643; s7 += s18 * 470296; s8 += s18 * 654183; s9 -= s18 * 997805; s10 += s18 * 136657; s11 -= s18 * 683901; s18 = 0; carry6 = (s6 + (1 << 20)) >> 21; s7 += carry6; s6 -= carry6 << 21; carry8 = (s8 + (1 << 20)) >> 21; s9 += carry8; s8 -= carry8 << 21; carry10 = (s10 + (1 << 20)) >> 21; s11 += carry10; s10 -= carry10 << 21; carry12 = (s12 + (1 << 20)) >> 21; s13 += carry12; s12 -= carry12 << 21; carry14 = (s14 + (1 << 20)) >> 21; s15 += carry14; s14 -= carry14 << 21; carry16 = (s16 + (1 << 20)) >> 21; s17 += carry16; s16 -= carry16 << 21; carry7 = (s7 + (1 << 20)) >> 21; s8 += carry7; s7 -= carry7 << 21; carry9 = (s9 + (1 << 20)) >> 21; s10 += carry9; s9 -= carry9 << 21; carry11 = (s11 + (1 << 20)) >> 21; s12 += carry11; s11 -= carry11 << 21; carry13 = (s13 + (1 << 20)) >> 21; s14 += carry13; s13 -= carry13 << 21; carry15 = (s15 + (1 << 20)) >> 21; s16 += carry15; s15 -= carry15 << 21; s5 += s17 * 666643; s6 += s17 * 470296; s7 += s17 * 654183; s8 -= s17 * 997805; s9 += s17 * 136657; s10 -= s17 * 683901; s17 = 0; s4 += s16 * 666643; s5 += s16 * 470296; s6 += s16 * 654183; s7 -= s16 * 997805; s8 += s16 * 136657; s9 -= s16 * 683901; s16 = 0; s3 += s15 * 666643; s4 += s15 * 470296; s5 += s15 * 654183; s6 -= s15 * 997805; s7 += s15 * 136657; s8 -= s15 * 683901; s15 = 0; s2 += s14 * 666643; s3 += s14 * 470296; s4 += s14 * 654183; s5 -= s14 * 997805; s6 += s14 * 136657; s7 -= s14 * 683901; s14 = 0; s1 += s13 * 666643; s2 += s13 * 470296; s3 += s13 * 654183; s4 -= s13 * 997805; s5 += s13 * 136657; s6 -= s13 * 683901; s13 = 0; s0 += s12 * 666643; s1 += s12 * 470296; s2 += s12 * 654183; s3 -= s12 * 997805; s4 += s12 * 136657; s5 -= s12 * 683901; s12 = 0; carry0 = (s0 + (1 << 20)) >> 21; s1 += carry0; s0 -= carry0 << 21; carry2 = (s2 + (1 << 20)) >> 21; s3 += carry2; s2 -= carry2 << 21; carry4 = (s4 + (1 << 20)) >> 21; s5 += carry4; s4 -= carry4 << 21; carry6 = (s6 + (1 << 20)) >> 21; s7 += carry6; s6 -= carry6 << 21; carry8 = (s8 + (1 << 20)) >> 21; s9 += carry8; s8 -= carry8 << 21; carry10 = (s10 + (1 << 20)) >> 21; s11 += carry10; s10 -= carry10 << 21; carry1 = (s1 + (1 << 20)) >> 21; s2 += carry1; s1 -= carry1 << 21; carry3 = (s3 + (1 << 20)) >> 21; s4 += carry3; s3 -= carry3 << 21; carry5 = (s5 + (1 << 20)) >> 21; s6 += carry5; s5 -= carry5 << 21; carry7 = (s7 + (1 << 20)) >> 21; s8 += carry7; s7 -= carry7 << 21; carry9 = (s9 + (1 << 20)) >> 21; s10 += carry9; s9 -= carry9 << 21; carry11 = (s11 + (1 << 20)) >> 21; s12 += carry11; s11 -= carry11 << 21; s0 += s12 * 666643; s1 += s12 * 470296; s2 += s12 * 654183; s3 -= s12 * 997805; s4 += s12 * 136657; s5 -= s12 * 683901; s12 = 0; carry0 = s0 >> 21; s1 += carry0; s0 -= carry0 << 21; carry1 = s1 >> 21; s2 += carry1; s1 -= carry1 << 21; carry2 = s2 >> 21; s3 += carry2; s2 -= carry2 << 21; carry3 = s3 >> 21; s4 += carry3; s3 -= carry3 << 21; carry4 = s4 >> 21; s5 += carry4; s4 -= carry4 << 21; carry5 = s5 >> 21; s6 += carry5; s5 -= carry5 << 21; carry6 = s6 >> 21; s7 += carry6; s6 -= carry6 << 21; carry7 = s7 >> 21; s8 += carry7; s7 -= carry7 << 21; carry8 = s8 >> 21; s9 += carry8; s8 -= carry8 << 21; carry9 = s9 >> 21; s10 += carry9; s9 -= carry9 << 21; carry10 = s10 >> 21; s11 += carry10; s10 -= carry10 << 21; carry11 = s11 >> 21; s12 += carry11; s11 -= carry11 << 21; s0 += s12 * 666643; s1 += s12 * 470296; s2 += s12 * 654183; s3 -= s12 * 997805; s4 += s12 * 136657; s5 -= s12 * 683901; s12 = 0; carry0 = s0 >> 21; s1 += carry0; s0 -= carry0 << 21; carry1 = s1 >> 21; s2 += carry1; s1 -= carry1 << 21; carry2 = s2 >> 21; s3 += carry2; s2 -= carry2 << 21; carry3 = s3 >> 21; s4 += carry3; s3 -= carry3 << 21; carry4 = s4 >> 21; s5 += carry4; s4 -= carry4 << 21; carry5 = s5 >> 21; s6 += carry5; s5 -= carry5 << 21; carry6 = s6 >> 21; s7 += carry6; s6 -= carry6 << 21; carry7 = s7 >> 21; s8 += carry7; s7 -= carry7 << 21; carry8 = s8 >> 21; s9 += carry8; s8 -= carry8 << 21; carry9 = s9 >> 21; s10 += carry9; s9 -= carry9 << 21; carry10 = s10 >> 21; s11 += carry10; s10 -= carry10 << 21; s[0] = (unsigned char) (s0 >> 0); s[1] = (unsigned char) (s0 >> 8); s[2] = (unsigned char) ((s0 >> 16) | (s1 << 5)); s[3] = (unsigned char) (s1 >> 3); s[4] = (unsigned char) (s1 >> 11); s[5] = (unsigned char) ((s1 >> 19) | (s2 << 2)); s[6] = (unsigned char) (s2 >> 6); s[7] = (unsigned char) ((s2 >> 14) | (s3 << 7)); s[8] = (unsigned char) (s3 >> 1); s[9] = (unsigned char) (s3 >> 9); s[10] = (unsigned char) ((s3 >> 17) | (s4 << 4)); s[11] = (unsigned char) (s4 >> 4); s[12] = (unsigned char) (s4 >> 12); s[13] = (unsigned char) ((s4 >> 20) | (s5 << 1)); s[14] = (unsigned char) (s5 >> 7); s[15] = (unsigned char) ((s5 >> 15) | (s6 << 6)); s[16] = (unsigned char) (s6 >> 2); s[17] = (unsigned char) (s6 >> 10); s[18] = (unsigned char) ((s6 >> 18) | (s7 << 3)); s[19] = (unsigned char) (s7 >> 5); s[20] = (unsigned char) (s7 >> 13); s[21] = (unsigned char) (s8 >> 0); s[22] = (unsigned char) (s8 >> 8); s[23] = (unsigned char) ((s8 >> 16) | (s9 << 5)); s[24] = (unsigned char) (s9 >> 3); s[25] = (unsigned char) (s9 >> 11); s[26] = (unsigned char) ((s9 >> 19) | (s10 << 2)); s[27] = (unsigned char) (s10 >> 6); s[28] = (unsigned char) ((s10 >> 14) | (s11 << 7)); s[29] = (unsigned char) (s11 >> 1); s[30] = (unsigned char) (s11 >> 9); s[31] = (unsigned char) (s11 >> 17); } ================================================ FILE: Vendor/ed25519-sparkle/src/sc.h ================================================ #ifndef SC_H #define SC_H /* The set of scalars is \Z/l where l = 2^252 + 27742317777372353535851937790883648493. */ void sc_reduce(unsigned char *s); void sc_muladd(unsigned char *s, const unsigned char *a, const unsigned char *b, const unsigned char *c); #endif ================================================ FILE: Vendor/ed25519-sparkle/src/seed.c ================================================ #include "ed25519.h" #ifndef ED25519_NO_SEED #ifdef _WIN32 #include #include #else #include #endif int ed25519_create_seed(unsigned char *seed) { #ifdef _WIN32 HCRYPTPROV prov; if (!CryptAcquireContext(&prov, NULL, NULL, PROV_RSA_FULL, CRYPT_VERIFYCONTEXT)) { return 1; } if (!CryptGenRandom(prov, 32, seed)) { CryptReleaseContext(prov, 0); return 1; } CryptReleaseContext(prov, 0); #else FILE *f = fopen("/dev/urandom", "rb"); if (f == NULL) { return 1; } fread(seed, 1, 32, f); fclose(f); #endif return 0; } #endif ================================================ FILE: Vendor/ed25519-sparkle/src/sha512.c ================================================ /* LibTomCrypt, modular cryptographic library -- Tom St Denis * * LibTomCrypt is a library that provides various cryptographic * algorithms in a highly modular and flexible manner. * * The library is free for all purposes without any express * guarantee it works. * * Tom St Denis, tomstdenis@gmail.com, http://libtom.org */ #include "fixedint.h" #include "sha512.h" /* the K array */ static const uint64_t K[80] = { UINT64_C(0x428a2f98d728ae22), UINT64_C(0x7137449123ef65cd), UINT64_C(0xb5c0fbcfec4d3b2f), UINT64_C(0xe9b5dba58189dbbc), UINT64_C(0x3956c25bf348b538), UINT64_C(0x59f111f1b605d019), UINT64_C(0x923f82a4af194f9b), UINT64_C(0xab1c5ed5da6d8118), UINT64_C(0xd807aa98a3030242), UINT64_C(0x12835b0145706fbe), UINT64_C(0x243185be4ee4b28c), UINT64_C(0x550c7dc3d5ffb4e2), UINT64_C(0x72be5d74f27b896f), UINT64_C(0x80deb1fe3b1696b1), UINT64_C(0x9bdc06a725c71235), UINT64_C(0xc19bf174cf692694), UINT64_C(0xe49b69c19ef14ad2), UINT64_C(0xefbe4786384f25e3), UINT64_C(0x0fc19dc68b8cd5b5), UINT64_C(0x240ca1cc77ac9c65), UINT64_C(0x2de92c6f592b0275), UINT64_C(0x4a7484aa6ea6e483), UINT64_C(0x5cb0a9dcbd41fbd4), UINT64_C(0x76f988da831153b5), UINT64_C(0x983e5152ee66dfab), UINT64_C(0xa831c66d2db43210), UINT64_C(0xb00327c898fb213f), UINT64_C(0xbf597fc7beef0ee4), UINT64_C(0xc6e00bf33da88fc2), UINT64_C(0xd5a79147930aa725), UINT64_C(0x06ca6351e003826f), UINT64_C(0x142929670a0e6e70), UINT64_C(0x27b70a8546d22ffc), UINT64_C(0x2e1b21385c26c926), UINT64_C(0x4d2c6dfc5ac42aed), UINT64_C(0x53380d139d95b3df), UINT64_C(0x650a73548baf63de), UINT64_C(0x766a0abb3c77b2a8), UINT64_C(0x81c2c92e47edaee6), UINT64_C(0x92722c851482353b), UINT64_C(0xa2bfe8a14cf10364), UINT64_C(0xa81a664bbc423001), UINT64_C(0xc24b8b70d0f89791), UINT64_C(0xc76c51a30654be30), UINT64_C(0xd192e819d6ef5218), UINT64_C(0xd69906245565a910), UINT64_C(0xf40e35855771202a), UINT64_C(0x106aa07032bbd1b8), UINT64_C(0x19a4c116b8d2d0c8), UINT64_C(0x1e376c085141ab53), UINT64_C(0x2748774cdf8eeb99), UINT64_C(0x34b0bcb5e19b48a8), UINT64_C(0x391c0cb3c5c95a63), UINT64_C(0x4ed8aa4ae3418acb), UINT64_C(0x5b9cca4f7763e373), UINT64_C(0x682e6ff3d6b2b8a3), UINT64_C(0x748f82ee5defb2fc), UINT64_C(0x78a5636f43172f60), UINT64_C(0x84c87814a1f0ab72), UINT64_C(0x8cc702081a6439ec), UINT64_C(0x90befffa23631e28), UINT64_C(0xa4506cebde82bde9), UINT64_C(0xbef9a3f7b2c67915), UINT64_C(0xc67178f2e372532b), UINT64_C(0xca273eceea26619c), UINT64_C(0xd186b8c721c0c207), UINT64_C(0xeada7dd6cde0eb1e), UINT64_C(0xf57d4f7fee6ed178), UINT64_C(0x06f067aa72176fba), UINT64_C(0x0a637dc5a2c898a6), UINT64_C(0x113f9804bef90dae), UINT64_C(0x1b710b35131c471b), UINT64_C(0x28db77f523047d84), UINT64_C(0x32caab7b40c72493), UINT64_C(0x3c9ebe0a15c9bebc), UINT64_C(0x431d67c49c100d4c), UINT64_C(0x4cc5d4becb3e42b6), UINT64_C(0x597f299cfc657e2a), UINT64_C(0x5fcb6fab3ad6faec), UINT64_C(0x6c44198c4a475817) }; /* Various logical functions */ #define ROR64c(x, y) \ ( ((((x)&UINT64_C(0xFFFFFFFFFFFFFFFF))>>((uint64_t)(y)&UINT64_C(63))) | \ ((x)<<((uint64_t)(64-((y)&UINT64_C(63)))))) & UINT64_C(0xFFFFFFFFFFFFFFFF)) #define STORE64H(x, y) \ { (y)[0] = (unsigned char)(((x)>>56)&255); (y)[1] = (unsigned char)(((x)>>48)&255); \ (y)[2] = (unsigned char)(((x)>>40)&255); (y)[3] = (unsigned char)(((x)>>32)&255); \ (y)[4] = (unsigned char)(((x)>>24)&255); (y)[5] = (unsigned char)(((x)>>16)&255); \ (y)[6] = (unsigned char)(((x)>>8)&255); (y)[7] = (unsigned char)((x)&255); } #define LOAD64H(x, y) \ { x = (((uint64_t)((y)[0] & 255))<<56)|(((uint64_t)((y)[1] & 255))<<48) | \ (((uint64_t)((y)[2] & 255))<<40)|(((uint64_t)((y)[3] & 255))<<32) | \ (((uint64_t)((y)[4] & 255))<<24)|(((uint64_t)((y)[5] & 255))<<16) | \ (((uint64_t)((y)[6] & 255))<<8)|(((uint64_t)((y)[7] & 255))); } #define Ch(x,y,z) (z ^ (x & (y ^ z))) #define Maj(x,y,z) (((x | y) & z) | (x & y)) #define S(x, n) ROR64c(x, n) #define R(x, n) (((x) &UINT64_C(0xFFFFFFFFFFFFFFFF))>>((uint64_t)n)) #define Sigma0(x) (S(x, 28) ^ S(x, 34) ^ S(x, 39)) #define Sigma1(x) (S(x, 14) ^ S(x, 18) ^ S(x, 41)) #define Gamma0(x) (S(x, 1) ^ S(x, 8) ^ R(x, 7)) #define Gamma1(x) (S(x, 19) ^ S(x, 61) ^ R(x, 6)) #ifndef MIN #define MIN(x, y) ( ((x)<(y))?(x):(y) ) #endif /* compress 1024-bits */ static int sha512_compress(sha512_context *md, unsigned char *buf) { uint64_t S[8], W[80], t0, t1; int i; /* copy state into S */ for (i = 0; i < 8; i++) { S[i] = md->state[i]; } /* copy the state into 1024-bits into W[0..15] */ for (i = 0; i < 16; i++) { LOAD64H(W[i], buf + (8*i)) } /* fill W[16..79] */ for (i = 16; i < 80; i++) { W[i] = Gamma1(W[i - 2]) + W[i - 7] + Gamma0(W[i - 15]) + W[i - 16]; } /* Compress */ #define RND(a,b,c,d,e,f,g,h,i) \ t0 = h + Sigma1(e) + Ch(e, f, g) + K[i] + W[i]; \ t1 = Sigma0(a) + Maj(a, b, c);\ d += t0; \ h = t0 + t1; for (i = 0; i < 80; i += 8) { RND(S[0],S[1],S[2],S[3],S[4],S[5],S[6],S[7],i+0) RND(S[7],S[0],S[1],S[2],S[3],S[4],S[5],S[6],i+1) RND(S[6],S[7],S[0],S[1],S[2],S[3],S[4],S[5],i+2) RND(S[5],S[6],S[7],S[0],S[1],S[2],S[3],S[4],i+3) RND(S[4],S[5],S[6],S[7],S[0],S[1],S[2],S[3],i+4) RND(S[3],S[4],S[5],S[6],S[7],S[0],S[1],S[2],i+5) RND(S[2],S[3],S[4],S[5],S[6],S[7],S[0],S[1],i+6) RND(S[1],S[2],S[3],S[4],S[5],S[6],S[7],S[0],i+7) } #undef RND /* feedback */ for (i = 0; i < 8; i++) { md->state[i] = md->state[i] + S[i]; } return 0; } /** Initialize the hash state @param md The hash state you wish to initialize @return 0 if successful */ int sha512_init(sha512_context * md) { if (md == NULL) return 1; md->curlen = 0; md->length = 0; md->state[0] = UINT64_C(0x6a09e667f3bcc908); md->state[1] = UINT64_C(0xbb67ae8584caa73b); md->state[2] = UINT64_C(0x3c6ef372fe94f82b); md->state[3] = UINT64_C(0xa54ff53a5f1d36f1); md->state[4] = UINT64_C(0x510e527fade682d1); md->state[5] = UINT64_C(0x9b05688c2b3e6c1f); md->state[6] = UINT64_C(0x1f83d9abfb41bd6b); md->state[7] = UINT64_C(0x5be0cd19137e2179); return 0; } /** Process a block of memory though the hash @param md The hash state @param in The data to hash @param inlen The length of the data (octets) @return 0 if successful */ int sha512_update (sha512_context * md, const unsigned char *in, size_t inlen) { size_t n; size_t i; int err; if (md == NULL) return 1; if (in == NULL) return 1; if (md->curlen > sizeof(md->buf)) { return 1; } while (inlen > 0) { if (md->curlen == 0 && inlen >= 128) { if ((err = sha512_compress (md, (unsigned char *)in)) != 0) { return err; } md->length += 128 * 8; in += 128; inlen -= 128; } else { n = MIN(inlen, (128 - md->curlen)); for (i = 0; i < n; i++) { md->buf[i + md->curlen] = in[i]; } md->curlen += n; in += n; inlen -= n; if (md->curlen == 128) { if ((err = sha512_compress (md, md->buf)) != 0) { return err; } md->length += 8*128; md->curlen = 0; } } } return 0; } /** Terminate the hash to get the digest @param md The hash state @param out [out] The destination of the hash (64 bytes) @return 0 if successful */ int sha512_final(sha512_context * md, unsigned char *out) { int i; if (md == NULL) return 1; if (out == NULL) return 1; if (md->curlen >= sizeof(md->buf)) { return 1; } /* increase the length of the message */ md->length += md->curlen * UINT64_C(8); /* append the '1' bit */ md->buf[md->curlen++] = (unsigned char)0x80; /* if the length is currently above 112 bytes we append zeros * then compress. Then we can fall back to padding zeros and length * encoding like normal. */ if (md->curlen > 112) { while (md->curlen < 128) { md->buf[md->curlen++] = (unsigned char)0; } sha512_compress(md, md->buf); md->curlen = 0; } /* pad upto 120 bytes of zeroes * note: that from 112 to 120 is the 64 MSB of the length. We assume that you won't hash * > 2^64 bits of data... :-) */ while (md->curlen < 120) { md->buf[md->curlen++] = (unsigned char)0; } /* store length */ STORE64H(md->length, md->buf+120) sha512_compress(md, md->buf); /* copy output */ for (i = 0; i < 8; i++) { STORE64H(md->state[i], out+(8*i)) } return 0; } int sha512(const unsigned char *message, size_t message_len, unsigned char *out) { sha512_context ctx; int ret; if ((ret = sha512_init(&ctx))) return ret; if ((ret = sha512_update(&ctx, message, message_len))) return ret; if ((ret = sha512_final(&ctx, out))) return ret; return 0; } ================================================ FILE: Vendor/ed25519-sparkle/src/sha512.h ================================================ #ifndef SHA512_H #define SHA512_H #include #include "fixedint.h" /* state */ typedef struct sha512_context_ { uint64_t length, state[8]; size_t curlen; unsigned char buf[128]; } sha512_context; int sha512_init(sha512_context * md); int sha512_final(sha512_context * md, unsigned char *out); int sha512_update(sha512_context * md, const unsigned char *in, size_t inlen); int sha512(const unsigned char *message, size_t message_len, unsigned char *out); #endif ================================================ FILE: Vendor/ed25519-sparkle/src/sign.c ================================================ #include "ed25519.h" #include "sha512.h" #include "ge.h" #include "sc.h" void ed25519_sign(unsigned char *signature, const unsigned char *message, size_t message_len, const unsigned char *public_key, const unsigned char *private_key) { sha512_context hash; unsigned char hram[64]; unsigned char r[64]; ge_p3 R; sha512_init(&hash); sha512_update(&hash, private_key + 32, 32); sha512_update(&hash, message, message_len); sha512_final(&hash, r); sc_reduce(r); ge_scalarmult_base(&R, r); ge_p3_tobytes(signature, &R); sha512_init(&hash); sha512_update(&hash, signature, 32); sha512_update(&hash, public_key, 32); sha512_update(&hash, message, message_len); sha512_final(&hash, hram); sc_reduce(hram); sc_muladd(signature + 32, hram, private_key, r); } ================================================ FILE: Vendor/ed25519-sparkle/src/verify.c ================================================ #include "ed25519.h" #include "sha512.h" #include "ge.h" #include "sc.h" static int consttime_equal(const unsigned char *x, const unsigned char *y) { unsigned char r = 0; r = x[0] ^ y[0]; #define F(i) r |= x[i] ^ y[i] F(1); F(2); F(3); F(4); F(5); F(6); F(7); F(8); F(9); F(10); F(11); F(12); F(13); F(14); F(15); F(16); F(17); F(18); F(19); F(20); F(21); F(22); F(23); F(24); F(25); F(26); F(27); F(28); F(29); F(30); F(31); #undef F return !r; } int ed25519_verify(const unsigned char *signature, const unsigned char *message, size_t message_len, const unsigned char *public_key) { unsigned char h[64]; unsigned char checker[32]; sha512_context hash; ge_p3 A; ge_p2 R; if (signature[63] & 224) { return 0; } if (ge_frombytes_negate_vartime(&A, public_key) != 0) { return 0; } sha512_init(&hash); sha512_update(&hash, signature, 32); sha512_update(&hash, public_key, 32); sha512_update(&hash, message, message_len); sha512_final(&hash, h); sc_reduce(h); ge_double_scalarmult_vartime(&R, h, &A, signature + 32); ge_tobytes(checker, &R); if (!consttime_equal(checker, signature)) { return 0; } return 1; } ================================================ FILE: bin/old_dsa_scripts/sign_update ================================================ #!/bin/bash set -e set -o pipefail if [ "$#" -ne 2 ]; then echo "Usage: $0 update_archive_file dsa_priv.pem" echo "This is an old DSA signing script for deprecated DSA keys." echo "Do not use this for new applications." exit 1 fi openssl=/usr/bin/openssl version=`$openssl version` if [[ $version =~ "OpenSSL 0.9" ]]; then # pre-10.13 system: Fall back to OpenSSL DSS1 digest because it does not like the -sha1 option $openssl dgst -sha1 -binary < "$1" | $openssl dgst -dss1 -sign "$2" | $openssl enc -base64 else # 10.13 and later: Use LibreSSL SHA1 digest $openssl dgst -sha1 -binary < "$1" | $openssl dgst -sha1 -sign "$2" | $openssl enc -base64 fi ================================================ FILE: common_cli/Secret.swift ================================================ // // secret.swift // Sparkle // // Created on 11/19/23. // Copyright © 2023 Sparkle Project. All rights reserved. // import Foundation func secretUsesRegularSeed(secret: Data) -> Bool { return (secret.count == 32) } func secretUsesOldHashedSeed(secret: Data) -> Bool { return (secret.count == 64 + 32) } // Secret is the data we store in the keychain // For newer generated keys, secret is the private seed // For older generated keys, secret is the private orlp/Ed25519 key concatenated with the public key // If the secret is of invalid length, this returns nil func decodePrivateAndPublicKeys(secret: Data) -> (privateKey: Data, publicKey: Data)? { let privateKey: Data let publicKey: Data if secretUsesRegularSeed(secret: secret) { let seed = secret var privateEdKey = Array(repeating: 0, count: 64) var publicEdKey = Array(repeating: 0, count: 32) seed.withUnsafeBytes { seedBytes in let seedBuffer: UnsafePointer = seedBytes.baseAddress!.assumingMemoryBound(to: UInt8.self) ed25519_create_keypair(&publicEdKey, &privateEdKey, seedBuffer) } privateKey = Data(privateEdKey) publicKey = Data(publicEdKey) } else if secretUsesOldHashedSeed(secret: secret) { privateKey = secret[0..<64] publicKey = secret[64...] } else { return nil } return (privateKey, publicKey) } enum DecodeSecretStringError : LocalizedError { case unableToReadData case unableToDecodeDataAsUTF8String public var errorDescription: String? { switch self { case .unableToReadData: return "Unable to read EdDSA private key data" case .unableToDecodeDataAsUTF8String: return "Unable to read EdDSA private key data as UTF-8 string" } } } // Reads secret base64 string from a file func decodeSecretString(filePath: String) throws -> String { let privateKeyString: String if #available(macOS 10.15.4, *) { // Prefer to use FileHandle which supports process substitution: // https://github.com/sparkle-project/Sparkle/issues/2605 let fileHandle = try FileHandle(forReadingFrom: URL(fileURLWithPath: filePath)) guard let data = try fileHandle.readToEnd() else { throw DecodeSecretStringError.unableToReadData } guard let decodedPrivateKeyString = String(data: data, encoding: .utf8) else { throw DecodeSecretStringError.unableToDecodeDataAsUTF8String } privateKeyString = decodedPrivateKeyString } else { privateKeyString = try String(contentsOf: URL(fileURLWithPath: filePath)) } return privateKeyString.trimmingCharacters(in: .whitespacesAndNewlines) } ================================================ FILE: common_cli/Signing.swift ================================================ // // signing.swift // Sparkle // // Created on 12/26/25. // Copyright © 2025 Sparkle Project. All rights reserved. // import Foundation struct PrivateKeys { var privateDSAKey: SecKey? var privateEdKey: Data? var publicEdKey: Data? init(privateDSAKey: SecKey?, privateEdKey: Data?, publicEdKey: Data?) { self.privateDSAKey = privateDSAKey self.privateEdKey = privateEdKey self.publicEdKey = publicEdKey } } enum SigningError: Error { case failedToDecodeSigningBlockAsUTF8 } // All of these are for legacy DSA related errors enum DSAError: Error { case invalidOpenSSLFormat case invalidSecTransformData case secKeychainOpenFailure case secItemImportFailure case secItemCopyFailure } // Adds a XML comment at the begining warning the developer about making future modifications to this signed appcast. func addSignWarningToAppcast(data inputData: Data) -> Data { let xmlData: Data do { let signWarningPrefix = " sparkle-sign-warning:" let signWarningMessage = "\nIMPORTANT: This file was signed by Sparkle. Any modifications to this file requires re-signing this file with generate_appcast or sign_update! The signed signature will be embedded at the end of this file.\n" let readAndWriteOptions: XMLNode.Options = [ XMLNode.Options.nodeLoadExternalEntitiesNever, XMLNode.Options.nodePreserveCDATA, XMLNode.Options.nodePreserveWhitespace, ] do { let document = try XMLDocument(data: inputData, options: readAndWriteOptions) var foundSignWarningComment: Bool = false if let documentChildren = document.children { for child in documentChildren { if child.kind == .comment, let commentValue = child.stringValue, commentValue.hasPrefix(signWarningPrefix) { foundSignWarningComment = true break } } } if !foundSignWarningComment { let newSigningWarningMessage = "\(signWarningPrefix)\(signWarningMessage)" let newCommentNode = XMLNode(kind: .comment) newCommentNode.stringValue = newSigningWarningMessage document.insertChild(newCommentNode, at: 0) xmlData = document.xmlData(options: readAndWriteOptions) } else { xmlData = inputData } } catch { print("Error: failed to parse XML data: \(error)") xmlData = inputData } } return xmlData } // Sign the contents of the XML data, and append the signing block (containing the signature / other info) // to the end of the appcast data. // The input data should have the signing block already stripped (use SPUExtractAppcastContent() func signAppcast(data contentData: Data, publicEdKey: Data, privateEdKey: Data) throws -> Data { let base64Signature = edSignature(data: contentData, publicEdKey: publicEdKey, privateEdKey: privateEdKey) let signingBlock = "\n" guard let signingBlockData = signingBlock.data(using: .utf8) else { // Extremely unlikely to occur throw SigningError.failedToDecodeSigningBlockAsUTF8 } var signedData = contentData signedData.append(signingBlockData) return signedData } // This inserts a HTML comment in the beginning of the data warning the developer // that future modifications to this file will require it to be re-signed func updateHTMLCommentSigningWarningInReleaseNotes(data: Data) -> Data? { let modificationWarningPrefix = "\n" let modificationWarningMessage = "\(modificationWarningPrefix)\nIMPORTANT: This file was signed by Sparkle. Any modifications to this file requires updating signatures in appcasts that reference this file! This will involve re-running generate_appcast or sign_update.\n\(modificationWarningSuffix)" var dataToSign = data dataToSign.insert(contentsOf: Data(modificationWarningMessage.utf8), at: dataToSign.startIndex) return dataToSign } #if GENERATE_APPCAST_BUILD_LEGACY_DSA_SUPPORT func loadPrivateDSAKey(at privateKeyURL: URL) throws -> SecKey { let data = try Data(contentsOf: privateKeyURL) var cfitems: CFArray? var format = SecExternalFormat.formatOpenSSL var type = SecExternalItemType.itemTypePrivateKey let status = SecItemImport(data as CFData, nil, &format, &type, SecItemImportExportFlags(rawValue: UInt32(0)), nil, nil, &cfitems) if status != errSecSuccess || cfitems == nil { print("Private DSA key file", privateKeyURL.path, "exists, but it could not be read. SecItemImport error", status) throw DSAError.secItemImportFailure } if format != SecExternalFormat.formatOpenSSL || type != SecExternalItemType.itemTypePrivateKey { print("Not an OpensSSL private key \(format) \(type)") throw DSAError.invalidOpenSSLFormat } return (cfitems! as NSArray)[0] as! SecKey } func loadPrivateDSAKey(named keyName: String, fromKeychainAt keychainURL: URL) throws -> SecKey { var keychain: SecKeychain? guard SecKeychainOpen(keychainURL.path, &keychain) == errSecSuccess, keychain != nil else { throw DSAError.secKeychainOpenFailure } let query: [CFString: CFTypeRef] = [ kSecClass: kSecClassKey, kSecAttrKeyClass: kSecAttrKeyClassPrivate, kSecAttrLabel: keyName as CFString, kSecMatchLimit: kSecMatchLimitOne, kSecUseKeychain: keychain!, kSecReturnRef: kCFBooleanTrue, ] var item: CFTypeRef? guard SecItemCopyMatching(query as CFDictionary, &item) == errSecSuccess, item != nil else { throw DSAError.secItemCopyFailure } return item! as! SecKey } func dsaSignature(path: URL, privateDSAKey: SecKey) throws -> String { var error: Unmanaged? let stream = InputStream(fileAtPath: path.path)! let dataReadTransform = SecTransformCreateReadTransformWithReadStream(stream) let dataDigestTransform = SecDigestTransformCreate(kSecDigestSHA1, 20, nil) guard let dataSignTransform = SecSignTransformCreate(privateDSAKey, &error) else { print("can't use the key") throw error!.takeRetainedValue() } let group = SecTransformCreateGroupTransform() SecTransformConnectTransforms(dataReadTransform, kSecTransformOutputAttributeName, dataDigestTransform, kSecTransformInputAttributeName, group, &error) if error != nil { throw error!.takeRetainedValue() } SecTransformConnectTransforms(dataDigestTransform, kSecTransformOutputAttributeName, dataSignTransform, kSecTransformInputAttributeName, group, &error) if error != nil { throw error!.takeRetainedValue() } let result = SecTransformExecute(group, &error) if error != nil { throw error!.takeRetainedValue() } guard let resultData = result as? Data else { throw DSAError.invalidSecTransformData } return resultData.base64EncodedString() } #endif func edSignature(data: Data, publicEdKey: Data, privateEdKey: Data) -> String { assert(publicEdKey.count == 32) assert(privateEdKey.count == 64) let data = Array(data) var output = Array(repeating: 0, count: 64) let pubkey = Array(publicEdKey), privkey = Array(privateEdKey) ed25519_sign(&output, data, data.count, pubkey, privkey) return Data(output).base64EncodedString() } func edSignature(path: URL, publicEdKey: Data, privateEdKey: Data) throws -> String { let data = try Data(contentsOf: path, options: .mappedIfSafe) return edSignature(data: data, publicEdKey: publicEdKey, privateEdKey: privateEdKey) } ================================================ FILE: generate_appcast/Appcast.swift ================================================ // // Created by Kornel on 23/12/2016. // Copyright © 2016 Sparkle Project. All rights reserved. // import Foundation func makeError(code: SUError, _ description: String) -> NSError { return NSError(domain: SUSparkleErrorDomain, code: Int(OSStatus(code.rawValue)), userInfo: [ NSLocalizedDescriptionKey: description, ]) } typealias UpdateVersion = String typealias FeedName = String struct Appcast { let inferredAppName: String let versionsInFeed: [UpdateVersion] let ignoredVersionsToInsert: Set let archives: [UpdateVersion: ArchiveItem] let deltaPathsUsed: Set let deltaFromVersionsUsed: Set } func makeAppcasts(archivesSourceDir: URL, outputPathURL: URL?, cacheDirectory cacheDir: URL, keys: PrivateKeys, versions: Set?, maxVersionsPerBranchInFeed: Int, newChannel: String?, newMinimumUpdateVersion: String?, majorVersion: String?, maximumDeltas: Int, deltaCompressionModeDescription: String, deltaCompressionLevel: UInt8, disableNestedCodeCheck: Bool, downloadURLPrefix: URL?, releaseNotesURLPrefix: URL?, verbose: Bool) throws -> [FeedName: Appcast] { let standardComparator = SUStandardVersionComparator() let descendingVersionComparator: (String, String) -> Bool = { return standardComparator.compareVersion($0, toVersion: $1) == .orderedDescending } let allUpdates = (try unarchiveUpdates(archivesSourceDir: archivesSourceDir, archivesDestDir: cacheDir, disableNestedCodeCheck: disableNestedCodeCheck, verbose: verbose)) .sorted(by: { descendingVersionComparator($0.version, $1.version) }) if allUpdates.count == 0 { throw makeError(code: .noUpdateError, "No usable archives found in \(archivesSourceDir.path)") } // Apply download and release notes prefixes for update in allUpdates { update.downloadUrlPrefix = downloadURLPrefix update.releaseNotesURLPrefix = releaseNotesURLPrefix } // Group updates by appcast feed var updatesByAppcast: [FeedName: [ArchiveItem]] = [:] for update in allUpdates { let appcastFile = update.feedURL?.lastPathComponent ?? "appcast.xml" updatesByAppcast[appcastFile, default: []].append(update) } // If a (single) output filename was specified on the command-line, but more than one // appcast file was found in the archives, then it's an error. if let outputPathURL = outputPathURL, updatesByAppcast.count > 1 { throw makeError(code: .appcastError, "Cannot write to \(outputPathURL.path): multiple appcasts found") } let group = DispatchGroup() var updateArchivesToSign: [ArchiveItem] = [] var appcastByFeed: [FeedName: Appcast] = [:] for (feed, updates) in updatesByAppcast { var archivesTable: [UpdateVersion: ArchiveItem] = [:] for update in updates { archivesTable[update.version] = update } let feedURL = outputPathURL ?? archivesSourceDir.appendingPathComponent(feed) // Find all the update versions & branches from our existing feed (if available) let feedUpdateBranches: [UpdateVersion: UpdateBranch] if let reachable = try? feedURL.checkResourceIsReachable(), reachable { feedUpdateBranches = try readAppcast(archives: archivesTable, appcastURL: feedURL) } else { feedUpdateBranches = [:] } // Find which versions are new and old but aren't in the feed and that we should ignore/skip var ignoredVersionsToInsert: Set = Set() var ignoredOldVersions: Set = Set() if let versions = versions { // Note for this path we may be adding old updates to ignoredVersionsToInsert too. // It is difficult to differentiate between old and potential new updates that aren't in the feed // As a consequence, some old updates may not be pruned with this option. for update in updates { if feedUpdateBranches[update.version] == nil && !versions.contains(update.version) { ignoredVersionsToInsert.insert(update.version) } } } else { // If the user doesn't specify which versions to generate updates for, // then by default we ignore generating updates that are less than the latest update in the existing feed // The reason why we need to do this is because new branch-specific flags the user can specify like the channel // or the major version will be applied to new unknown updates. We can only absolutely be sure to apply this to // updates that are greater in version than the top of the current feed, or if the user uses --versions. for latestUpdateCandidate in updates { if feedUpdateBranches[latestUpdateCandidate.version] != nil { // Found the latest update in the feed let latestUpdateVersionInFeed = latestUpdateCandidate.version // Filter out any new potential updates that are less than our latest update in our existing feed for update in updates { if feedUpdateBranches[update.version] == nil && descendingVersionComparator(latestUpdateVersionInFeed, update.version) { ignoredOldVersions.insert(update.version) } } break } } } // Find new update versions and their branches var newUpdateBranches: [UpdateVersion: UpdateBranch] = [:] do { for update in updates { if !ignoredOldVersions.contains(update.version) && !ignoredVersionsToInsert.contains(update.version) && feedUpdateBranches[update.version] == nil { newUpdateBranches[update.version] = UpdateBranch(minimumUpdateVersion: newMinimumUpdateVersion, minimumSystemVersion: update.minimumSystemVersion, maximumSystemVersion: nil, minimumAutoupdateVersion: majorVersion, hardwareRequirements: update.hardwareRequirements, channel: newChannel) } } } // Compute latest versions per distinct branch we need to keep // Also compute the batch of recent versions we should preserve/add in the feed let versionsPreservedInFeed: [UpdateVersion] var latestVersionPerBranch: Set = [] do { // Group update versions by branch var updatesGroupedByBranch: [UpdateBranch: [UpdateVersion]] = [:] for (version, branch) in feedUpdateBranches { updatesGroupedByBranch[branch, default: []].append(version) } for (version, branch) in newUpdateBranches { updatesGroupedByBranch[branch, default: []].append(version) } // Grab latest batch of versions per branch for (branch, versions) in updatesGroupedByBranch { updatesGroupedByBranch[branch] = Array(versions.sorted(by: descendingVersionComparator).prefix(maxVersionsPerBranchInFeed)) } // Remove extraneous versions for branches that have converged, // as long as the user doesn't opt into keeping all versions in the feed if maxVersionsPerBranchInFeed < Int.max { for (branch, versions) in updatesGroupedByBranch { guard branch.channel != nil else { continue } let defaultChannelBranch = UpdateBranch(minimumUpdateVersion: branch.minimumUpdateVersion, minimumSystemVersion: branch.minimumSystemVersion, maximumSystemVersion: branch.maximumSystemVersion, minimumAutoupdateVersion: branch.minimumAutoupdateVersion, hardwareRequirements: branch.hardwareRequirements, channel: nil) guard let defaultChannelVersions = updatesGroupedByBranch[defaultChannelBranch] else { continue } if descendingVersionComparator(defaultChannelVersions[0], versions[0]) { updatesGroupedByBranch[branch] = [versions[0]] } } } // Grab latest versions per branch var latestBatchOfVersionsPerBranch: Set = [] for (_, versions) in updatesGroupedByBranch { latestBatchOfVersionsPerBranch.formUnion(versions) latestVersionPerBranch.insert(versions[0]) } versionsPreservedInFeed = Array(latestBatchOfVersionsPerBranch).sorted(by: descendingVersionComparator) } // Update signatures for the latest updates we keep in the feed for version in versionsPreservedInFeed { guard let update = archivesTable[version] else { continue } updateArchivesToSign.append(update) group.enter() DispatchQueue.global().async { #if GENERATE_APPCAST_BUILD_LEGACY_DSA_SUPPORT if let privateDSAKey = keys.privateDSAKey { do { update.dsaSignature = try dsaSignature(path: update.archivePath, privateDSAKey: privateDSAKey) } catch { print(update, error) } } else if update.supportsDSA { print("Note: did not sign with legacy DSA \(update.archivePath.path) because private DSA key file was not specified") } #endif if let publicEdKey = update.publicEdKey { if let privateEdKey = keys.privateEdKey, let expectedPublicKey = keys.publicEdKey { if publicEdKey == expectedPublicKey { do { update.edSignature = try edSignature(path: update.archivePath, publicEdKey: publicEdKey, privateEdKey: privateEdKey) } catch { update.signingError = error print(update, error) } } else { print("Warning: SUPublicEDKey in the app \(update.archivePath.path) does not match key EdDSA in the Keychain. Run generate_keys and update Info.plist to match") } } else { let error = makeError(code: .insufficientSigningError, "Could not sign \(update.archivePath.path) due to lack of private EdDSA key") update.signingError = error print("Error: could not sign \(update.archivePath.path) due to lack of private EdDSA key") } } group.leave() } } // Generate delta updates from the latest updates we keep // Keep track of which delta archives we need referenced in the appcast still var deltaPathsUsed: Set = [] var deltaFromVersionsUsed: Set = [] for version in versionsPreservedInFeed { guard let latestItem = archivesTable[version] else { continue } // We only generate deltas for the latest version per branch, // but we still wanted to record the used delta updates for a batch of recent updates // This is to support rollback in case the top newly generated update isn't exactly what the user wants let generatingDeltas = latestVersionPerBranch.contains(version) let itemDeltasLock = NSLock() var numDeltas = 0 let appBaseName = latestItem.appPath.deletingPathExtension().lastPathComponent for item in updates { if numDeltas >= maximumDeltas { break } // No downgrades if .orderedAscending != standardComparator.compareVersion(item.version, toVersion: latestItem.version) { continue } // Old version will not be able to verify the new version if !item.supportsDSA && item.publicEdKey == nil { continue } // No need to generate delta updates that don't match minimumUpdateVersion if let latestUpdateBranch = feedUpdateBranches[latestItem.version] ?? newUpdateBranches[latestItem.version], let latestMinimumUpdateVersion = latestUpdateBranch.minimumUpdateVersion, standardComparator.compareVersion(item.version, toVersion: latestMinimumUpdateVersion) == .orderedAscending { continue } let deltaBaseName = appBaseName + latestItem.version + "-" + item.version let deltaPath = archivesSourceDir.appendingPathComponent(deltaBaseName).appendingPathExtension("delta") deltaPathsUsed.insert(deltaPath.path) deltaFromVersionsUsed.insert(item.version) numDeltas += 1 if !generatingDeltas { continue } var delta: DeltaUpdate let ignoreMarkerPath = cacheDir.appendingPathComponent(deltaPath.lastPathComponent).appendingPathExtension(".ignore") let fm = FileManager.default if fm.fileExists(atPath: ignoreMarkerPath.path) { continue } if !fm.fileExists(atPath: deltaPath.path) { // Test if old and new app have the same code signing signature. If not, omit a warning. // This is a good time to do this check because our delta handling code sets a marker // to avoid this path each time generate_appcast is called. let oldAppCodeSigned = SUCodeSigningVerifier.bundle(atURLIsCodeSigned: item.appPath) let newAppCodeSigned = SUCodeSigningVerifier.bundle(atURLIsCodeSigned: latestItem.appPath) if oldAppCodeSigned != newAppCodeSigned && !newAppCodeSigned { print("Warning: New app is not code signed but older version (\(item)) is: \(latestItem)") } else if oldAppCodeSigned && newAppCodeSigned { do { try SUCodeSigningVerifier.codeSignatureIsValid(atBundleURL: latestItem.appPath, andMatchesSignatureAtBundleURL: item.appPath) } catch { print("Warning: found mismatch code signing identity between \(item) and \(latestItem)") } } do { // Decide the most appropriate delta version let deltaVersion: SUBinaryDeltaMajorVersion if let frameworkVersion = item.frameworkVersion { switch standardComparator.compareVersion(frameworkVersion, toVersion: "2041") { case .orderedSame: fallthrough case .orderedDescending: deltaVersion = .version4 case .orderedAscending: switch standardComparator.compareVersion(frameworkVersion, toVersion: "2010") { case .orderedSame: fallthrough case .orderedDescending: deltaVersion = .version3 case .orderedAscending: deltaVersion = .version2 } } } else { deltaVersion = SUBinaryDeltaMajorVersionDefault print("Warning: Sparkle.framework version for \(item.appPath.lastPathComponent) (\(item.shortVersion) (\(item.version))) was not found. Falling back to generating delta using default delta version..") } let requestedDeltaCompressionMode = deltaCompressionModeFromDescription(deltaCompressionModeDescription, nil) // Version 2 formats only support bzip2, none, and default options let deltaCompressionMode: SPUDeltaCompressionMode if deltaVersion == .version2 { switch requestedDeltaCompressionMode { case .LZFSE: fallthrough case .LZ4: fallthrough case .LZMA: fallthrough case .ZLIB: deltaCompressionMode = .bzip2 print("Warning: Delta compression mode '\(deltaCompressionModeDescription)' was requested but using default compression instead because version 2 delta file from version \(item.version) needs to be generated..") case SPUDeltaCompressionModeDefault: fallthrough case .none: fallthrough case .bzip2: deltaCompressionMode = requestedDeltaCompressionMode @unknown default: // This shouldn't happen print("Warning: failed to parse delta compression mode \(deltaCompressionModeDescription). There is a logic bug in generate_appcast.") deltaCompressionMode = SPUDeltaCompressionModeDefault } } else { deltaCompressionMode = requestedDeltaCompressionMode } delta = try DeltaUpdate.create(from: item, to: latestItem, deltaVersion: deltaVersion, deltaCompressionMode: deltaCompressionMode, deltaCompressionLevel: deltaCompressionLevel, archivePath: deltaPath) } catch { print("Could not create delta update", deltaPath.path, error) continue } } else { delta = DeltaUpdate(fromVersion: item.version, archivePath: deltaPath, sparkleExecutableFileSize: item.sparkleExecutableFileSize, sparkleLocales: item.sparkleLocales) } // Require delta to be a bit smaller if delta.fileSize / 7 > latestItem.fileSize / 8 { markDeltaAsIgnored(delta: delta, markerPath: ignoreMarkerPath) continue } itemDeltasLock.lock() latestItem.deltas.append(delta) itemDeltasLock.unlock() group.enter() DispatchQueue.global().async { #if GENERATE_APPCAST_BUILD_LEGACY_DSA_SUPPORT if item.supportsDSA, let privateDSAKey = keys.privateDSAKey { do { delta.dsaSignature = try dsaSignature(path: deltaPath, privateDSAKey: privateDSAKey) } catch { print(delta.archivePath.lastPathComponent, error) } } #endif if let publicEdKey = item.publicEdKey, let privateEdKey = keys.privateEdKey { do { delta.edSignature = try edSignature(path: deltaPath, publicEdKey: publicEdKey, privateEdKey: privateEdKey) } catch { print(delta.archivePath.lastPathComponent, error) } } do { var hasAnyDSASignature = (delta.edSignature != nil) #if GENERATE_APPCAST_BUILD_LEGACY_DSA_SUPPORT hasAnyDSASignature = hasAnyDSASignature || (delta.dsaSignature != nil) #endif if !hasAnyDSASignature { markDeltaAsIgnored(delta: delta, markerPath: ignoreMarkerPath) print("Delta \(delta.archivePath.path) ignored, because it could not be signed") itemDeltasLock.lock() latestItem.deltas.removeAll { $0 === delta } itemDeltasLock.unlock() } } group.leave() } } } let inferredAppName = updates[0].appPath.deletingPathExtension().lastPathComponent let appcast = Appcast(inferredAppName: inferredAppName, versionsInFeed: versionsPreservedInFeed, ignoredVersionsToInsert: ignoredVersionsToInsert, archives: archivesTable, deltaPathsUsed: deltaPathsUsed, deltaFromVersionsUsed: deltaFromVersionsUsed) appcastByFeed[feed] = appcast } group.wait() // Check for fatal signing errors for update in updateArchivesToSign { if let signingError = update.signingError { throw signingError } } return appcastByFeed } func moveOldUpdatesFromAppcasts(archivesSourceDir: URL, oldFilesDirectory: URL, cacheDirectory: URL, appcasts: [Appcast], autoPruneUpdates: Bool) -> (movedCount: Int, prunedCount: Int) { let fileManager = FileManager.default let suFileManager = SUFileManager() // Create old files updates directory if needed var createdOldFilesDirectory = false let makeOldFilesDirectory: () -> Bool = { guard !createdOldFilesDirectory else { return true } if fileManager.fileExists(atPath: oldFilesDirectory.path) { createdOldFilesDirectory = true return true } do { try fileManager.createDirectory(at: oldFilesDirectory, withIntermediateDirectories: false) createdOldFilesDirectory = true return true } catch { print("Warning: failed to create \(oldFilesDirectory.lastPathComponent) in \(archivesSourceDir.lastPathComponent): \(error)") return false } } var movedItemsCount = 0 // Move aside all old unused update items for appcast in appcasts { let versionsInFeedSet = Set(appcast.versionsInFeed) for (version, update) in appcast.archives { guard !versionsInFeedSet.contains(version) && !appcast.deltaFromVersionsUsed.contains(version) && !appcast.ignoredVersionsToInsert.contains(version) else { continue } let archivePath = update.archivePath guard makeOldFilesDirectory() else { return (movedItemsCount, 0) } do { try suFileManager.updateModificationAndAccessTimeOfItem(at: archivePath) } catch { print("Warning: failed to update modification time for \(archivePath.path): \(error)") } do { try fileManager.moveItem(at: archivePath, to: oldFilesDirectory.appendingPathComponent(archivePath.lastPathComponent)) movedItemsCount += 1 // Remove cache for the update let appCachePath = update.appPath.deletingLastPathComponent() let _ = try? fileManager.removeItem(at: appCachePath) } catch { print("Warning: failed to move \(archivePath.lastPathComponent) to \(oldFilesDirectory.lastPathComponent): \(error)") } let htmlReleaseNotesFile = archivePath.deletingPathExtension().appendingPathExtension("html") let plainTextReleaseNotesFile = archivePath.deletingPathExtension().appendingPathExtension("txt") let markdownReleaseNotesFile = archivePath.deletingPathExtension().appendingPathExtension("md") let markdownSecondaryReleaseNotesFile = archivePath.deletingPathExtension().appendingPathExtension("markdown") let releaseNotesFile: URL? if fileManager.fileExists(atPath: htmlReleaseNotesFile.path) { releaseNotesFile = htmlReleaseNotesFile } else if fileManager.fileExists(atPath: plainTextReleaseNotesFile.path) { releaseNotesFile = plainTextReleaseNotesFile } else if fileManager.fileExists(atPath: markdownReleaseNotesFile.path) { releaseNotesFile = markdownReleaseNotesFile } else if fileManager.fileExists(atPath: markdownSecondaryReleaseNotesFile.path) { releaseNotesFile = markdownSecondaryReleaseNotesFile } else { releaseNotesFile = nil } if let releaseNotesFile = releaseNotesFile { do { try suFileManager.updateModificationAndAccessTimeOfItem(at: releaseNotesFile) } catch { print("Warning: failed to update modification time for \(releaseNotesFile.path): \(error)") } do { try fileManager.moveItem(at: releaseNotesFile, to: oldFilesDirectory.appendingPathComponent(releaseNotesFile.lastPathComponent)) movedItemsCount += 1 } catch { print("Warning: failed to move \(releaseNotesFile.lastPathComponent) to \(oldFilesDirectory.lastPathComponent): \(error)") } } } } // Move aside all unused delta items in the archives directory // We will be missing out on ignore markers in the cache for delta items because they're difficult to fetch // However they are zero-sized so they don't take much space anyway do { let directoryContents = try fileManager.contentsOfDirectory(atPath: archivesSourceDir.path) for filename in directoryContents { guard filename.hasSuffix(".delta") else { continue } let deltaURL = archivesSourceDir.appendingPathComponent(filename) do { var foundDeltaItemUsage = false for appcast in appcasts { if appcast.deltaPathsUsed.contains(deltaURL.path) { foundDeltaItemUsage = true break } } guard !foundDeltaItemUsage else { continue } } guard makeOldFilesDirectory() else { return (movedItemsCount, 0) } movedItemsCount += 1 do { try suFileManager.updateModificationAndAccessTimeOfItem(at: deltaURL) } catch { print("Warning: Failed to update modification time for \(deltaURL.lastPathComponent): \(error)") } do { try fileManager.moveItem(at: deltaURL, to: oldFilesDirectory.appendingPathComponent(filename)) } catch { print("Warning: failed to move \(deltaURL.lastPathComponent) to \(oldFilesDirectory.lastPathComponent): \(error)") } } } catch { print("Warning: failed to list contents of \(archivesSourceDir.lastPathComponent) during pruning: \(error)") } var pruneCount = 0 if autoPruneUpdates { // Garbage collect the old updates directory do { let directoryContents = try fileManager.contentsOfDirectory(atPath: oldFilesDirectory.path) // Delete files that have roughly not been touched for 14 days let prunedFileDeletionInterval: TimeInterval = 86400 * 14 let currentDate = Date() for filename in directoryContents { guard !filename.hasPrefix(".") else { continue } let fileURL = oldFilesDirectory.appendingPathComponent(filename) if let resourceValues = try? fileURL.resourceValues(forKeys: [.contentModificationDateKey]), let lastModificationDate = resourceValues.contentModificationDate { if currentDate.timeIntervalSince(lastModificationDate) >= prunedFileDeletionInterval { do { try fileManager.removeItem(at: fileURL) pruneCount += 1 } catch { print("Warning: failed to delete old update file \(oldFilesDirectory.lastPathComponent)/\(fileURL.lastPathComponent): \(error)") } } } } } catch { // Nothing to log for failing to fetch prunedDirectory } } return (movedItemsCount, pruneCount) } func markDeltaAsIgnored(delta: DeltaUpdate, markerPath: URL) { _ = try? FileManager.default.removeItem(at: delta.archivePath) _ = try? Data.init().write(to: markerPath); // 0-sized file } ================================================ FILE: generate_appcast/ArchiveItem.swift ================================================ // // Created by Kornel on 22/12/2016. // Copyright © 2016 Sparkle Project. All rights reserved. // import Foundation import UniformTypeIdentifiers struct UpdateBranch: Hashable { let minimumUpdateVersion: String? let minimumSystemVersion: String? let maximumSystemVersion: String? let minimumAutoupdateVersion: String? let hardwareRequirements: String? let channel: String? } class DeltaUpdate { let fromVersion: String let archivePath: URL #if GENERATE_APPCAST_BUILD_LEGACY_DSA_SUPPORT var dsaSignature: String? #endif var edSignature: String? let sparkleExecutableFileSize: Int? let sparkleLocales: String? init(fromVersion: String, archivePath: URL, sparkleExecutableFileSize: Int?, sparkleLocales: String?) { self.archivePath = archivePath self.fromVersion = fromVersion self.sparkleExecutableFileSize = sparkleExecutableFileSize self.sparkleLocales = sparkleLocales } var fileSize: Int64 { let archiveFileAttributes = try! FileManager.default.attributesOfItem(atPath: self.archivePath.path) return (archiveFileAttributes[.size] as! NSNumber).int64Value } class func create(from: ArchiveItem, to: ArchiveItem, deltaVersion: SUBinaryDeltaMajorVersion, deltaCompressionMode: SPUDeltaCompressionMode, deltaCompressionLevel: UInt8, archivePath: URL) throws -> DeltaUpdate { var createDiffError: NSError? if !createBinaryDelta(from.appPath.path, to.appPath.path, archivePath.path, deltaVersion, deltaCompressionMode, deltaCompressionLevel, false, &createDiffError) { throw createDiffError! } // Ensure applying the diff also succeeds let fileManager = FileManager.default let tempApplyToPath = to.appPath.deletingLastPathComponent().appendingPathComponent(".temp_" + to.appPath.lastPathComponent) let _ = try? fileManager.removeItem(at: tempApplyToPath) var applyDiffError: NSError? if !applyBinaryDelta(from.appPath.path, tempApplyToPath.path, archivePath.path, false, { _ in }, &applyDiffError) { let _ = try? fileManager.removeItem(at: archivePath) throw applyDiffError! } let _ = try? fileManager.removeItem(at: tempApplyToPath) return DeltaUpdate(fromVersion: from.version, archivePath: archivePath, sparkleExecutableFileSize: from.sparkleExecutableFileSize, sparkleLocales: from.sparkleLocales) } } class ArchiveItem: CustomStringConvertible { let version: String // swiftlint:disable identifier_name let _shortVersion: String? let minimumSystemVersion: String let hardwareRequirements: String? let frameworkVersion: String? let sparkleExecutableFileSize: Int? let sparkleLocales: String? let archivePath: URL let appPath: URL let feedURL: URL? let publicEdKey: Data? let supportsDSA: Bool let requiresSignedAppcast: Bool let archiveFileAttributes: [FileAttributeKey: Any] var deltas: [DeltaUpdate] #if GENERATE_APPCAST_BUILD_LEGACY_DSA_SUPPORT var dsaSignature: String? #endif var edSignature: String? var downloadUrlPrefix: URL? var releaseNotesURLPrefix: URL? var signingError: Error? @available(macOS 10.15.4, *) private static func binaryContainsPreARMSlice(_ fileURL: URL) -> Bool? { guard let fileHandle = try? FileHandle(forReadingFrom: fileURL) else { return nil } defer { try? fileHandle.close() } // First, read just enough bytes to determine the magic number (4 bytes) let MAGIC_SIZE = 4 guard let magicData = try? fileHandle.read(upToCount: MAGIC_SIZE), magicData.count == MAGIC_SIZE else { return nil } let preARMSlices: [cpu_type_t] = [CPU_TYPE_X86_64, CPU_TYPE_I386, CPU_TYPE_POWERPC, CPU_TYPE_POWERPC64] // Read magic number let magic = magicData.withUnsafeBytes { $0.load(as: UInt32.self) } // Check if this is a fat binary if magic == FAT_CIGAM || magic == FAT_CIGAM_64 { // For fat binaries, we need to read the rest of the fat_header to get the number of architectures guard let remainingHeaderData = try? fileHandle.read(upToCount: MemoryLayout.size - MAGIC_SIZE), remainingHeaderData.count == MemoryLayout.size - MAGIC_SIZE else { return nil } // Read number of architectures let nfat_arch = remainingHeaderData.withUnsafeBytes { let value = $0.load(fromByteOffset: 0, as: UInt32.self) return value.bigEndian } let is64BitMagic = (magic == FAT_CIGAM_64) let fatArchSize = is64BitMagic ? MemoryLayout.size : MemoryLayout.size // Read each architecture header for i in 0...size + (i * fatArchSize) // We need to seek from the beginning of the file guard let _ = try? fileHandle.seek(toOffset: UInt64(offset)), let archHeaderData = try? fileHandle.read(upToCount: fatArchSize), archHeaderData.count >= fatArchSize else { continue } let cputype = archHeaderData.withUnsafeBytes { let value = $0.load(as: Int32.self) return Int32(bigEndian: value) } if preARMSlices.contains(cputype) { return true } } return false } else if magic == MH_MAGIC || magic == MH_MAGIC_64 || magic == MH_CIGAM || magic == MH_CIGAM_64 { // For Mach-O binaries, we need to read the rest of the mach_header to get CPU type let is64BitMagic = (magic == MH_MAGIC_64 || magic == MH_CIGAM_64) let machHeaderSize = is64BitMagic ? MemoryLayout.size : MemoryLayout.size // Read the rest of the header guard let remainingHeaderData = try? fileHandle.read(upToCount: machHeaderSize - MAGIC_SIZE), remainingHeaderData.count == machHeaderSize - MAGIC_SIZE else { return nil } let reversedEndian = (magic == MH_CIGAM || magic == MH_CIGAM_64) let cputype = remainingHeaderData.withUnsafeBytes { let value = $0.load(fromByteOffset: 0, as: Int32.self) return reversedEndian ? Int32(bigEndian: value) : Int32(littleEndian: value) } return preARMSlices.contains(cputype) } else { return nil } } init(version: String, shortVersion: String?, feedURL: URL?, requiresSignedAppcast: Bool, minimumSystemVersion: String?, hardwareRequirements: String?, frameworkVersion: String?, sparkleExecutableFileSize: Int?, sparkleLocales: String?, publicEdKey: String?, supportsDSA: Bool, appPath: URL, archivePath: URL) throws { self.version = version self._shortVersion = shortVersion self.feedURL = feedURL self.requiresSignedAppcast = requiresSignedAppcast self.minimumSystemVersion = minimumSystemVersion ?? "10.13" self.hardwareRequirements = hardwareRequirements self.frameworkVersion = frameworkVersion self.sparkleExecutableFileSize = sparkleExecutableFileSize self.sparkleLocales = sparkleLocales self.archivePath = archivePath self.appPath = appPath self.supportsDSA = supportsDSA if let publicEdKey = publicEdKey { self.publicEdKey = Data(base64Encoded: publicEdKey) } else { self.publicEdKey = nil } let path = (self.archivePath.path as NSString).resolvingSymlinksInPath self.archiveFileAttributes = try FileManager.default.attributesOfItem(atPath: path) self.deltas = [] } convenience init(fromArchive archivePath: URL, unarchivedDir: URL, validateBundle: Bool, disableNestedCodeCheck: Bool) throws { let resourceKeys: [URLResourceKey] if #available(macOS 11, *) { resourceKeys = [.contentTypeKey] } else { resourceKeys = [.typeIdentifierKey] } let items = try FileManager.default.contentsOfDirectory(at: unarchivedDir, includingPropertiesForKeys: resourceKeys, options: .skipsHiddenFiles) let bundles = items.filter({ if let resourceValues = try? $0.resourceValues(forKeys: Set(resourceKeys)) { if #available(macOS 11, *) { return resourceValues.contentType!.conforms(to: .bundle) } else { return UTTypeConformsTo(resourceValues.typeIdentifier! as CFString, kUTTypeBundle) } } else { return false } }) if bundles.count > 0 { if bundles.count > 1 { throw makeError(code: .unarchivingError, "Too many bundles in \(unarchivedDir.path) \(bundles)") } let appPath = bundles[0] // If requested to validate the bundle, ensure it is properly signed if validateBundle && SUCodeSigningVerifier.bundle(atURLIsCodeSigned: appPath) { try SUCodeSigningVerifier.codeSignatureIsValid(atBundleURL: appPath, checkNestedCode: !disableNestedCodeCheck) } guard let infoPlist = NSDictionary(contentsOf: appPath.appendingPathComponent("Contents/Info.plist")) else { throw makeError(code: .unarchivingError, "No plist \(appPath.path)") } guard let version = infoPlist[kCFBundleVersionKey!] as? String else { throw makeError(code: .unarchivingError, "No Version \(kCFBundleVersionKey as String? ?? "missing kCFBundleVersionKey") \(appPath)") } let shortVersion = infoPlist["CFBundleShortVersionString"] as? String let publicEdKey = infoPlist[SUPublicEDKeyKey] as? String #if GENERATE_APPCAST_BUILD_LEGACY_DSA_SUPPORT let supportsDSA = infoPlist[SUPublicDSAKeyKey] != nil || infoPlist[SUPublicDSAKeyFileKey] != nil #else let supportsDSA = false #endif var feedURL: URL? if let feedURLStr = infoPlist["SUFeedURL"] as? String { feedURL = URL(string: feedURLStr) if feedURL?.pathExtension == "php" { feedURL = feedURL!.deletingLastPathComponent() feedURL = feedURL!.appendingPathComponent("appcast.xml") } } let requiresSignedAppcast: Bool if let requiresSignedAppcastValue = infoPlist[SURequireSignedFeedKey] as? Bool { requiresSignedAppcast = requiresSignedAppcastValue } else { requiresSignedAppcast = false } // Intel Macs shouldn't be supported on macOS 27+ and // we don't have any other hardware requirements except for arm64 right now var mayNeedHardwareRequirement = true let minimumSystemVersion = infoPlist["LSMinimumSystemVersion"] as? String if let minimumSystemVersion { let versionComparator = SUStandardVersionComparator() if versionComparator.compareVersion(minimumSystemVersion, toVersion: "27.0") != .orderedAscending { mayNeedHardwareRequirement = false } } var hardwareRequirements: String? = nil if mayNeedHardwareRequirement { if #available(macOS 10.15.4, *) { if let executableName = infoPlist[kCFBundleExecutableKey as String] as? String { let executablePath = appPath.appendingPathComponent("Contents/MacOS/\(executableName)") if let containsPreARMSlice = Self.binaryContainsPreARMSlice(executablePath), !containsPreARMSlice { hardwareRequirements = SUAppcastElementHardwareRequirementARM64 } } } } var frameworkVersion: String? = nil let sparkleExecutableFileSize: Int? let sparkleLocales: String? do { let fileManager = FileManager.default let frameworksURL: URL? let canonicalFrameworksURL = appPath.appendingPathComponent("Contents/Frameworks/Sparkle.framework") if fileManager.fileExists(atPath: canonicalFrameworksURL.path) { frameworksURL = canonicalFrameworksURL } else { // The framework may be inside another framework or plug-in. Find it. if let enumerator = fileManager.enumerator(at: appPath, includingPropertiesForKeys: [], options: [.skipsHiddenFiles], errorHandler: nil) { var foundFrameworksURL: URL? for case let fileURL as URL in enumerator { let name = fileURL.lastPathComponent // Skip Resources in bundles entirely because frameworks shouldn't be in there and we don't want to pay the cost of scanning in there guard name != "Resources" else { enumerator.skipDescendants() continue } if name == "Sparkle.framework" { foundFrameworksURL = fileURL break } } frameworksURL = foundFrameworksURL } else { frameworksURL = nil } } if let frameworksURL = frameworksURL { let resourcesURL = frameworksURL.appendingPathComponent("Resources").resolvingSymlinksInPath() if let frameworkInfoPlist = NSDictionary(contentsOf: resourcesURL.appendingPathComponent("Info.plist")) { frameworkVersion = frameworkInfoPlist[kCFBundleVersionKey as String] as? String } let frameworkExecutableURL = frameworksURL.appendingPathComponent("Sparkle").resolvingSymlinksInPath() do { let resourceValues = try frameworkExecutableURL.resourceValues(forKeys: [.fileSizeKey]) sparkleExecutableFileSize = resourceValues.fileSize } catch { sparkleExecutableFileSize = nil } do { let resourcesDirectoryContents = try fileManager.contentsOfDirectory(atPath: resourcesURL.path) let localeExtension = ".lproj" let localeExtensionCount = localeExtension.count let maxLocalesToProcess = 7 var localesPresent: [String] = [] var localeIndex = 0 for filename in resourcesDirectoryContents { guard filename.hasSuffix(localeExtension) else { continue } // English and Base directories are the least likely to be stripped, // so let's not bother recording them. guard filename != "en" && filename != "Base" else { continue } let locale = String(filename.dropLast(localeExtensionCount)) localesPresent.append(locale) localeIndex += 1 if localeIndex >= maxLocalesToProcess { break } } if localesPresent.count > 0 { sparkleLocales = localesPresent.joined(separator: ",") } else { sparkleLocales = nil } } catch { sparkleLocales = nil } } else { sparkleExecutableFileSize = nil sparkleLocales = nil } } try self.init(version: version, shortVersion: shortVersion, feedURL: feedURL, requiresSignedAppcast: requiresSignedAppcast, minimumSystemVersion: minimumSystemVersion, hardwareRequirements: hardwareRequirements, frameworkVersion: frameworkVersion, sparkleExecutableFileSize: sparkleExecutableFileSize, sparkleLocales: sparkleLocales, publicEdKey: publicEdKey, supportsDSA: supportsDSA, appPath: appPath, archivePath: archivePath) } else { throw makeError(code: .missingUpdateError, "No supported items in \(unarchivedDir) \(items) [note: only .app bundles are supported]") } } var shortVersion: String { return self._shortVersion ?? self.version } var description: String { return "\(self.archivePath) \(self.version)" } var archiveURL: URL? { guard let escapedFilename = self.archivePath.lastPathComponent.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) else { return nil } if let downloadUrlPrefix = self.downloadUrlPrefix { // if a download url prefix was given use this one return URL(string: escapedFilename, relativeTo: downloadUrlPrefix) } else if let relativeFeedUrl = self.feedURL { return URL(string: escapedFilename, relativeTo: relativeFeedUrl) } return URL(string: escapedFilename) } var pubDate: String { let date = self.archiveFileAttributes[.creationDate] as! Date let formatter = DateFormatter() formatter.locale = Locale(identifier: "en_US_POSIX") formatter.dateFormat = "EEE, dd MMM yyyy HH:mm:ss ZZ" return formatter.string(from: date) } var fileSize: Int64 { return (self.archiveFileAttributes[.size] as! NSNumber).int64Value } var releaseNotesPath: URL? { var basename = self.archivePath.deletingPathExtension() if basename.pathExtension == "tar" { // tar.gz basename = basename.deletingPathExtension() } let htmlReleaseNotes = basename.appendingPathExtension("html") if FileManager.default.fileExists(atPath: htmlReleaseNotes.path) { return htmlReleaseNotes } let plainTextReleaseNotes = basename.appendingPathExtension("txt") if FileManager.default.fileExists(atPath: plainTextReleaseNotes.path) { return plainTextReleaseNotes } let markdownReleaseNotes = basename.appendingPathExtension("md") if FileManager.default.fileExists(atPath: markdownReleaseNotes.path) { return markdownReleaseNotes } let markdownSecondaryReleaseNotes = basename.appendingPathExtension("markdown") if FileManager.default.fileExists(atPath: markdownSecondaryReleaseNotes.path) { return markdownSecondaryReleaseNotes } return nil } private func getReleaseNotesAsFragment(_ path: URL, _ embedReleaseNotesAlways: Bool) -> (content: String, format: String)? { guard let data = try? Data(contentsOf: path) else { return nil } let contentData: Data let format: String let pathExtension = path.pathExtension switch pathExtension { case "txt", "TXT": format = "plain-text" contentData = data case "md", "MD", "markdown", "MARKDOWN": format = "markdown" contentData = SPUExtractReleaseNotesContent(data) default: format = "html" contentData = SPUExtractReleaseNotesContent(data) } if embedReleaseNotesAlways { guard let contentString = String(data: contentData, encoding: .utf8) else { print("Error: failed to read release notes as UTF8 string: \(path.lastPathComponent)") return nil } return (contentString, format) } else if format == "html", let contentString = String(data: contentData, encoding: .utf8), !contentString.localizedCaseInsensitiveContains(" (content: String, format: String)? { if let path = self.releaseNotesPath { return self.getReleaseNotesAsFragment(path, embedReleaseNotesAlways) } return nil } func releaseNotesURL(releaseNotesPath: URL, embedReleaseNotesAlways: Bool) -> URL? { // The file is already used as inline description if self.getReleaseNotesAsFragment(releaseNotesPath, embedReleaseNotesAlways) != nil { return nil } return self.releaseNoteURL(for: releaseNotesPath.lastPathComponent) } func releaseNoteURL(for unescapedFilename: String) -> URL? { guard let escapedFilename = unescapedFilename.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) else { return nil } if let releaseNotesURLPrefix = self.releaseNotesURLPrefix { // If a URL prefix for release notes was passed on the commandline, use it return URL(string: escapedFilename, relativeTo: releaseNotesURLPrefix) } else if let relativeURL = self.feedURL { return URL(string: escapedFilename, relativeTo: relativeURL) } else { return URL(string: escapedFilename) } } func localizedReleaseNotes() -> [(String, URL, URL)] { var basename = archivePath.deletingPathExtension() if basename.pathExtension == "tar" { basename = basename.deletingPathExtension() } var localizedReleaseNotes = [(String, URL, URL)]() for languageCode in Locale.isoLanguageCodes { let baseLocalizedReleaseNoteURL = basename .appendingPathExtension(languageCode) let htmlLocalizedReleaseNoteURL = baseLocalizedReleaseNoteURL.appendingPathExtension("html") let plainTextLocalizedReleaseNoteURL = baseLocalizedReleaseNoteURL.appendingPathExtension("txt") let markdownLocalizedReleaseNoteURL = baseLocalizedReleaseNoteURL.appendingPathExtension("md") let markdownSecondaryLocalizedReleaseNoteURL = baseLocalizedReleaseNoteURL.appendingPathExtension("markdown") let localizedReleaseNoteURL: URL? if (try? htmlLocalizedReleaseNoteURL.checkResourceIsReachable()) ?? false { localizedReleaseNoteURL = htmlLocalizedReleaseNoteURL } else if (try? plainTextLocalizedReleaseNoteURL.checkResourceIsReachable()) ?? false { localizedReleaseNoteURL = plainTextLocalizedReleaseNoteURL } else if (try? markdownLocalizedReleaseNoteURL.checkResourceIsReachable()) ?? false { localizedReleaseNoteURL = markdownLocalizedReleaseNoteURL } else if (try? markdownSecondaryLocalizedReleaseNoteURL.checkResourceIsReachable()) ?? false { localizedReleaseNoteURL = markdownSecondaryLocalizedReleaseNoteURL } else { localizedReleaseNoteURL = nil } if let localizedReleaseNoteURL = localizedReleaseNoteURL, let localizedReleaseNoteRemoteURL = self.releaseNoteURL(for: localizedReleaseNoteURL.lastPathComponent) { localizedReleaseNotes.append((languageCode, localizedReleaseNoteRemoteURL, localizedReleaseNoteURL)) } } return localizedReleaseNotes } let mimeType = "application/octet-stream" } ================================================ FILE: generate_appcast/Bridging-Header.h ================================================ #import #import "SPUExtractSignedFeed.h" #import "SUStandardVersionComparator.h" #import "SUConstants.h" #import "SUErrors.h" #import "SUUnarchiver.h" #import "SUBinaryDeltaUnarchiver.h" #import "SUBinaryDeltaCreate.h" #import "SUBinaryDeltaApply.h" #import "SUBinaryDeltaCommon.h" #import "SUSignatures.h" #import "SUCodeSigningVerifier.h" #import "SPUInstallationType.h" #import "SUFileManager.h" #import "ed25519.h" ================================================ FILE: generate_appcast/FeedXML.swift ================================================ // // Created by Kornel on 22/12/2016. // Copyright © 2016 Sparkle Project. All rights reserved. // import Foundation func findElement(name: String, parent: XMLElement) -> XMLElement? { if let found = try? parent.nodes(forXPath: name) { if found.count > 0 { if let element = found[0] as? XMLElement { return element } } } return nil } func createElement(name: String, parent: XMLElement) -> XMLElement { let element = XMLElement(name: name) parent.addChild(element) return element } func findOrCreateElement(name: String, parent: XMLElement) -> XMLElement { if let element = findElement(name: name, parent: parent) { return element } return createElement(name: name, parent: parent) } func text(_ text: String) -> XMLNode { return XMLNode.text(withStringValue: text) as! XMLNode } func extractVersion(parent: XMLNode) -> String? { guard let itemElement = parent as? XMLElement else { return nil } // Look for version attribute in enclosure if let enclosure = findElement(name: "enclosure", parent: itemElement) { if let versionAttribute = enclosure.attribute(forName: SUAppcastAttributeVersion) { return versionAttribute.stringValue } } // Look for top level version element if let versionElement = findElement(name: SUAppcastElementVersion, parent: itemElement) { return versionElement.stringValue } return nil } func readAppcast(archives: [String: ArchiveItem], appcastURL: URL) throws -> [String: UpdateBranch] { let options: XMLNode.Options = [ XMLNode.Options.nodeLoadExternalEntitiesNever, XMLNode.Options.nodePreserveCDATA, XMLNode.Options.nodePreserveWhitespace, ] let doc = try XMLDocument(contentsOf: appcastURL, options: options) let rootNodes = try doc.nodes(forXPath: "/rss") if rootNodes.count != 1 { throw makeError(code: .appcastError, "Weird XML? \(appcastURL.path)") } let root = rootNodes[0] as! XMLElement let channelNodes = try root.nodes(forXPath: "channel") if channelNodes.count == 0 { throw makeError(code: .appcastError, "Weird Feed? No channels: \(appcastURL.path)") } let channel = channelNodes[0] as! XMLElement guard let itemNodes = try? channel.nodes(forXPath: "item") else { return [:] } var updateBranches: [String: UpdateBranch] = [:] for item in itemNodes { guard let item = item as? XMLElement else { continue } let version: String? if let versionElement = findElement(name: SUAppcastElementVersion, parent: item) { version = versionElement.stringValue } else if let enclosure = findElement(name: "enclosure", parent: item), let versionAttribute = enclosure.attribute(forName: SUAppcastAttributeVersion) { version = versionAttribute.stringValue } else { version = nil } guard let version = version else { continue } let minimumUpdateVersion: String? if let minUpdateVer = findElement(name: SUAppcastElementMinimumUpdateVersion, parent: item) { minimumUpdateVersion = minUpdateVer.stringValue } else { minimumUpdateVersion = nil } let minimumSystemVersion: String? if let minVer = findElement(name: SUAppcastElementMinimumSystemVersion, parent: item) { minimumSystemVersion = minVer.stringValue } else if let archive = archives[version] { minimumSystemVersion = archive.minimumSystemVersion } else { minimumSystemVersion = nil } let maximumSystemVersion: String? if let maxVer = findElement(name: SUAppcastElementMaximumSystemVersion, parent: item) { maximumSystemVersion = maxVer.stringValue } else { maximumSystemVersion = nil } let minimumAutoupdateVersion: String? if let minimumAutoupdateVersionElement = findElement(name: SUAppcastElementMinimumAutoupdateVersion, parent: item) { minimumAutoupdateVersion = minimumAutoupdateVersionElement.stringValue } else { minimumAutoupdateVersion = nil } let hardwareRequirements: String? if let hardwareRequirementsElement = findElement(name: SUAppcastElementHardwareRequirements, parent: item) { hardwareRequirements = hardwareRequirementsElement.stringValue } else { hardwareRequirements = nil } let sparkleChannel: String? if let sparkleChannelElement = findElement(name: SUAppcastElementChannel, parent: item) { sparkleChannel = sparkleChannelElement.stringValue } else { sparkleChannel = nil } let updateBranch = UpdateBranch(minimumUpdateVersion: minimumUpdateVersion, minimumSystemVersion: minimumSystemVersion, maximumSystemVersion: maximumSystemVersion, minimumAutoupdateVersion: minimumAutoupdateVersion, hardwareRequirements: hardwareRequirements, channel: sparkleChannel) updateBranches[version] = updateBranch } return updateBranches } private func signReleaseNotesIfNeeded(element releaseNotesElement: XMLElement, filePath unresolvedReleaseNotesPath: URL, signedReleaseNoteFiles: inout [String: (String, UInt)], requiresSignedAppcast: Bool, disableEmbeddedSignWarning: Bool, keys: PrivateKeys) throws { guard let publicEdKey = keys.publicEdKey, let privateEdKey = keys.privateEdKey else { return } let existingEdSignatureAttribute = releaseNotesElement.attribute(forName: SUAppcastAttributeEDSignature) guard requiresSignedAppcast || existingEdSignatureAttribute != nil else { return } let resolvedReleaseNotesPath = unresolvedReleaseNotesPath.resolvingSymlinksInPath() let releaseNotesBase64Signature: String let releaseNotesLength: UInt if let (existingReleaseNotesBase64Signature, existingReleaseNotesLength) = signedReleaseNoteFiles[resolvedReleaseNotesPath.path] { releaseNotesBase64Signature = existingReleaseNotesBase64Signature releaseNotesLength = existingReleaseNotesLength } else { var releaseNotesData = try Data(contentsOf: resolvedReleaseNotesPath) if !disableEmbeddedSignWarning { // Update release notes file (except for plain-text ones) to include warning about making // future modifications in the file let pathExtension = resolvedReleaseNotesPath.pathExtension if pathExtension == "html" || pathExtension == "md" || pathExtension == "markdown" { do { if let updatedReleaseNotesData = updateHTMLCommentSigningWarningInReleaseNotes(data: releaseNotesData) { try updatedReleaseNotesData.write(to: resolvedReleaseNotesPath, options: .atomic) print("Updated \(resolvedReleaseNotesPath.lastPathComponent) to include signing warning for making further modifications.") releaseNotesData = updatedReleaseNotesData } } catch { // Fallback to using original release notes print("Warning: failed to update release notes to include signing warning for making further modifications. Skipping updating \(resolvedReleaseNotesPath.path)") } } } releaseNotesBase64Signature = try edSignature(path: resolvedReleaseNotesPath, publicEdKey: publicEdKey, privateEdKey: privateEdKey) releaseNotesLength = UInt(releaseNotesData.count) signedReleaseNoteFiles[resolvedReleaseNotesPath.path] = (releaseNotesBase64Signature, releaseNotesLength) } if let existingEdSignatureAttribute { existingEdSignatureAttribute.stringValue = releaseNotesBase64Signature } else { let signatureAttribute = XMLNode.attribute(withName: SUAppcastAttributeEDSignature, stringValue: releaseNotesBase64Signature) as! XMLNode releaseNotesElement.addAttribute(signatureAttribute) } if let existingLengthAttribute = releaseNotesElement.attribute(forName: SUAppcastAttributeLength) { existingLengthAttribute.stringValue = "\(releaseNotesLength)" } else { let lengthAttribute = XMLNode.attribute(withName: SUAppcastAttributeLength, stringValue: "\(releaseNotesLength)") as! XMLNode releaseNotesElement.addAttribute(lengthAttribute) } } func writeAppcast(appcastDestPath: URL, keys: PrivateKeys, disableEmbeddedSignWarning: Bool, appcast: Appcast, fullReleaseNotesLink: String?, preferToEmbedReleaseNotes: Bool, link: String?, newChannel: String?, newMinimumUpdateVersion: String?, majorVersion: String?, ignoreSkippedUpgradesBelowVersion: String?, phasedRolloutInterval: Int?, criticalUpdateVersion: String?, informationalUpdateVersions: [String]?) throws -> (numNewUpdates: Int, numExistingUpdates: Int, numUpdatesRemoved: Int) { let appBaseName = appcast.inferredAppName let sparkleNS = "http://www.andymatuschak.org/xml-namespaces/sparkle" let appcastWasPreviouslySigned: Bool var doc: XMLDocument do { let options: XMLNode.Options = [ XMLNode.Options.nodeLoadExternalEntitiesNever, XMLNode.Options.nodePreserveCDATA, XMLNode.Options.nodePreserveWhitespace, ] let xmlData = try Data(contentsOf: appcastDestPath) doc = try XMLDocument(data: xmlData, options: options) let contentData = SPUExtractAppcastContent(xmlData, nil, nil) appcastWasPreviouslySigned = contentData.count != xmlData.count } catch { let root = XMLElement(name: "rss") root.addAttribute(XMLNode.attribute(withName: "xmlns:sparkle", stringValue: sparkleNS) as! XMLNode) root.addAttribute(XMLNode.attribute(withName: "version", stringValue: "2.0") as! XMLNode) doc = XMLDocument(rootElement: root) doc.isStandalone = true appcastWasPreviouslySigned = false } var channel: XMLElement let rootNodes = try doc.nodes(forXPath: "/rss") if rootNodes.count != 1 { throw makeError(code: .appcastError, "Weird XML? \(appcastDestPath.path)") } let root = rootNodes[0] as! XMLElement let channelNodes = try root.nodes(forXPath: "channel") var numUpdatesRemoved: Int = 0 if channelNodes.count > 0 { channel = channelNodes[0] as! XMLElement // Enumerate through all existing update items and remove any that we aren't going to keep let versionsInFeedSet = Set(appcast.versionsInFeed) var nodesToRemove: [XMLElement] = [] if let itemNodes = try? channel.nodes(forXPath: "item") { for item in itemNodes { guard let item = item as? XMLElement else { continue } let version: String? if let versionElement = findElement(name: SUAppcastElementVersion, parent: item) { version = versionElement.stringValue } else if let enclosure = findElement(name: "enclosure", parent: item), let versionAttribute = enclosure.attribute(forName: SUAppcastAttributeVersion) { version = versionAttribute.stringValue } else { version = nil } guard let version = version else { continue } if !versionsInFeedSet.contains(version) { nodesToRemove.append(item) } } // Remove old nodes from highest-to-lowest index order for node in nodesToRemove.reversed() { channel.removeChild(at: node.index) } numUpdatesRemoved = nodesToRemove.count } } else { channel = XMLElement(name: "channel") channel.addChild(XMLElement.element(withName: "title", stringValue: appBaseName) as! XMLElement) root.addChild(channel) } // If appcast has not already been signed, see if any of its update archives require signing let requiresSignedAppcast: Bool if appcastWasPreviouslySigned { requiresSignedAppcast = true } else { var requiresSignedAppcastFromArchives = false for version in appcast.versionsInFeed { guard let update = appcast.archives[version] else { continue } if update.requiresSignedAppcast { requiresSignedAppcastFromArchives = true break } } requiresSignedAppcast = requiresSignedAppcastFromArchives } // Keep track of already signed release note files so we avoid // re-signing the same file in case the developer is using symlinks // Path -> [(EdSignature, Length)] var signedReleaseNoteFiles: [String: (String, UInt)] = [:] var numNewUpdates = 0 var numExistingUpdates = 0 let versionComparator = SUStandardVersionComparator() var numItems = 0 for version in appcast.versionsInFeed { guard let update = appcast.archives[version] else { continue } var item: XMLElement var existingItems = try channel.nodes(forXPath: "item[enclosure[@\(SUAppcastAttributeVersion)=\"\(update.version)\"]]") if existingItems.count == 0 { // Fall back to see if any items are using the element version variant existingItems = try channel.nodes(forXPath: "item[\(SUAppcastElementVersion)=\"\(update.version)\"]") } let createNewItem = (existingItems.count == 0) if createNewItem { numNewUpdates += 1 } else { numExistingUpdates += 1 } numItems += 1 if createNewItem { item = XMLElement.element(withName: "item") as! XMLElement // When we insert a new item, find the best place to insert the new update item in // This takes account of existing items and even ones that we don't have existing info on var foundBestUpdateInsertion = false if let itemNodes = try? channel.nodes(forXPath: "item") { for childItemNode in itemNodes { guard let childItemNode = childItemNode as? XMLElement else { continue } guard let childItemVersion = extractVersion(parent: childItemNode) else { continue } if versionComparator.compareVersion(update.version, toVersion: childItemVersion) == .orderedDescending { channel.insertChild(item, at: childItemNode.index) foundBestUpdateInsertion = true break } } } if !foundBestUpdateInsertion { channel.addChild(item) } } else { item = existingItems[0] as! XMLElement } if nil == findElement(name: "title", parent: item) { item.addChild(XMLElement.element(withName: "title", stringValue: update.shortVersion) as! XMLElement) } if nil == findElement(name: "pubDate", parent: item) { item.addChild(XMLElement.element(withName: "pubDate", stringValue: update.pubDate) as! XMLElement) } if createNewItem { // Set link if let link = link, let linkElement = XMLElement.element(withName: SURSSElementLink, uri: sparkleNS) as? XMLElement { linkElement.setChildren([text(link)]) item.addChild(linkElement) } // Set full release notes if let fullReleaseNotesLink = fullReleaseNotesLink, let fullReleaseNotesElement = XMLElement.element(withName: SUAppcastElementFullReleaseNotesLink, uri: sparkleNS) as? XMLElement { fullReleaseNotesElement.setChildren([text(fullReleaseNotesLink)]) item.addChild(fullReleaseNotesElement) } // Set new channel name if let newChannelName = newChannel, let channelNameElement = XMLElement.element(withName: SUAppcastElementChannel, uri: sparkleNS) as? XMLElement { channelNameElement.setChildren([text(newChannelName)]) item.addChild(channelNameElement) } // Set minimum update version if let newMinimumUpdateVersion, let minimumUpdateVersionElement = XMLElement.element(withName: SUAppcastElementMinimumUpdateVersion, uri: sparkleNS) as? XMLElement { minimumUpdateVersionElement.setChildren([text(newMinimumUpdateVersion)]) item.addChild(minimumUpdateVersionElement) } // Set last major version if let minimumAutoupdateVersion = majorVersion, let minimumAutoupdateVersionElement = XMLElement.element(withName: SUAppcastElementMinimumAutoupdateVersion, uri: sparkleNS) as? XMLElement { minimumAutoupdateVersionElement.setChildren([text(minimumAutoupdateVersion)]) item.addChild(minimumAutoupdateVersionElement) } // Set ignore skipped upgrades below version if let ignoreSkippedUpgradesBelowVersion = ignoreSkippedUpgradesBelowVersion, let ignoreSkippedUpgradesBelowVersionElement = XMLElement.element(withName: SUAppcastElementIgnoreSkippedUpgradesBelowVersion, uri: sparkleNS) as? XMLElement { ignoreSkippedUpgradesBelowVersionElement.setChildren([text(ignoreSkippedUpgradesBelowVersion)]) item.addChild(ignoreSkippedUpgradesBelowVersionElement) } // Set phased rollout interval if let phasedRolloutInterval = phasedRolloutInterval, let phasedRolloutIntervalElement = XMLElement.element(withName: SUAppcastElementPhasedRolloutInterval, uri: sparkleNS) as? XMLElement { phasedRolloutIntervalElement.setChildren([text(String(phasedRolloutInterval))]) item.addChild(phasedRolloutIntervalElement) } // Set last critical update version if let criticalUpdateVersion = criticalUpdateVersion, let criticalUpdateElement = XMLElement.element(withName: SUAppcastElementCriticalUpdate, uri: sparkleNS) as? XMLElement { if criticalUpdateVersion.count > 0 { criticalUpdateElement.setAttributesWith([SUAppcastAttributeVersion: criticalUpdateVersion]) } item.addChild(criticalUpdateElement) } // Set informational update versions if let informationalUpdateVersions = informationalUpdateVersions, let informationalUpdateElement = XMLElement.element(withName: SUAppcastElementInformationalUpdate, uri: sparkleNS) as? XMLElement { let versionElements: [XMLElement] = informationalUpdateVersions.compactMap({ informationalUpdateVersion in let element: XMLElement? let informationalVersionText: String if informationalUpdateVersion.hasPrefix("<") { element = XMLElement.element(withName: SUAppcastElementBelowVersion, uri: sparkleNS) as? XMLElement informationalVersionText = String(informationalUpdateVersion.dropFirst()) } else { element = XMLElement.element(withName: SUAppcastElementVersion, uri: sparkleNS) as? XMLElement informationalVersionText = informationalUpdateVersion } element?.setChildren([text(informationalVersionText)]) return element }) informationalUpdateElement.setChildren(versionElements) item.addChild(informationalUpdateElement) } } var versionElement = findElement(name: SUAppcastElementVersion, parent: item) if nil == versionElement { versionElement = XMLElement.element(withName: SUAppcastElementVersion, uri: sparkleNS) as? XMLElement item.addChild(versionElement!) } versionElement?.setChildren([text(update.version)]) var shortVersionElement = findElement(name: SUAppcastElementShortVersionString, parent: item) if nil == shortVersionElement { shortVersionElement = XMLElement.element(withName: SUAppcastElementShortVersionString, uri: sparkleNS) as? XMLElement item.addChild(shortVersionElement!) } shortVersionElement?.setChildren([text(update.shortVersion)]) // Override the minimum system version with the version from the archive, // only if an existing item doesn't specify one let minimumSystemVersion: String var minVer = findElement(name: SUAppcastElementMinimumSystemVersion, parent: item) if let minVer = minVer { minimumSystemVersion = minVer.stringValue ?? update.minimumSystemVersion } else { minVer = XMLElement.element(withName: SUAppcastElementMinimumSystemVersion, uri: sparkleNS) as? XMLElement item.addChild(minVer!) minimumSystemVersion = update.minimumSystemVersion } minVer?.setChildren([text(minimumSystemVersion)]) // Override the hardware requirements with requirements from the archive, // only if an existing item doesn't specify one if let hardwareRequirements = update.hardwareRequirements, findElement(name: SUAppcastElementHardwareRequirements, parent: item) == nil { if let hardwareRequirementsElement = XMLElement.element(withName: SUAppcastElementHardwareRequirements, uri: sparkleNS) as? XMLElement { hardwareRequirementsElement.setChildren([text(hardwareRequirements)]) item.addChild(hardwareRequirementsElement) } } // Look for an existing release notes element let releaseNotesXpath = "\(SUAppcastElementReleaseNotesLink)" let results = ((try? item.nodes(forXPath: releaseNotesXpath)) as? [XMLElement])? .filter { !($0.attributes ?? []) .contains(where: { $0.name == SUXMLLanguage }) } let relElement = results?.first // If an existing item has a release notes item, don't automatically embed release notes even if the user // prefers to embed release notes (we only respect this choice for updates without a release notes item or a new item) let embedReleaseNotesAlways = preferToEmbedReleaseNotes && (relElement == nil) if let (descriptionContents, descriptionFormat) = update.releaseNotesContent(embedReleaseNotesAlways: embedReleaseNotesAlways) { let descElement = findOrCreateElement(name: "description", parent: item) let cdata = XMLNode(kind: .text, options: .nodeIsCDATA) cdata.stringValue = descriptionContents descElement.setChildren([cdata]) if descriptionFormat != "html" { descElement.addAttribute(XMLNode.attribute(withName: SUAppcastAttributeFormat, stringValue: descriptionFormat) as! XMLNode) } } else if let existingDescriptionElement = findElement(name: "description", parent: item) { // The update doesn't include embedded release notes. Remove it. item.removeChild(at: existingDescriptionElement.index) } if let releaseNotesFilePath = update.releaseNotesPath, let url = update.releaseNotesURL(releaseNotesPath: releaseNotesFilePath, embedReleaseNotesAlways: embedReleaseNotesAlways) { // The update includes a valid release notes URL let releaseNotesElement: XMLElement if let existingReleaseNotesElement = relElement { releaseNotesElement = existingReleaseNotesElement } else { releaseNotesElement = XMLElement.element(withName: SUAppcastElementReleaseNotesLink) as! XMLElement item.addChild(releaseNotesElement) } // Update release notes releaseNotesElement.stringValue = url.absoluteString // Sign the release notes try signReleaseNotesIfNeeded(element: releaseNotesElement, filePath: releaseNotesFilePath, signedReleaseNoteFiles: &signedReleaseNoteFiles, requiresSignedAppcast: requiresSignedAppcast, disableEmbeddedSignWarning: disableEmbeddedSignWarning, keys: keys) } else if let childIndex = relElement?.index { // The update doesn't include a release notes URL. Remove it. item.removeChild(at: childIndex) } // Retrieve all existing language nodes for release notes let languageNotesNodes = ((try? item.nodes(forXPath: releaseNotesXpath)) as? [XMLElement])? .map { ($0, $0.attribute(forName: SUXMLLanguage)?.stringValue )} .filter { $0.1 != nil } ?? [] // Remove all language nodes that don't have corresponding release notes file let localizedReleaseNotes = update.localizedReleaseNotes() for (node, language) in languageNotesNodes.reversed() where !localizedReleaseNotes.contains(where: { $0.0 == language }) { item.removeChild(at: node.index) } // Update all existing and insert missing localized release notes nodes for (language, url, releaseNotesFilePath) in localizedReleaseNotes { let localizedReleaseNotesElement: XMLElement if let foundNodeIndex = languageNotesNodes.firstIndex(where: { $0.1 == language }) { localizedReleaseNotesElement = languageNotesNodes[foundNodeIndex].0 localizedReleaseNotesElement.stringValue = url.absoluteString } else { let localizedNode = XMLNode.element( withName: SUAppcastElementReleaseNotesLink, children: [XMLNode.text(withStringValue: url.absoluteString) as! XMLNode], attributes: [XMLNode.attribute(withName: SUXMLLanguage, stringValue: language) as! XMLNode]) as! XMLElement item.addChild(localizedNode) localizedReleaseNotesElement = localizedNode } // Sign the localized release notes try signReleaseNotesIfNeeded(element: localizedReleaseNotesElement, filePath: releaseNotesFilePath, signedReleaseNoteFiles: &signedReleaseNoteFiles, requiresSignedAppcast: requiresSignedAppcast, disableEmbeddedSignWarning: disableEmbeddedSignWarning, keys: keys) } var enclosure = findElement(name: "enclosure", parent: item) if nil == enclosure { enclosure = XMLElement.element(withName: "enclosure") as? XMLElement item.addChild(enclosure!) } guard let archiveURL = update.archiveURL?.absoluteString else { throw makeError(code: .appcastError, "Bad archive name or feed URL") } var attributes = [ XMLNode.attribute(withName: "url", stringValue: archiveURL) as! XMLNode, XMLNode.attribute(withName: "length", stringValue: String(update.fileSize)) as! XMLNode, XMLNode.attribute(withName: "type", stringValue: update.mimeType) as! XMLNode, ] if let sig = update.edSignature { attributes.append(XMLNode.attribute(withName: SUAppcastAttributeEDSignature, uri: sparkleNS, stringValue: sig) as! XMLNode) } #if GENERATE_APPCAST_BUILD_LEGACY_DSA_SUPPORT if let sig = update.dsaSignature { attributes.append(XMLNode.attribute(withName: SUAppcastAttributeDSASignature, uri: sparkleNS, stringValue: sig) as! XMLNode) } #endif enclosure!.attributes = attributes if update.deltas.count > 0 { var deltas = findElement(name: SUAppcastElementDeltas, parent: item) if nil == deltas { deltas = XMLElement.element(withName: SUAppcastElementDeltas, uri: sparkleNS) as? XMLElement item.addChild(deltas!) } else { deltas!.setChildren([]) } for delta in update.deltas { var attributes = [ XMLNode.attribute(withName: "url", stringValue: URL(string: delta.archivePath.lastPathComponent.addingPercentEncoding(withAllowedCharacters: CharacterSet.urlPathAllowed)!, relativeTo: update.archiveURL)!.absoluteString) as! XMLNode, XMLNode.attribute(withName: SUAppcastAttributeDeltaFrom, uri: sparkleNS, stringValue: delta.fromVersion) as! XMLNode, XMLNode.attribute(withName: "length", stringValue: String(delta.fileSize)) as! XMLNode, XMLNode.attribute(withName: "type", stringValue: "application/octet-stream") as! XMLNode, ] if let sparkleExecutableFileSize = delta.sparkleExecutableFileSize { attributes.append(XMLNode.attribute(withName: SUAppcastAttributeDeltaFromSparkleExecutableSize, stringValue: String(sparkleExecutableFileSize)) as! XMLNode) } if let sparkleLocales = delta.sparkleLocales { attributes.append(XMLNode.attribute(withName: SUAppcastAttributeDeltaFromSparkleLocales, stringValue: sparkleLocales) as! XMLNode) } if let sig = delta.edSignature { attributes.append(XMLNode.attribute(withName: SUAppcastAttributeEDSignature, uri: sparkleNS, stringValue: sig) as! XMLNode) } #if GENERATE_APPCAST_BUILD_LEGACY_DSA_SUPPORT if let sig = delta.dsaSignature { attributes.append(XMLNode.attribute(withName: SUAppcastAttributeDSASignature, uri: sparkleNS, stringValue: sig) as! XMLNode) } #endif deltas!.addChild(XMLNode.element(withName: "enclosure", children: nil, attributes: attributes) as! XMLElement) } } } let options: XMLNode.Options = [.nodeCompactEmptyElement, .nodePrettyPrint] let unsignedDocData = doc.xmlData(options: options) // Sign the appcast if needed let finalDocData: Data if requiresSignedAppcast, let publicKey = keys.publicEdKey, let privateKey = keys.privateEdKey { let contentData = SPUExtractAppcastContent(unsignedDocData, nil, nil) let dataToSign = disableEmbeddedSignWarning ? contentData : addSignWarningToAppcast(data: contentData) finalDocData = try signAppcast(data: dataToSign, publicEdKey: publicKey, privateEdKey: privateKey) } else { finalDocData = unsignedDocData } _ = try XMLDocument(data: finalDocData, options: XMLNode.Options()); // Verify that it was generated correctly, which does not always happen! try finalDocData.write(to: appcastDestPath) return (numNewUpdates, numExistingUpdates, numUpdatesRemoved) } ================================================ FILE: generate_appcast/URL+Hashing.swift ================================================ // // URL+Hashing.swift // generate_appcast // // Created by Nate Weaver on 2020-05-01. // Copyright © 2020 Sparkle Project. All rights reserved. // import Foundation import CommonCrypto extension FileHandle { /// Calculate the SHA-256 hash of the file referenced by the file handle. /// /// - Returns: The SHA-256 hash of the file (as a hexadecimal string). func sha256String() -> String { // This uses CommonCrypto instead of CryptoKit so it can work on macOS < 10.15 var context = CC_SHA256_CTX() CC_SHA256_Init(&context) while true { let data = self.readData(ofLength: 65_536) guard data.count > 0 else { break } _ = data.withUnsafeBytes { CC_SHA256_Update(&context, $0.baseAddress, numericCast($0.count)) } } let hash = UnsafeMutableBufferPointer.allocate(capacity: Int(CC_SHA256_DIGEST_LENGTH)) CC_SHA256_Final(hash.baseAddress, &context) defer { hash.deallocate() } return hash.reduce("") { $0 + String(format: "%02x", $1) } } } extension URL { /// Calculates the SHA-256 hash of the file referenced by the URL. /// /// - Returns: The SHA-256 hash of the file (as a hexadecimal string), or `nil` if /// the URL doesn't point to a file. func sha256String() -> String? { guard self.isFileURL else { return nil } guard let filehandle = try? FileHandle(forReadingFrom: self) else { return nil } return filehandle.sha256String() } } ================================================ FILE: generate_appcast/Unarchive.swift ================================================ // // Created by Kornel on 22/12/2016. // Copyright © 2016 Sparkle Project. All rights reserved. // import Foundation func unarchive(itemPath: URL, archiveDestDir: URL, callback: @escaping (Error?) -> Void) { let fileManager = FileManager.default let tempDir = archiveDestDir.appendingPathExtension("tmp") _ = try? fileManager.removeItem(at: tempDir) _ = try? fileManager.createDirectory(at: tempDir, withIntermediateDirectories: true, attributes: [:]) if let unarchiver = SUUnarchiver.unarchiver(forPath: itemPath.path, extractionDirectory: tempDir.path, updatingHostBundlePath: nil, decryptionPassword: nil, expectingInstallationType: SPUInstallationTypeApplication) { unarchiver.unarchive(completionBlock: { (error: Error?) in if error != nil { callback(error) return } do { try fileManager.moveItem(at: tempDir, to: archiveDestDir) callback(nil) } catch { callback(error) } }, progressBlock: nil, waitForCleanup: true) } else { callback(makeError(code: .unarchivingError, "Not a supported archive format: \(itemPath.path)")) } } func unarchiveUpdates(archivesSourceDir: URL, archivesDestDir: URL, disableNestedCodeCheck: Bool, verbose: Bool) throws -> [ArchiveItem] { if verbose { print("Unarchiving to temp directory", archivesDestDir.path) } let group = DispatchGroup() let fileManager = FileManager.default // Create a dictionary of archive destination directories -> archive source path // so we can ignore duplicate archive entries before trying to unarchive archives in parallel var fileEntries: [URL: URL] = [:] let dir = try fileManager.contentsOfDirectory(atPath: archivesSourceDir.path) for item in dir { if item.hasPrefix(".") { continue } let itemURL = archivesSourceDir.appendingPathComponent(item) let fileExtension = itemURL.pathExtension // Note: keep this list in sync with SUPipedUnarchiver guard ["zip", "tar", "gz", "tgz", "bz2", "tbz", "xz", "txz", "lzma", "dmg", "aar", "yaa"].contains(fileExtension) else { continue } let itemPath = archivesSourceDir.appendingPathComponent(item) // Ignore directories var isDir: ObjCBool = false if fileManager.fileExists(atPath: itemPath.path, isDirectory: &isDir) && isDir.boolValue { continue } let archiveDestDir: URL if let hash = itemPath.sha256String() { archiveDestDir = archivesDestDir.appendingPathComponent(hash) } else { archiveDestDir = archivesDestDir.appendingPathComponent(itemPath.lastPathComponent) } // Ignore duplicate archives if let existingItemPath = fileEntries[archiveDestDir] { throw makeError(code: .appcastError, "Duplicate update archives are not supported. Found '\(existingItemPath.lastPathComponent)' and '\(itemPath.lastPathComponent)'. Please remove one of them from the appcast generation directory.") } fileEntries[archiveDestDir] = itemPath } var unarchived: [String: ArchiveItem] = [:] var updateParseError: Error? = nil var running = 0 for (archiveDestDir, itemPath) in fileEntries { let addItem = { (validateBundle: Bool) in do { let item = try ArchiveItem(fromArchive: itemPath, unarchivedDir: archiveDestDir, validateBundle: validateBundle, disableNestedCodeCheck: disableNestedCodeCheck) if verbose { print("Found archive", item) } objc_sync_enter(unarchived) // Make sure different archives don't contain the same update too if let existingArchive = unarchived[item.version] { updateParseError = makeError(code: .appcastError, "Duplicate updates are not supported. Found archives '\(existingArchive.archivePath.lastPathComponent)' and '\(itemPath.lastPathComponent)' which contain the same bundle version. Please remove one of these archives from the appcast generation directory.") } else { unarchived[item.version] = item } objc_sync_exit(unarchived) } catch { print("Skipped", itemPath.lastPathComponent, error) } } if fileManager.fileExists(atPath: archiveDestDir.path) { addItem(false) } else { group.enter() unarchive(itemPath: itemPath, archiveDestDir: archiveDestDir) { (error: Error?) in if let error = error { print("Could not unarchive", itemPath.path, error) } else { addItem(true) } group.leave() } } // Crude limit of concurrency running += 1 if running >= 8 { running = 0 group.wait() } } group.wait() if let updateParseError = updateParseError { throw updateParseError } return Array(unarchived.values) } ================================================ FILE: generate_appcast/main.swift ================================================ // // main.swift // Appcast // // Created by Kornel on 20/12/2016. // Copyright © 2016 Sparkle Project. All rights reserved. // import Foundation import ArgumentParser func loadPrivateKeys(_ account: String, _ privateDSAKey: SecKey?, _ privateEdString: String?, allowNewPrivateKey: Bool) -> PrivateKeys? { var privateEdKey: Data? var publicEdKey: Data? var item: CFTypeRef? var secret: Data? // private + public key is provided as argument (for older format) if let privateEdString { if let data = Data(base64Encoded: privateEdString) { if allowNewPrivateKey || secretUsesOldHashedSeed(secret: data) { // We always allow the old format without private seed secret = data } else if !allowNewPrivateKey && secretUsesRegularSeed(secret: data) { // Don't support deprecated option for newly generated keys print("Error: specifying private key as the argument is no longer supported.") return nil } else { print("Error: Private key not decoded from the argument, which has \(data.count) bytes. Please provide a valid key and confirm the contents of the key are correct.") return nil } } else { print("Error: Private key not decoded from the argument because it isn't base64 encoded. Please provide a valid key and confirm the contents of the key are correct.") return nil } } // get keys from kechain instead else { let res = SecItemCopyMatching([ kSecClass as String: kSecClassGenericPassword, kSecAttrService as String: "https://sparkle-project.org", kSecAttrAccount as String: account, kSecAttrProtocol as String: kSecAttrProtocolSSH, kSecReturnData as String: kCFBooleanTrue!, ] as CFDictionary, &item) if res == errSecSuccess, let encoded = item as? Data { if let data = Data(base64Encoded: encoded) { secret = data } else { print("Error: Failed to base64 decode secret data from keychain") return nil } } else { print("Warning: Private key for account \(account) not found in the Keychain (\(res)). Please run the generate_keys tool") } } if let secret { guard let (privateKey, publicKey) = decodePrivateAndPublicKeys(secret: secret) else { print("Error: Failed to decode private and public keys from secret data") return nil } privateEdKey = privateKey publicEdKey = publicKey } return PrivateKeys(privateDSAKey: privateDSAKey, privateEdKey: privateEdKey, publicEdKey: publicEdKey) } let DEFAULT_MAX_CDATA_THRESHOLD = 1000 struct GenerateAppcast: ParsableCommand { static let programName = "generate_appcast" static let programNamePath: String = CommandLine.arguments.first ?? "./\(programName)" static let cacheDirectory = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask)[0].appendingPathComponent("Sparkle_generate_appcast") static let oldFilesDirectoryName = "old_updates" static let DEFAULT_MAX_VERSIONS_PER_BRANCH_IN_FEED = 3 static let DEFAULT_MAXIMUM_DELTAS = 5 @Option(help: ArgumentHelp("The account name in your keychain associated with your private EdDSA (ed25519) key to use for signing new updates.")) var account : String = "ed25519" @Option(name: .customLong("ed-key-file"), help: ArgumentHelp("Path to the private EdDSA key file. If not specified, the private EdDSA key will be read from the Keychain instead. '-' can be used to echo the EdDSA key from a 'secret' environment variable to the standard input stream. For example: echo \"$PRIVATE_KEY_SECRET\" | ./\(programName) --ed-key-file -", valueName: "private-EdDSA-key-file")) var privateEdKeyPath: String? @Flag(name: .long, help: ArgumentHelp("Disables adding a warning to signed appcast and release note files explaining that further modifications will require re-signing them. This flag has no effect if the files already have a signing warning embedded.")) var disableSigningWarning: Bool = false #if GENERATE_APPCAST_BUILD_LEGACY_DSA_SUPPORT @Option(name: .customShort("f"), help: ArgumentHelp("Path to the private DSA key file. Only use this option for transitioning to EdDSA from older updates.", valueName: "private-dsa-key-file"), transform: { URL(fileURLWithPath: $0) }) var privateDSAKeyURL: URL? @Option(name: .customShort("n"), help: ArgumentHelp("The name of the private DSA key. This option must be used together with `-k`. Only use this option for transitioning to EdDSA from older updates.", valueName: "dsa-key-name")) var privateDSAKeyName: String? #endif @Option(name: .customShort("s"), help: ArgumentHelp("(DEPRECATED): The private EdDSA string (128 characters). This option is deprecated. Please use the Keychain, or pass the key as standard input when using the --ed-key-file - option instead. This option is no longer supported for newly generated keys.", valueName: "private-EdDSA-key")) var privateEdString : String? #if GENERATE_APPCAST_BUILD_LEGACY_DSA_SUPPORT @Option(name: .customShort("k"), help: ArgumentHelp("The path to the keychain to look up the private DSA key. This option must be used together with `-n`. Only use this option for transitioning to EdDSA from older updates.", valueName: "keychain-for-dsa"), transform: { URL(fileURLWithPath: $0) }) var keychainURL: URL? #endif @Option(name: .customLong("download-url-prefix"), help: ArgumentHelp("A URL that will be used as prefix for the URL from where updates will be downloaded.", valueName: "url"), transform: { URL(string: $0) }) var downloadURLPrefix : URL? @Option(name: .customLong("release-notes-url-prefix"), help: ArgumentHelp("A URL that will be used as prefix for constructing URLs for release notes.", valueName: "url"), transform: { URL(string: $0) }) var releaseNotesURLPrefix : URL? @Flag(name: .customLong("embed-release-notes"), help: ArgumentHelp("Embed release notes in a new update's description. By default, release note files are only embedded if they are HTML and do not include DOCTYPE or body tags. This flag forces release note files for newly created updates to always be embedded.")) var embedReleaseNotes : Bool = false @Option(name: .customLong("full-release-notes-url"), help: ArgumentHelp("A URL that will be used for the full release notes.", valueName: "url")) var fullReleaseNotesURL: String? @Option(name: .long, help: ArgumentHelp("A URL to the application's website which Sparkle may use for directing users to if they cannot download a new update from within the application. This will be used for new generated update items. By default, no product link is used.", valueName: "link")) var link: String? @Option(name: .long, help: ArgumentHelp("An optional comma delimited list of application versions (specified by CFBundleVersion) to generate new update items for. By default, new update items are inferred from the available archives and current feed. Use this option if you need to insert only a specific new version or insert an old update in the feed at a different branch point (e.g. with a different minimum OS version or channel).", valueName: "versions"), transform: { Set($0.components(separatedBy: ",")) }) var versions: Set? @Option(name: .customLong("maximum-versions"), help: ArgumentHelp("The maximum number of versions to preserve in the generated appcast for each branch point (e.g. with a different minimum OS requirement). If this value is 0, then all items in the appcast are preserved.", valueName: "maximum-versions"), transform: { value -> Int in if let intValue = Int(value) { return (intValue <= 0) ? Int.max : intValue } else { return DEFAULT_MAX_VERSIONS_PER_BRANCH_IN_FEED } }) var maxVersionsPerBranchInFeed: Int = DEFAULT_MAX_VERSIONS_PER_BRANCH_IN_FEED @Option(name: .long, help: ArgumentHelp("The maximum number of delta items to create for the latest update for each branch point (e.g. with a different minimum OS requirement).", valueName: "maximum-deltas")) var maximumDeltas: Int = DEFAULT_MAXIMUM_DELTAS @Option(name: .long, help: ArgumentHelp(COMPRESSION_METHOD_ARGUMENT_DESCRIPTION, valueName: "delta-compression")) var deltaCompression: String = "default" @Option(name: .long, help: .hidden) var deltaCompressionLevel: UInt8 = 0 @Option(name: .long, help: ArgumentHelp("The Sparkle channel name that will be used for generating new updates. By default, no channel is used. Old applications need to be using Sparkle 2 to use this feature.", valueName: "channel-name")) var channel: String? @Option(name: .long, help: ArgumentHelp("The minimum update version requirement that will be used for generating new updates. By default, no minimum update version is used.", valueName: "minimum-update-version")) var minimumUpdateVersion: String? @Option(name: .long, help: ArgumentHelp("The last major or minimum autoupdate sparkle:version that will be used for generating new updates. By default, no last major version is used.", valueName: "major-version")) var majorVersion: String? @Option(name: .long, help: ArgumentHelp("Ignore skipped major upgrades below this specified version. Only applicable for major upgrades.", valueName: "below-version")) var ignoreSkippedUpgradesBelowVersion: String? @Option(name: .long, help: ArgumentHelp("The phased rollout interval in seconds that will be used for generating new updates. By default, no phased rollout interval is used.", valueName: "phased-rollout-interval"), transform: { Int($0) }) var phasedRolloutInterval: Int? @Option(name: .long, help: ArgumentHelp("The last critical update sparkle:version that will be used for generating new updates. An empty string argument will treat this update as critical coming from any application version. By default, no last critical update version is used. Old applications need to be using Sparkle 2 to use this feature.", valueName: "critical-update-version")) var criticalUpdateVersion: String? @Option(name: .long, help: ArgumentHelp("A comma delimited list of application sparkle:version's that will see newly generated updates as being informational only. An empty string argument will treat this update as informational coming from any application version. Prefix a version string with '<' to indicate (eg \"<2.5\") to indicate older versions than the one specified should treat the update as informational only. By default, updates are not informational only. --link must also be provided. Old applications need to be using Sparkle 2 to use this feature, and 2.1 or later to use the '<' upper bound feature.", valueName: "informational-update-versions"), transform: { $0.components(separatedBy: ",").filter({$0.count > 0}) }) var informationalUpdateVersions: [String]? @Flag(name: .customLong("auto-prune-update-files"), help: ArgumentHelp("Automatically remove old update files in \(oldFilesDirectoryName) that haven't been touched in 2 weeks")) var autoPruneUpdates: Bool = false @Option(name: .customShort("o"), help: ArgumentHelp("Path to filename for the generated appcast (allowed when only one will be created).", valueName: "output-path"), transform: { URL(fileURLWithPath: $0) }) var outputPathURL: URL? @Argument(help: "The path to the directory containing the update archives and delta files.", transform: { URL(fileURLWithPath: $0, isDirectory: true) }) var archivesSourceDir: URL @Flag(help: .hidden) var verbose: Bool = false @Flag(name: .customLong("disable-nested-code-check"), help: .hidden) var disableNestedCodeCheck: Bool = false static var configuration = CommandConfiguration( abstract: "Generate appcast from a directory of Sparkle update archives.", discussion: """ Appcast files and deltas will be written to the archives directory. If an appcast file is already present in the archives directory, that file will be re-used and updated with new entries. Otherwise, a new appcast file will be generated and written. Old updates are automatically removed from the generated appcast feed and their update files are moved to \(oldFilesDirectoryName)/ If --auto-prune-update-files is passed, old update files in this directory are deleted after 2 weeks. You may want to exclude files from this directory from being uploaded. Use the --versions option if you need to insert an update that is older than the latest update in your feed, or if you need to insert only a specific new version with certain parameters. .html, .md, or .txt files that have the same filename as an archive (except for the file extension) will be used for release notes for that item. For HTML release notes, if the contents of these files do not include a DOCTYPE or body tags, they will be treated as embedded CDATA release notes. Release notes for new items can be forced to be embedded by passing --embed-release-notes If appcast signing is required by any of the updates, the appcast and release note files may be updated for signing. If you make any manual modifications to the appcast or release note files, please re-run \(programName) to ensure all signatures are updated. For new update entries, Sparkle infers the minimum system OS and hardware requirements based on your update's bundle (e.g. via inspecting LSMinimumSystemVersion or detecting which architecture slices are available). An example of an archives directory may look like: ./my-app-release-zipfiles/ MyApp 1.0.zip MyApp 1.0.html MyApp 1.1.dmg MyApp 1.1.md appcast.xml \(oldFilesDirectoryName)/ EXAMPLES: \(programNamePath) ./my-app-release-dmg-files/ \(programNamePath) -o appcast-name.xml ./my-app-release-dmg-files/ For more advanced options that can be used for publishing updates, see https://sparkle-project.org/documentation/publishing/ for further documentation. Extracted archives that are needed are cached in \((cacheDirectory.path as NSString).abbreviatingWithTildeInPath) to avoid re-computation in subsequent runs. Note that \(programName) does not support package-based (.pkg) updates. """) func validate() throws { #if GENERATE_APPCAST_BUILD_LEGACY_DSA_SUPPORT guard (keychainURL == nil) == (privateDSAKeyName == nil) else { throw ValidationError("Both -n and -k options must be provided together, or neither should be provided.") } // Both keychain/dsa key name options, and private dsa key file options cannot coexist guard (keychainURL == nil) || (privateDSAKeyURL == nil) else { throw ValidationError("-f cannot be provided if -n and -k is provided") } #endif guard (privateEdKeyPath == nil) || (privateEdString == nil) else { throw ValidationError("--ed-key-file cannot be provided if -s is provided") } if let versions = versions { guard versions.count > 0 else { throw ValidationError("--versions must specify at least one application version.") } } guard (informationalUpdateVersions == nil) || (link != nil) else { throw ValidationError("--link must be specified if --informational-update-versions is specified.") } guard deltaCompressionLevel >= 0 && deltaCompressionLevel <= 9 else { throw ValidationError("Invalid --delta-compression-level value was passed.") } var validCompression: ObjCBool = false let _ = deltaCompressionModeFromDescription(deltaCompression, &validCompression) if !validCompression.boolValue { throw ValidationError("Invalid --delta-compression \(deltaCompression) was passed.") } } func run() throws { // Ignore SIGPIPE because we won't want read or write failures due to broken pipe to unexpectably // terminate the process, when extracting archives signal(SIGPIPE, SIG_IGN) // Extract the keys let privateDSAKey : SecKey? #if GENERATE_APPCAST_BUILD_LEGACY_DSA_SUPPORT if let privateDSAKeyURL = privateDSAKeyURL { do { privateDSAKey = try loadPrivateDSAKey(at: privateDSAKeyURL) } catch { print("Unable to load DSA private key from", privateDSAKeyURL.path, "\n", error) throw ExitCode(1) } } else if let keychainURL = keychainURL, let privateDSAKeyName = privateDSAKeyName { do { privateDSAKey = try loadPrivateDSAKey(named: privateDSAKeyName, fromKeychainAt: keychainURL) } catch { print("Unable to load DSA private key '\(privateDSAKeyName)' from keychain at", keychainURL.path, "\n", error) throw ExitCode(1) } } else { privateDSAKey = nil } #else privateDSAKey = nil #endif let allowNewPrivateKey: Bool let privateEdKeyString: String? if let privateEdString = privateEdString { privateEdKeyString = privateEdString print("Warning: The -s option for passing the private EdDSA key is insecure and deprecated. Please see its help usage for more information.") allowNewPrivateKey = false } else if let privateEdKeyPath = privateEdKeyPath { do { if privateEdKeyPath == "-" && !FileManager.default.fileExists(atPath: privateEdKeyPath) { if let line = readLine(strippingNewline: true) { privateEdKeyString = line } else { print("Unable to read EdDSA private key from standard input") throw ExitCode(1) } } else { do { privateEdKeyString = try decodeSecretString(filePath: privateEdKeyPath) } catch { print(error.localizedDescription) throw ExitCode(1) } } allowNewPrivateKey = true } catch { print("Unable to load EdDSA private key from", privateEdKeyPath, "\n", error) throw ExitCode(1) } } else { privateEdKeyString = nil allowNewPrivateKey = true } guard let keys = loadPrivateKeys(account, privateDSAKey, privateEdKeyString, allowNewPrivateKey: allowNewPrivateKey) else { throw ExitCode(1) } do { let appcastsByFeed = try makeAppcasts(archivesSourceDir: archivesSourceDir, outputPathURL: outputPathURL, cacheDirectory: GenerateAppcast.cacheDirectory, keys: keys, versions: versions, maxVersionsPerBranchInFeed: maxVersionsPerBranchInFeed, newChannel: channel, newMinimumUpdateVersion: minimumUpdateVersion, majorVersion: majorVersion, maximumDeltas: maximumDeltas, deltaCompressionModeDescription: deltaCompression, deltaCompressionLevel: deltaCompressionLevel, disableNestedCodeCheck: disableNestedCodeCheck, downloadURLPrefix: downloadURLPrefix, releaseNotesURLPrefix: releaseNotesURLPrefix, verbose: verbose) let oldFilesDirectory = archivesSourceDir.appendingPathComponent(GenerateAppcast.oldFilesDirectoryName) let pluralizeWord = { $0 == 1 ? $1 : "\($1)s" } for (appcastFile, appcast) in appcastsByFeed { // If an output filename was specified, use it. // Otherwise, use the name of the appcast file found in the archive. let appcastDestPath = outputPathURL ?? URL(fileURLWithPath: appcastFile, relativeTo: archivesSourceDir) // Write the appcast let (numNewUpdates, numExistingUpdates, numUpdatesRemoved) = try writeAppcast(appcastDestPath: appcastDestPath, keys: keys, disableEmbeddedSignWarning: disableSigningWarning, appcast: appcast, fullReleaseNotesLink: fullReleaseNotesURL, preferToEmbedReleaseNotes: embedReleaseNotes, link: link, newChannel: channel, newMinimumUpdateVersion: minimumUpdateVersion, majorVersion: majorVersion, ignoreSkippedUpgradesBelowVersion: ignoreSkippedUpgradesBelowVersion, phasedRolloutInterval: phasedRolloutInterval, criticalUpdateVersion: criticalUpdateVersion, informationalUpdateVersions: informationalUpdateVersions) // Inform the user, pluralizing "update" if necessary let pluralizeUpdates = { pluralizeWord($0, "update") } let newUpdatesString = pluralizeUpdates(numNewUpdates) let existingUpdatesString = pluralizeUpdates(numExistingUpdates) let removedUpdatesString = pluralizeUpdates(numUpdatesRemoved) print("Wrote \(numNewUpdates) new \(newUpdatesString), updated \(numExistingUpdates) existing \(existingUpdatesString), and removed \(numUpdatesRemoved) old \(removedUpdatesString) in \(appcastFile)") } let (moveCount, prunedCount) = moveOldUpdatesFromAppcasts(archivesSourceDir: archivesSourceDir, oldFilesDirectory: oldFilesDirectory, cacheDirectory: GenerateAppcast.cacheDirectory, appcasts: Array(appcastsByFeed.values), autoPruneUpdates: autoPruneUpdates) if moveCount > 0 { print("Moved \(moveCount) old update \(pluralizeWord(moveCount, "file")) to \(oldFilesDirectory.lastPathComponent)") } if prunedCount > 0 { print("Pruned \(prunedCount) old update \(pluralizeWord(prunedCount, "file"))") } } catch { print("Error generating appcast from directory", archivesSourceDir.path, "\n", error) throw ExitCode(1) } } } DispatchQueue.global().async(execute: { GenerateAppcast.main() CFRunLoopStop(CFRunLoopGetMain()) }) CFRunLoopRun() ================================================ FILE: generate_keys/Bridging-Header.h ================================================ #import #import "SUConstants.h" #import "SUErrors.h" #import "SUSignatures.h" #import "ed25519.h" ================================================ FILE: generate_keys/main.swift ================================================ // // main.swift // generate_keys // // Created by Kornel on 15/09/2018. // Copyright © 2018 Sparkle Project. All rights reserved. // import Foundation import Security import ArgumentParser let PRIVATE_KEY_LABEL = "Private key for signing Sparkle updates" private func commonKeychainItemAttributes(account: String) -> [String: Any] { /// Attributes used for both adding a new item and matching an existing one. return [ /// The type of the item (a generic password). kSecClass as String: kSecClassGenericPassword as String, /// The service string for the item (the Sparkle homepage URL). kSecAttrService as String: "https://sparkle-project.org", /// The account name for the item (in this case, the key type). kSecAttrAccount as String: account, /// The protocol used by the service (not actually used, so we claim SSH). kSecAttrProtocol as String: kSecAttrProtocolSSH as String, ] } private func failure(_ message: String) -> Never { /// Checking for both `TERM` and `isatty()` correctly detects Xcode. if ProcessInfo.processInfo.environment["TERM"] != nil && isatty(STDOUT_FILENO) != 0 { print("\u{001b}[1;91mERROR:\u{001b}[0m ", terminator: "") } else { print("ERROR: ", terminator: "") } print(message) exit(1) } func findSecret(account: String) -> Data? { var item: CFTypeRef? let res = SecItemCopyMatching(commonKeychainItemAttributes(account: account).merging([ /// Return a matched item's value as a CFData object. kSecReturnData as String: kCFBooleanTrue!, ], uniquingKeysWith: { $1 }) as CFDictionary, &item) switch res { case errSecSuccess: if let secret = (item as? Data).flatMap({ Data(base64Encoded: $0) }) { return secret } else { failure(""" Item found, but is corrupt or has been overwritten! Please delete the existing item from the keychain and try again. """) } case errSecItemNotFound: return nil case errSecAuthFailed: failure(""" Access denied. Can't check existing keys in the keychain. Go to Keychain Access.app, lock the login keychain, then unlock it again. """) case errSecUserCanceled: failure(""" User canceled the authorization request. To retry, run this tool again. """) case errSecInteractionNotAllowed: failure(""" The operating system has blocked access to the Keychain. You may be trying to run this command from a script over SSH, which is not supported. """) case let res: print(""" Unable to access an existing item in the Keychain due to an unknown error: \(res). You can look up this error at """) // Note: Don't bother percent-encoding `res`, it's always an integer value and will not need escaping. } exit(1) } func generatePublicKeyAndSeed() -> (publicEdKey: Data, seed: Data) { var seed = Array(repeating: 0, count: 32) var publicEdKey = Array(repeating: 0, count: 32) var privateEdKey = Array(repeating: 0, count: 64) guard ed25519_create_seed(&seed) == 0 else { failure("Unable to initialize random seed. Try restarting your computer.") } ed25519_create_keypair(&publicEdKey, &privateEdKey, seed) return (Data(publicEdKey), Data(seed)) } func storeSecret(account: String, publicEdKey: Data, secret: Data) { let query = commonKeychainItemAttributes(account: account).merging([ /// Mark the new item as sensitive (requires keychain password to export - e.g. a private key). kSecAttrIsSensitive as String: kCFBooleanTrue!, /// Mark the new item as permanent (supposedly, "stored in the keychain when created", but not actually /// used for generic passwords - we set it anyway for good measure). kSecAttrIsPermanent as String: kCFBooleanTrue!, /// The label of the new item (shown as its name/title in Keychain Access). kSecAttrLabel as String: PRIVATE_KEY_LABEL, /// A comment regarding the item's content (can be viewed in Keychain Access; we give the public key here). kSecAttrComment as String: "Public key (SUPublicEDKey value) for this key is:\n\n\(Data(publicEdKey).base64EncodedString())", /// A short description of the item's contents (shown as "kind" in Keychain Access"). kSecAttrDescription as String: "private key", /// The actual data content of the new item. kSecValueData as String: secret.base64EncodedData() as CFData ], uniquingKeysWith: { $1 }) as CFDictionary switch SecItemAdd(query, nil) { case errSecSuccess: break case errSecDuplicateItem: failure("You already have a conflicting key in your Keychain which was not found during lookup.") case errSecAuthFailed: failure(""" System denied access to the Keychain. Unable to save the new key. Go to Keychain Access.app, lock the login keychain, then unlock it again. """) case let res: failure(""" The key could not be saved to the Keychain due to an unknown error: \(res). You can look up this error at """) } } func printNewPublicKeyUsage(_ publicKey: Data) { print(""" A key has been generated and saved in your keychain. Add the `SUPublicEDKey` key to the Info.plist of each app for which you intend to use Sparkle for distributing updates. It should appear like this: SUPublicEDKey \(publicKey.base64EncodedString()) """) } struct GenerateKeys: ParsableCommand { @Option(help: ArgumentHelp("The account name to use when generating or looking up keys from your keychain. If this is not specified, a default global account is used instead. We recommend using different accounts for different organizations.")) var account: String = "ed25519" @Flag(name: .customShort("p"), help: ArgumentHelp("Looks up and just prints the existing public key stored in the Keychain.")) var lookUpPublicKey: Bool = false @Option(name: .customShort("x"), help: ArgumentHelp("Exports your private key from your login keychain and writes it to private-key-file. Note the contents of this sensitive exported file are the same as the password to the \"\(PRIVATE_KEY_LABEL)\" item in your keychain. For advanced usage if the private key is generated in the new format (i.e. the key file after base64 decoding is 32 bytes), then the exported key file is the base64 encoding of the private seed. The seed can be used to create the private/public keypair with other tools that support EdDSA signing.", valueName: "private-key-file")) var exportedPrivateKeyFile: String? @Option(name: .customShort("f"), help: ArgumentHelp("Imports the private key from private-key-file into your keychain instead of generating a new key. This file has likely been exported via -x option from another machine. Any existing \"\(PRIVATE_KEY_LABEL)\" items listed in Keychain Access may need to be removed manually first before proceeding.", valueName: "private-key-file")) var importedPrivateKeyFile: String? static var configuration: CommandConfiguration = CommandConfiguration( abstract: "Generate public & private keys for signing Sparkle based app updates.", discussion: """ This tool generates a public & private keys and uses the macOS Keychain to store the private key for signing app updates which will be distributed via Sparkle. This key will be associated with your user account. Note: You only need one signing key, no matter how many apps you embed Sparkle in. The keychain may ask permission for this tool to access an existing key, if one exists, or for permission to save the new key. You must allow access in order to successfully proceed. In the default mode ran without any arguments, the public key and how it should be used in your application's Info.plist will be printed. If a private key was already generated in your Keychain, that key will be used and not overridden. You may additionally use options to only look up the existing public key for automation (-p), export the private key from your Keychain to a file for transferring the key (-x), or import the private key into your Keychain from a file (-f). """) func validate() throws { if lookUpPublicKey { guard exportedPrivateKeyFile == nil && importedPrivateKeyFile == nil else { throw ValidationError("-p option cannot be provided together with -x or -f.") } } else { guard exportedPrivateKeyFile == nil || importedPrivateKeyFile == nil else { throw ValidationError("Both -x and -f options cannot be provided together.") } } } func run() throws { if lookUpPublicKey { /// Lookup mode - print just the pubkey and exit if let secret = findSecret(account: account) { guard let (_, pubKey) = decodePrivateAndPublicKeys(secret: secret) else { failure("Stored private key must be 32 or 96 bytes (for the older format) decoded. Instead it is \(secret.count) bytes decoded.") } print(pubKey.base64EncodedString()) } else { failure("No existing signing key found!") } } else if let exportedPrivateKeyFile = exportedPrivateKeyFile { /// Export mode - export the key-pair file from the user's keychain let exportURL = URL(fileURLWithPath: exportedPrivateKeyFile) if let reachable = try? exportURL.checkResourceIsReachable(), reachable { failure("private-key-file already exists: \(exportURL.path)") } guard let secret = findSecret(account: account) else { failure("No existing signing key found!") } do { try secret.base64EncodedString().write(to: exportURL, atomically: true, encoding: .utf8) } catch { failure("Failed to write exported file: \(error)") } } else if let importedPrivateKeyFile = importedPrivateKeyFile { /// Import mode - import the specified key-pair file let secretBase64File = importedPrivateKeyFile let secretBase64: String do { secretBase64 = try decodeSecretString(filePath: secretBase64File) } catch { failure("Failed to read private-key-file: \(error.localizedDescription)") } guard let secret = Data(base64Encoded: secretBase64, options: .init()) else { failure("Failed to decode base64 encoded key data from: \(secretBase64)") } guard let (_, publicKey) = decodePrivateAndPublicKeys(secret: secret) else { failure("Imported key must be 32 or 96 bytes (for the older format) decoded. Instead it is \(secret.count) bytes decoded.") } print("Importing signing key..\n") storeSecret(account: account, publicEdKey: publicKey, secret: secret) printNewPublicKeyUsage(publicKey) } else { /// Default mode - find an existing public key and print its usage, or generate new keys if let secret = findSecret(account: account) { guard let (_, pubKey) = decodePrivateAndPublicKeys(secret: secret) else { failure("Stored private key must be 32 or 96 bytes (for the older format) decoded. Instead it is \(secret.count) bytes decoded.") } print(""" A pre-existing signing key was found. This is how it should appear in your Info.plist: SUPublicEDKey \(pubKey.base64EncodedString()) """) } else { print("Generating a new signing key. This may take a moment, depending on your machine.") let (pubKey, seed) = generatePublicKeyAndSeed() // New keys that are generated only store the seed as the secret // Old keys store private orlp/Ed25519 key + public key storeSecret(account: account, publicEdKey: pubKey, secret: seed) printNewPublicKeyUsage(pubKey) } } } } GenerateKeys.main() ================================================ FILE: sign_update/Bridging-Header.h ================================================ #import #import "SPUExtractSignedFeed.h" #import "SUConstants.h" #import "SUErrors.h" #import "SUSignatures.h" #import "ed25519.h" ================================================ FILE: sign_update/main.swift ================================================ // // main.swift // sign_update // // Created by Kornel on 16/09/2018. // Copyright © 2018 Sparkle Project. All rights reserved. // import Foundation import Security import ArgumentParser func findKeysInKeychain(account: String) throws -> (Data, Data) { var item: CFTypeRef? let res = SecItemCopyMatching([ kSecClass as String: kSecClassGenericPassword, kSecAttrService as String: "https://sparkle-project.org", kSecAttrAccount as String: account, kSecAttrProtocol as String: kSecAttrProtocolSSH, kSecReturnData as String: kCFBooleanTrue!, ] as CFDictionary, &item) if res == errSecSuccess { guard let encoded = item as? Data else { print("ERROR! Unable to decode data from Keychain") throw ExitCode.failure } guard let secret = Data(base64Encoded: encoded) else { print("ERROR! Unable to decode data from Keychain as base64") throw ExitCode.failure } guard let (privateKey, publicKey) = decodePrivateAndPublicKeys(secret: secret) else { print("ERROR! Key pair data stored in keychain has \(secret.count) bytes which is invalid") throw ExitCode.failure } return (privateKey, publicKey) } else if res == errSecItemNotFound { print("ERROR! Signing key not found for account \(account). Please run generate_keys tool first or provide key with --ed-key-file ") } else if res == errSecAuthFailed { print("ERROR! Access denied. Can't get keys from the keychain.") print("Go to Keychain Access.app, lock the login keychain, then unlock it again.") } else if res == errSecUserCanceled { print("ABORTED! You've cancelled the request to read the key from the Keychain. Please run the tool again.") } else if res == errSecInteractionNotAllowed { print("ERROR! The operating system has blocked access to the Keychain.") } else { print("ERROR! Unable to access required key in the Keychain: \(res) (you can look it up at osstatus.com)") } throw ExitCode.failure } func findKeys(inFile secretFile: String) throws -> (Data, Data) { let secretString: String if secretFile == "-" && !FileManager.default.fileExists(atPath: secretFile) { if let line = readLine(strippingNewline: true) { secretString = line } else { print("ERROR! Unable to read EdDSA private key from standard input") throw ExitCode(1) } } else { secretString = try decodeSecretString(filePath: secretFile) } return try findKeys(inString: secretString, allowNewFormat: true) } func findKeys(inString secretBase64String: String, allowNewFormat: Bool) throws -> (Data, Data) { guard let secret = Data(base64Encoded: secretBase64String, options: .init()) else { print("ERROR! Failed to decode base64 encoded key data from: \(secretBase64String)") throw ExitCode.failure } guard allowNewFormat || !secretUsesRegularSeed(secret: secret) else { print("ERROR! Specifying private key as an argument is no longer supported.") throw ExitCode.failure } guard let (privateKey, publicKey) = decodePrivateAndPublicKeys(secret: secret) else { print("ERROR! Imported key must be 64 bytes or 96 bytes (for the older format) decoded. Instead it is \(secret.count) bytes decoded.") throw ExitCode.failure } return (privateKey, publicKey) } struct SignUpdate: ParsableCommand { static let programName = "sign_update" @Option(help: ArgumentHelp("The account name in your keychain associated with your private keys to use for signing.")) var account: String = "ed25519" @Flag(help: ArgumentHelp("Verify that the file is signed correctly. If this is set, a second argument denoting the signature must be passed after the .", valueName: "verify")) var verify: Bool = false @Option(name: [.customShort("f"), .customLong("ed-key-file")], help: ArgumentHelp("Path to the file containing the private EdDSA (ed25519) key. '-' can be used to echo the EdDSA key from a 'secret' environment variable to the standard input stream. For example: echo \"$PRIVATE_KEY_SECRET\" | ./\(programName) --ed-key-file -", valueName: "private-key-file")) var privateKeyFile: String? @Flag(name: .customShort("p"), help: ArgumentHelp("Only prints the signature when signing a file without extra metadata. For signing XML files, nothing will be printed because the signature is embedded inside the file.")) var printOnlySignature: Bool = false @Argument(help: "The update archive, delta update, package (pkg), release notes file, or update feed (xml) to sign or verify. If the file is a update feed (xml), the file will be modified to include the generated signature. If the file is a release notes file, the file may also be updated to include a warning that it is signed (unless --disable-signing-warning is specified).") var filePath: String @Flag(name: .long, help: ArgumentHelp("Disables adding a warning to signed appcast and release note files explaining that further modifications will require re-signing them. This flag has no effect if the files already have a signing warning embedded.")) var disableSigningWarning: Bool = false @Argument(help: "The signature to verify when --verify is passed. Don't pass this option for verifying appcast XML feeds, which already have a signature embedded.") var verifySignature: String? @Option(name: .customShort("s"), help: ArgumentHelp("(DEPRECATED): The private EdDSA (ed25519) key. Please use the Keychain, or pass the key as standard input when using --ed-key-file - instead. This option is no longer supported for newly generated keys.", valueName: "private-key")) var privateKey: String? static var configuration: CommandConfiguration = CommandConfiguration( abstract: "Sign or verify an update file using your signing keys.", discussion: """ sign_update can be used to sign or verify update archives, delta updates, pkg updates, appcast feeds, and release note files. The signing keys are automatically read from the Keychain if no is specified. For signing update archives, sign_update will output an EdDSA signature and length attributes to use for your update's appcast item enclosure. For signing release note files, sign_update will output an EdDSA signature and length attributes to use for your update's appcast releaseNotesLink. Additionally, the release notes file may be modified to include a warning about making future modifications to the file (unless --disable-signing-warning is specified). For signing appcast feeds, sign_update will embed the signature inside the XML file and include a warning about making future modifications to the file (unless --disable-signing-warning is specified). For signing files, you can use -p to only print the EdDSA signature for automation. """) private var filePathIsFeed: Bool { return filePath.hasSuffix(".xml") || filePath.hasSuffix(".XML") } private var filePathIsHTMLReleaseNotes: Bool { return filePath.hasSuffix(".html") || filePath.hasSuffix(".htm") } private var filePathIsMarkdownReleaseNotes: Bool { return filePath.hasSuffix(".md") || filePath.hasSuffix(".markdown") } private var filePathIsReleaseNotes: Bool { return filePath.hasSuffix(".txt") || self.filePathIsMarkdownReleaseNotes || self.filePathIsHTMLReleaseNotes } func validate() throws { guard privateKey == nil || privateKeyFile == nil else { throw ValidationError("Both --ed-key-file and -s options cannot be provided.") } guard !verify || verifySignature != nil || self.filePathIsFeed else { throw ValidationError(" must be passed as a second argument after if --verify is passed.") } guard !self.filePathIsFeed || verifySignature == nil else { throw ValidationError(" must not be passed for signing appcast feeds, which already have the signature embedded.") } guard !verify || !printOnlySignature else { throw ValidationError("Both --verify and -p options cannot be provided.") } } func run() throws { let (priv, pub): (Data, Data) if let privateKey = privateKey?.trimmingCharacters(in: .whitespacesAndNewlines) { fputs("Warning: The -s option for passing the private EdDSA key is insecure and deprecated. Please see its help usage for more information.\n", stderr) (priv, pub) = try findKeys(inString: privateKey, allowNewFormat: false) } else if let privateKeyFile = privateKeyFile { (priv, pub) = try findKeys(inFile: privateKeyFile) } else { (priv, pub) = try findKeysInKeychain(account: account) } let fileURL = URL(fileURLWithPath: filePath, isDirectory: false).resolvingSymlinksInPath() let data = try Data.init(contentsOf: fileURL, options: .mappedIfSafe) if verify { // Verify the signature let dataToVerify: Data let base64Signature: String let expectedContentLength: UInt64 if let verifySignature { base64Signature = verifySignature dataToVerify = data expectedContentLength = UInt64(data.count) } else { assert(self.filePathIsFeed) var processedBase64Signature: NSString? = nil var processedExpectedContentLength: UInt64 = 0 dataToVerify = SPUExtractAppcastContent(data, &processedBase64Signature, &processedExpectedContentLength) expectedContentLength = processedExpectedContentLength guard let processedBase64Signature else { print("Error: failed to extract signature from appcast. Is the appcast signed?") throw ExitCode.failure } base64Signature = processedBase64Signature as String } guard let signatureData = Data(base64Encoded: base64Signature, options: .ignoreUnknownCharacters) else { print("Error: failed to decode base64 signature: \(base64Signature)") throw ExitCode.failure } let signatureBytes = Array(signatureData) guard signatureBytes.count == 64 else { print("Error: signature passed in has an invalid byte count.") throw ExitCode.failure } let dataBytesToVerify = Array(dataToVerify) let publicKeyBytes = Array(pub) if ed25519_verify(signatureBytes, dataBytesToVerify, dataBytesToVerify.count, publicKeyBytes) == 0 { print("Error: failed to pass signing verification.") let dataBytesCount = UInt64(dataBytesToVerify.count) if expectedContentLength != dataBytesCount { print("\(expectedContentLength) bytes were expected to be signed, but actually read \(dataBytesCount) bytes.") } throw ExitCode.failure } } else { // Sign the update if self.filePathIsFeed { let contentData = SPUExtractAppcastContent(data, nil, nil) let dataToSign: Data = disableSigningWarning ? contentData : addSignWarningToAppcast(data: contentData) let addedSignWarningToAppcast = (contentData.count != dataToSign.count) let signedData = try signAppcast(data: dataToSign, publicEdKey: pub, privateEdKey: priv) try signedData.write(to: fileURL, options: .atomic) if !printOnlySignature { if !addedSignWarningToAppcast { print("") } else { print("") } } } else { let signedData: Data let updatedSigningWarningInReleaseNotes: Bool if disableSigningWarning || (!self.filePathIsHTMLReleaseNotes && !self.filePathIsMarkdownReleaseNotes) { signedData = data updatedSigningWarningInReleaseNotes = false } else { if let updatedDataForSigning = updateHTMLCommentSigningWarningInReleaseNotes(data: data) { do { try updatedDataForSigning.write(to: fileURL, options: .atomic) signedData = updatedDataForSigning updatedSigningWarningInReleaseNotes = true } catch { // Fallback to original data if the release notes is not updatable signedData = data updatedSigningWarningInReleaseNotes = false } } else { signedData = data updatedSigningWarningInReleaseNotes = false } } let sig = edSignature(data: signedData, publicEdKey: pub, privateEdKey: priv) if printOnlySignature { print(sig) } else { if self.filePathIsReleaseNotes { if updatedSigningWarningInReleaseNotes { print("") } print("sparkle:edSignature=\"\(sig)\" sparkle:length=\"\(signedData.count)\"") } else { print("sparkle:edSignature=\"\(sig)\" length=\"\(signedData.count)\"") } } } } } } SignUpdate.main() ================================================ FILE: sparkle-cli/Info.plist ================================================ CFBundleDevelopmentRegion en CFBundleExecutable $(EXECUTABLE_NAME) CFBundleIconFile CFBundleIdentifier $(PRODUCT_BUNDLE_IDENTIFIER) CFBundleInfoDictionaryVersion 6.0 CFBundleName $(PRODUCT_NAME) CFBundlePackageType APPL CFBundleShortVersionString $(MARKETING_VERSION) CFBundleSignature ???? CFBundleVersion ${CURRENT_PROJECT_VERSION} LSUIElement 1 LSMinimumSystemVersion $(MACOSX_DEPLOYMENT_TARGET) NSHumanReadableCopyright Copyright © 2016 Sparkle Project. All rights reserved. NSPrincipalClass NSApplication NSAppTransportSecurity NSAllowsArbitraryLoads ================================================ FILE: sparkle-cli/SPUCommandLineDriver.h ================================================ // // SUCommandLineDriver.h // sparkle-cli // // Created by Mayur Pawashe on 4/10/16. // Copyright © 2016 Sparkle Project. All rights reserved. // #import NS_ASSUME_NONNULL_BEGIN @class SUUpdatePermissionResponse; SPU_OBJC_DIRECT_MEMBERS @interface SPUCommandLineDriver : NSObject - (nullable instancetype)initWithUpdateBundlePath:(NSString *)updateBundlePath applicationBundlePath:(nullable NSString *)applicationBundlePath allowedChannels:(NSSet *)allowedChannels customFeedURL:(nullable NSString *)customFeedURL userAgentName:(nullable NSString *)userAgentName updatePermissionResponse:(nullable SUUpdatePermissionResponse *)updatePermissionResponse deferInstallation:(BOOL)deferInstallation interactiveInstallation:(BOOL)interactiveInstallation allowMajorUpgrades:(BOOL)allowMajorUpgrades verbose:(BOOL)verbose; - (void)runAndCheckForUpdatesNow:(BOOL)checkForUpdatesNow; - (void)probeForUpdates; @end NS_ASSUME_NONNULL_END ================================================ FILE: sparkle-cli/SPUCommandLineDriver.m ================================================ // // SUCommandLineDriver.m // sparkle-cli // // Created by Mayur Pawashe on 4/10/16. // Copyright © 2016 Sparkle Project. All rights reserved. // #import "SPUCommandLineDriver.h" #import #import #import #import "SPUCommandLineUserDriver.h" #define SPARKLE_CLI_ERROR_DOMAIN @"sparkle-cli" typedef NS_ENUM(NSInteger, CLIErrorCode) { CLIErrorCodeCannotPerformCheck = 1, CLIErrorCodeCannotInstallPackage, CLIErrorCodeCannotInstallMajorUpgrade }; typedef NS_ENUM(int, CLIErrorExitStatus) { CLIErrorExitStatusMajorUpgradeNotAllowed = 2, CLIErrorExitStatusInstallerInteractionNotAllowed = 3, CLIErrorExitStatusUpdateNotFound = 4, CLIErrorExitStatusUpdateCancelledAuthorization = 5, CLIErrorExitStatusUpdatePermissionRequested = 6, //CLIErrorCodeCannotInstallInteractivePackageAsRoot = 7, CLIErrorExitStatusInstallationWriteNoPermissionError = 8, }; @interface SPUCommandLineDriver () @end @implementation SPUCommandLineDriver { SPUUpdater *_updater; SUUpdatePermissionResponse *_updatePermissionResponse; NSSet *_allowedChannels; NSString *_customFeedURL; BOOL _verbose; BOOL _probingForUpdates; BOOL _interactive; BOOL _allowMajorUpgrades; } - (instancetype)initWithUpdateBundlePath:(NSString *)updateBundlePath applicationBundlePath:(nullable NSString *)applicationBundlePath allowedChannels:(NSSet *)allowedChannels customFeedURL:(nullable NSString *)customFeedURL userAgentName:(nullable NSString *)customUserAgentName updatePermissionResponse:(nullable SUUpdatePermissionResponse *)updatePermissionResponse deferInstallation:(BOOL)deferInstallation interactiveInstallation:(BOOL)interactiveInstallation allowMajorUpgrades:(BOOL)allowMajorUpgrades verbose:(BOOL)verbose { self = [super init]; if (self != nil) { NSBundle *updateBundle = [NSBundle bundleWithPath:updateBundlePath]; if (updateBundle == nil) { return nil; } NSBundle *applicationBundle = nil; if (applicationBundlePath == nil) { applicationBundle = updateBundle; } else { applicationBundle = [NSBundle bundleWithPath:(NSString * _Nonnull)applicationBundlePath]; if (applicationBundle == nil) { return nil; } } _verbose = verbose; _interactive = interactiveInstallation; _allowMajorUpgrades = allowMajorUpgrades; _allowedChannels = allowedChannels; _customFeedURL = [customFeedURL copy]; _updatePermissionResponse = updatePermissionResponse; id userDriver = [[SPUCommandLineUserDriver alloc] initWithUpdatePermissionResponse:updatePermissionResponse deferInstallation:deferInstallation verbose:verbose]; _updater = [[SPUUpdater alloc] initWithHostBundle:updateBundle applicationBundle:applicationBundle userDriver:userDriver delegate:self]; { // Retrieve a suitable user agent. NSString *userAgentString; NSBundle *mainBundle = [NSBundle mainBundle]; if (customUserAgentName != nil) { // Let's use the user agent name that the user passed to us userAgentString = SPUMakeUserAgentWithBundle(mainBundle, [NSString stringWithFormat:@" (%@)", customUserAgentName]); } else { // Are we embedded inside of another responsible app? NSURL *parentDirectoryURL = mainBundle.bundleURL.URLByDeletingLastPathComponent; NSURL *parentParentDirectoryURL = parentDirectoryURL.URLByDeletingLastPathComponent; if ([parentParentDirectoryURL.lastPathComponent isEqualToString:@"Contents"] && ([parentDirectoryURL.lastPathComponent isEqualToString:@"Resources"] || [parentDirectoryURL.lastPathComponent isEqualToString:@"MacOS"] || [parentDirectoryURL.lastPathComponent isEqualToString:@"Helpers"])) { NSURL *responsibleApplicationURL = parentParentDirectoryURL.URLByDeletingLastPathComponent; NSBundle *responsibleBundle = [NSBundle bundleWithURL:responsibleApplicationURL]; if (responsibleBundle == nil) { userAgentString = SPUMakeUserAgentWithBundle(mainBundle, nil); } else { userAgentString = SPUMakeUserAgentWithBundle(responsibleBundle, @" (sparkle)"); } } else { userAgentString = SPUMakeUserAgentWithBundle(mainBundle, nil); } } _updater.userAgentString = userAgentString; } } return self; } - (void)updater:(SPUUpdater *)__unused updater willScheduleUpdateCheckAfterDelay:(NSTimeInterval)delay __attribute__((noreturn)) { if (_verbose) { fprintf(stderr, "Last update check occurred too soon. Try again after %0.0f second(s).", delay); } exit(EXIT_SUCCESS); } - (void)updaterWillNotScheduleUpdateCheck:(SPUUpdater *)__unused updater __attribute__((noreturn)) { if (_verbose) { fprintf(stderr, "Automatic update checks are disabled. Exiting.\n"); } exit(EXIT_SUCCESS); } - (BOOL)updaterShouldPromptForPermissionToCheckForUpdates:(SPUUpdater *)__unused updater { if (_updatePermissionResponse == nil) { // We don't want to make this decision on behalf of the user. fprintf(stderr, "Error: Asked to grant update permission and --grant-automatic-checks is not specified. Exiting.\n"); exit(CLIErrorExitStatusUpdatePermissionRequested); } return YES; } // If the installation is not interactive, we should not perform an update check if we don't have permission to update the bundle path - (BOOL)updater:(SPUUpdater *)updater mayPerformUpdateCheck:(SPUUpdateCheck)updateCheck error:(NSError * __autoreleasing *)error { switch (updateCheck) { case SPUUpdateCheckUpdates: case SPUUpdateCheckUpdatesInBackground: { if (_interactive) { return YES; } BOOL rootUser = (geteuid() == 0); if (rootUser) { return YES; } if (!SPUSystemNeedsAuthorizationAccessForBundlePath(_updater.hostBundle.bundlePath)) { return YES; } if (error != NULL) { *error = [NSError errorWithDomain:SPARKLE_CLI_ERROR_DOMAIN code:CLIErrorCodeCannotPerformCheck userInfo:@{ NSLocalizedDescriptionKey: @"A new update check cannot be performed because updating this bundle will require user authorization. Please use --interactive or run as root to allow this." }]; } return NO; } case SPUUpdateCheckUpdateInformation: return YES; } } // If the installation is not interactive, we should only proceed with application based updates and not package-based ones - (BOOL)updater:(SPUUpdater *)updater shouldProceedWithUpdate:(nonnull SUAppcastItem *)updateItem updateCheck:(SPUUpdateCheck)updateCheck error:(NSError * __autoreleasing *)error { // We can always probe for update information if (updateCheck == SPUUpdateCheckUpdateInformation) { return YES; } // If we encounter a major upgrade and not allowed to act on it, then error if (updateItem.majorUpgrade && !_allowMajorUpgrades) { if (error != NULL) { *error = [NSError errorWithDomain:SPARKLE_CLI_ERROR_DOMAIN code:CLIErrorCodeCannotInstallMajorUpgrade userInfo:@{ NSLocalizedDescriptionKey: @"Major upgrade available but not allowed to install it. Pass --allow-major-upgrades to allow this." }]; } return NO; } if (!_interactive && geteuid() != 0) { // applicable for non-root only if (![updateItem.installationType isEqualToString:SPUInstallationTypeApplication]) { // Any package based updates will require authorization and therefore interaction if (error != NULL) { *error = [NSError errorWithDomain:SPARKLE_CLI_ERROR_DOMAIN code:CLIErrorCodeCannotInstallPackage userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"A new package-based update has been found (%@), but installing it will require user authorization. Please use --interactive to allow this.", updateItem.versionString] }]; } return NO; } } return YES; } - (NSSet *)allowedChannelsForUpdater:(SPUUpdater *)__unused updater { return _allowedChannels; } - (nullable NSString *)feedURLStringForUpdater:(SPUUpdater *)__unused updater { return _customFeedURL; } // In case we find an update during probing - (void)updater:(SPUUpdater *)__unused updater didFindValidUpdate:(SUAppcastItem *)item { if (_probingForUpdates) { if (_verbose) { if (item.majorUpgrade) { fprintf(stderr, "Major upgrade available.\n"); } else { fprintf(stderr, "Update available!\n"); } } } } - (void)updater:(SPUUpdater *)updater didFinishUpdateCycleForUpdateCheck:(SPUUpdateCheck)__unused updateCheck error:(nullable NSError *)error __attribute__((noreturn)) { if (error == nil) { if (_verbose) { fprintf(stderr, "Exiting.\n"); } exit(EXIT_SUCCESS); } else if ([error.domain isEqualToString:SPARKLE_CLI_ERROR_DOMAIN]) { fprintf(stderr, "%s\n", error.localizedDescription.UTF8String); if (error.code == CLIErrorCodeCannotInstallMajorUpgrade) { // Major upgrades are not allowed exit(CLIErrorExitStatusMajorUpgradeNotAllowed); } else { // This is one of our own interactive update failures exit(CLIErrorExitStatusInstallerInteractionNotAllowed); } } else if (error.code == SUNoUpdateError) { if (_verbose) { fprintf(stderr, "No new update available!\n"); } exit(CLIErrorExitStatusUpdateNotFound); } else if (error.code == SUInstallationCanceledError) { // User canceled authorization themselves assert(_interactive); if (_verbose) { fprintf(stderr, "Update was cancelled.\n"); } exit(CLIErrorExitStatusUpdateCancelledAuthorization); } else if (error.code == SUInstallationWriteNoPermissionError) { fprintf(stderr, "Error: %s", error.localizedDescription.UTF8String); if (error.localizedRecoverySuggestion != nil) { fprintf(stderr, " %s", error.localizedRecoverySuggestion.UTF8String); } fprintf(stderr, "\n"); exit(CLIErrorExitStatusInstallationWriteNoPermissionError); } else { fprintf(stderr, "Error: Update has failed due to error %ld (%s). %s\n", (long)error.code, error.domain.UTF8String, error.localizedDescription.UTF8String); exit(EXIT_FAILURE); } } - (BOOL)updater:(SPUUpdater *)updater shouldDownloadReleaseNotesForUpdate:(nonnull SUAppcastItem *)__unused item { return _verbose; } - (void)startUpdater SPU_OBJC_DIRECT { NSError *updaterError = nil; if (![_updater startUpdater:&updaterError]) { fprintf(stderr, "Error: Failed to initialize updater with error (%ld): %s\n", updaterError.code, updaterError.localizedDescription.UTF8String); exit(EXIT_FAILURE); } } - (void)runAndCheckForUpdatesNow:(BOOL)checkForUpdatesNow { [self startUpdater]; if (checkForUpdatesNow) { // When we start the updater, this scheduled check will start afterwards too [_updater checkForUpdates]; } } - (void)probeForUpdates { [self startUpdater]; // When we start the updater, this info check will start afterwards too _probingForUpdates = YES; [_updater checkForUpdateInformation]; } @end ================================================ FILE: sparkle-cli/SPUCommandLineUserDriver.h ================================================ // // SUCommandLineUserDriver.h // Sparkle // // Created by Mayur Pawashe on 4/10/16. // Copyright © 2016 Sparkle Project. All rights reserved. // #import #import NS_ASSUME_NONNULL_BEGIN SPU_OBJC_DIRECT_MEMBERS @interface SPUCommandLineUserDriver : NSObject - (instancetype)initWithUpdatePermissionResponse:(nullable SUUpdatePermissionResponse *)updatePermissionResponse deferInstallation:(BOOL)deferInstallation verbose:(BOOL)verbose; @end NS_ASSUME_NONNULL_END ================================================ FILE: sparkle-cli/SPUCommandLineUserDriver.m ================================================ // // SUCommandLineUserDriver.m // Sparkle // // Created by Mayur Pawashe on 4/10/16. // Copyright © 2016 Sparkle Project. All rights reserved. // #import "SPUCommandLineUserDriver.h" #import #import @implementation SPUCommandLineUserDriver { SUUpdatePermissionResponse *_updatePermissionResponse; NSString *_lastProgressReported; uint64_t _bytesDownloaded; uint64_t _bytesToDownload; BOOL _deferInstallation; BOOL _verbose; } - (instancetype)initWithUpdatePermissionResponse:(nullable SUUpdatePermissionResponse *)updatePermissionResponse deferInstallation:(BOOL)deferInstallation verbose:(BOOL)verbose { self = [super init]; if (self != nil) { _updatePermissionResponse = updatePermissionResponse; _deferInstallation = deferInstallation; _verbose = verbose; } return self; } - (void)showUpdatePermissionRequest:(SPUUpdatePermissionRequest *)__unused request reply:(void (^)(SUUpdatePermissionResponse *))reply { if (_verbose) { fprintf(stderr, "Granting permission for automatic update checks with sending system profile %s...\n", _updatePermissionResponse.sendSystemProfile ? "enabled" : "disabled"); } reply(_updatePermissionResponse); } - (void)showUserInitiatedUpdateCheckWithCancellation:(void (^)(void))__unused cancellation { if (_verbose) { fprintf(stderr, "Checking for Updates...\n"); } } - (void)displayReleaseNotes:(const char * _Nullable)releaseNotes SPU_OBJC_DIRECT { if (releaseNotes != NULL) { fprintf(stderr, "Release notes:\n"); fprintf(stderr, "%s\n", releaseNotes); } } - (void)displayHTMLReleaseNotes:(NSData *)releaseNotes SPU_OBJC_DIRECT { // Note: this is the only API we rely on here that references AppKit // We shouldn't invoke it when the calling process is ran under root. // If only there was an API to translated HTML -> text that didn't rely on AppKit.. if (geteuid() != 0) { NSAttributedString *attributedString = [[NSAttributedString alloc] initWithHTML:releaseNotes documentAttributes:nil]; [self displayReleaseNotes:attributedString.string.UTF8String]; } } - (void)displayPlainTextReleaseNotes:(NSData *)releaseNotes encoding:(NSStringEncoding)encoding SPU_OBJC_DIRECT { NSString *string = [[NSString alloc] initWithData:releaseNotes encoding:encoding]; [self displayReleaseNotes:string.UTF8String]; } - (void)showUpdateWithAppcastItem:(SUAppcastItem *)appcastItem updateAdjective:(NSString *)updateAdjective { if (_verbose) { fprintf(stderr, "Found %s update! (%s)\n", updateAdjective.UTF8String, appcastItem.displayVersionString.UTF8String); if (appcastItem.itemDescription != nil) { NSData *descriptionData = [appcastItem.itemDescription dataUsingEncoding:NSUTF8StringEncoding]; if (descriptionData != nil) { NSString *itemDescriptionFormat = appcastItem.itemDescriptionFormat; if (itemDescriptionFormat != nil && ([itemDescriptionFormat isEqualToString:@"plain-text"] || [itemDescriptionFormat isEqualToString:@"markdown"])) { [self displayPlainTextReleaseNotes:descriptionData encoding:NSUTF8StringEncoding]; } else { [self displayHTMLReleaseNotes:descriptionData]; } } } } } - (void)showUpdateFoundWithAppcastItem:(SUAppcastItem *)appcastItem state:(SPUUserUpdateState *)state reply:(void (^)(SPUUserUpdateChoice))reply { if (appcastItem.informationOnlyUpdate) { fprintf(stderr, "Found information for new update: %s\n", appcastItem.infoURL.absoluteString.UTF8String); reply(SPUUserUpdateChoiceDismiss); } else { switch (state.stage) { case SPUUserUpdateStageNotDownloaded: [self showUpdateWithAppcastItem:appcastItem updateAdjective:@"new"]; reply(SPUUserUpdateChoiceInstall); break; case SPUUserUpdateStageDownloaded: [self showUpdateWithAppcastItem:appcastItem updateAdjective:@"downloaded"]; reply(SPUUserUpdateChoiceInstall); break; case SPUUserUpdateStageInstalling: if (_deferInstallation) { if (_verbose) { fprintf(stderr, "Deferring Installation.\n"); } reply(SPUUserUpdateChoiceDismiss); } else { reply(SPUUserUpdateChoiceInstall); } break; } } } - (void)showUpdateReleaseNotesWithDownloadData:(SPUDownloadData *)downloadData { if (_verbose) { NSString *MIMEType = downloadData.MIMEType; NSString *fileExtension = downloadData.URL.pathExtension; if ([MIMEType isEqualToString:@"text/plain"] || [MIMEType isEqualToString:@"text/markdown"] || [MIMEType isEqualToString:@"text/x-markdown"] || [fileExtension caseInsensitiveCompare:@"txt"] == NSOrderedSame || [fileExtension caseInsensitiveCompare:@"md"] == NSOrderedSame || [fileExtension caseInsensitiveCompare:@"markdown"] == NSOrderedSame) { NSStringEncoding encoding; if (downloadData.textEncodingName == nil) { encoding = NSUTF8StringEncoding; } else { CFStringEncoding cfEncoding = CFStringConvertIANACharSetNameToEncoding((CFStringRef)downloadData.textEncodingName); if (cfEncoding != kCFStringEncodingInvalidId) { encoding = CFStringConvertEncodingToNSStringEncoding(cfEncoding); } else { encoding = NSUTF8StringEncoding; } } [self displayPlainTextReleaseNotes:downloadData.data encoding:encoding]; } else { [self displayHTMLReleaseNotes:downloadData.data]; } } } - (void)showUpdateReleaseNotesFailedToDownloadWithError:(NSError *)error { if (_verbose) { fprintf(stderr, "Error: Unable to download release notes: %s\n", error.localizedDescription.UTF8String); } } - (void)showUpdateNotFoundWithError:(NSError *)__unused error acknowledgement:(void (^)(void))acknowledgement { acknowledgement(); } - (void)showUpdaterError:(NSError *)__unused error acknowledgement:(void (^)(void))acknowledgement { acknowledgement(); } - (void)showDownloadInitiatedWithCancellation:(void (^)(void))__unused cancellation { if (_verbose) { _lastProgressReported = nil; fprintf(stderr, "Downloading Update...\n"); } } - (void)showDownloadDidReceiveExpectedContentLength:(uint64_t)expectedContentLength { if (_verbose) { fprintf(stderr, "Downloading %llu bytes...\n", expectedContentLength); } _bytesDownloaded = 0; _bytesToDownload = expectedContentLength; } - (void)showDownloadDidReceiveDataOfLength:(uint64_t)length { _bytesDownloaded += length; // In case our expected content length was incorrect if (_bytesDownloaded > _bytesToDownload) { _bytesToDownload = _bytesDownloaded; } if (_bytesToDownload > 0 && _verbose) { NSString *currentProgressPercentage = [NSString stringWithFormat:@"%.0f%%", ((double)_bytesDownloaded * 100.0 / (double)_bytesToDownload)]; // Only report progress advancement when percentage significantly advances if (_lastProgressReported == nil || ![_lastProgressReported isEqualToString:currentProgressPercentage]) { fprintf(stderr, "Downloaded %llu out of %llu bytes (%s)\n", _bytesDownloaded, _bytesToDownload, currentProgressPercentage.UTF8String); _lastProgressReported = currentProgressPercentage; } } } - (void)showDownloadDidStartExtractingUpdate { if (_verbose) { _lastProgressReported = nil; fprintf(stderr, "Extracting Update...\n"); } } - (void)showExtractionReceivedProgress:(double)progress { if (_verbose) { NSString *currentProgressPercentage = [NSString stringWithFormat:@"%.0f%%", progress * 100]; // Only report progress advancement when percentage significantly advances if (_lastProgressReported == nil || ![_lastProgressReported isEqualToString:currentProgressPercentage]) { fprintf(stderr, "Extracting Update (%s)\n", currentProgressPercentage.UTF8String); _lastProgressReported = currentProgressPercentage; } } } - (void)showReadyToInstallAndRelaunch:(void (^)(SPUUserUpdateChoice))installUpdateHandler { if (_deferInstallation) { if (_verbose) { fprintf(stderr, "Deferring Installation.\n"); } installUpdateHandler(SPUUserUpdateChoiceDismiss); } else { installUpdateHandler(SPUUserUpdateChoiceInstall); } } - (void)showInstallingUpdateWithApplicationTerminated:(BOOL)__unused applicationTerminated retryTerminatingApplication:(void (^)(void))__unused retryTerminatingApplication { if (_verbose) { fprintf(stderr, "Installing Update...\n"); } } - (void)showUpdateInstalledAndRelaunched:(BOOL)__unused relaunched acknowledgement:(void (^)(void))acknowledgement { if (_verbose) { fprintf(stderr, "Installation Finished.\n"); } acknowledgement(); } - (void)dismissUpdateInstallation { } @end ================================================ FILE: sparkle-cli/main.m ================================================ // // main.m // sparkle-cli // // Created by Mayur Pawashe on 4/10/16. // Copyright © 2016 Sparkle Project. All rights reserved. // #import #import #import "SPUCommandLineDriver.h" #include #define APPLICATION_FLAG "application" #define DEFER_FLAG "defer-install" #define VERBOSE_FLAG "verbose" #define CHECK_NOW_FLAG "check-immediately" #define GRANT_AUTOMATIC_CHECKING_FLAG "grant-automatic-checks" #define SEND_PROFILE_FLAG "send-profile" #define PROBE_FLAG "probe" #define INTERACTIVE_FLAG "interactive" #define FEED_URL_FLAG "feed-url" #define CHANNELS_FLAG "channels" #define ALLOW_MAJOR_UPGRADES_FLAG "allow-major-upgrades" #define USER_AGENT_NAME "user-agent-name" static void printUsage(char **argv) { fprintf(stderr, "Usage: %s bundle [--%s app-path] [--%s] [--%s] [--%s chan1,chan2,…] [--%s feed-url] [--%s display-name] [--%s] [--%s] [--%s] [--%s] [--%s] [--%s]\n", argv[0], APPLICATION_FLAG, CHECK_NOW_FLAG, PROBE_FLAG, CHANNELS_FLAG, FEED_URL_FLAG, USER_AGENT_NAME, GRANT_AUTOMATIC_CHECKING_FLAG, SEND_PROFILE_FLAG, DEFER_FLAG, INTERACTIVE_FLAG, ALLOW_MAJOR_UPGRADES_FLAG, VERBOSE_FLAG); fprintf(stderr, "Description:\n"); fprintf(stderr, " Check if any new updates for a Sparkle supported bundle need to be installed.\n\n"); fprintf(stderr, " If any new updates need to be installed, the user application\n is terminated and the update is installed immediately unless --%s\n is specified. If the application was alive, then it will be relaunched after.\n\n", DEFER_FLAG); fprintf(stderr, " To check if an update is available without installing, use --%s.\n\n", PROBE_FLAG); fprintf(stderr, " if no updates are available now, or if the last update check was recently\n (unless --%s is specified) then nothing is done.\n\n", CHECK_NOW_FLAG); fprintf(stderr, " If update permission is requested and --%s is not\n specified, then checking for updates is aborted.\n\n", GRANT_AUTOMATIC_CHECKING_FLAG); fprintf(stderr, " Unless --%s is specified, this tool will not request for escalated\n authorization. Alternatively, this tool can be run as root under an active user login\n session, which will not require (and disallow) interaction.\n\n", INTERACTIVE_FLAG); fprintf(stderr, " If --%s is specified, this tool will exit leaving a spawned process\n for finishing the installation after the target application terminates.\n\n", DEFER_FLAG); fprintf(stderr, " If update installation fails due to not having permission (e.g. from Gatekeeper) to replace the old bundle, an exit status of 8 is returned.\n"); fprintf(stderr, " Please specify --%s if you intend to use this tool in an automated way.\n", USER_AGENT_NAME); fprintf(stderr, "Options:\n"); fprintf(stderr, " --%s\n Path to the application to watch for termination and to relaunch.\n If not provided, this is assumed to be the same as the bundle.\n", APPLICATION_FLAG); fprintf(stderr, " --%s\n Immediately checks for updates to install.\n Without this, updates are checked only when needed on a scheduled basis.\n", CHECK_NOW_FLAG); fprintf(stderr, " --%s\n Probe for updates. Check if any updates are available but do not install.\n An exit status of 0 is returned if a new update is available.\n", PROBE_FLAG); fprintf(stderr, " --%s\n Allows probing and installing major upgrades. Without passing this, an exit\n status of 2 is returned if a major upgrade is found.\n", ALLOW_MAJOR_UPGRADES_FLAG); fprintf(stderr, " --%s\n List of allowed Sparkle channels to look for updates in. By default,\n only the default channel is used.\n", CHANNELS_FLAG); fprintf(stderr, " --%s\n URL for appcast feed. This URL will be used for the feed instead of the one\n in the bundle's Info.plist or in the bundle's user defaults.\n", FEED_URL_FLAG); fprintf(stderr, " --%s\n Display name that will be included as a part of the User-Agent string.\n We encourage setting this so developers know what is querying their feed.\n Otherwise, this value may be set and inferred automatically.\n", USER_AGENT_NAME); fprintf(stderr, " --%s\n Allows prompting the user for an authorization dialog prompt if the\n installer needs elevated privileges.\n Without passing this, an exit status of 3 is returned if an update\n requires user interaction. An exit status of 5 is returned\n if the user cancels the authorization prompt.\n", INTERACTIVE_FLAG); fprintf(stderr, " --%s\n If update permission is requested, this enables automatic update checks.\n Note that this behavior may overwrite the user's defaults for the bundle.\n This option has no effect if --%s is passed, or if the\n user has replied to this request already, or if the developer configured\n to skip it. Without passing this, an exit status of 6 is returned\n if permission is needed.\n", GRANT_AUTOMATIC_CHECKING_FLAG, CHECK_NOW_FLAG); fprintf(stderr, " --%s\n Choose to send system profile information if update permission is requested.\n This option can only take effect if --%s is passed.\n", SEND_PROFILE_FLAG, GRANT_AUTOMATIC_CHECKING_FLAG); fprintf(stderr, " --%s\n Defer installation until after the application terminates on its own. The\n application will not be relaunched unless the installation is resumed later.\n", DEFER_FLAG); fprintf(stderr, " --%s\n Enable verbose logging.\n", VERBOSE_FLAG); } int main(int argc, char **argv) { @autoreleasepool { struct option longOptions[] = { {APPLICATION_FLAG, required_argument, NULL, 0}, {CHANNELS_FLAG, required_argument, NULL, 0}, {FEED_URL_FLAG, required_argument, NULL, 0}, {USER_AGENT_NAME, required_argument, NULL, 0}, {DEFER_FLAG, no_argument, NULL, 0}, {VERBOSE_FLAG, no_argument, NULL, 0}, {CHECK_NOW_FLAG, no_argument, NULL, 0}, {GRANT_AUTOMATIC_CHECKING_FLAG, no_argument, NULL, 0}, {SEND_PROFILE_FLAG, no_argument, NULL, 0}, {PROBE_FLAG, no_argument, NULL, 0}, {INTERACTIVE_FLAG, no_argument, NULL, 0}, {ALLOW_MAJOR_UPGRADES_FLAG, no_argument, NULL, 0}, {0, 0, 0, 0} }; NSString *applicationPath = nil; NSString *feedURL = nil; NSString *userAgentName = nil; NSSet *channels = [NSSet set]; BOOL deferInstall = NO; BOOL verbose = NO; BOOL checkForUpdatesNow = NO; BOOL grantAutomaticChecking = NO; BOOL sendProfile = NO; BOOL probeForUpdates = NO; BOOL interactive = NO; BOOL allowMajorUpgrades = NO; while (YES) { int optionIndex = 0; int choice = getopt_long(argc, argv, "", longOptions, &optionIndex); if (choice == -1) { break; } switch (choice) { case 0: if (strcmp(APPLICATION_FLAG, longOptions[optionIndex].name) == 0) { assert(optarg != NULL); applicationPath = [[NSString alloc] initWithUTF8String:optarg]; if (applicationPath == nil) { printUsage(argv); return EXIT_FAILURE; } } else if (strcmp(FEED_URL_FLAG, longOptions[optionIndex].name) == 0) { assert(optarg != NULL); feedURL = [[NSString alloc] initWithUTF8String:optarg]; if (feedURL == nil) { printUsage(argv); return EXIT_FAILURE; } } else if (strcmp(USER_AGENT_NAME, longOptions[optionIndex].name) == 0) { assert(optarg != NULL); userAgentName = [[NSString alloc] initWithUTF8String:optarg]; if (userAgentName == nil) { printUsage(argv); return EXIT_FAILURE; } } else if (strcmp(CHANNELS_FLAG, longOptions[optionIndex].name) == 0) { assert(optarg != NULL); NSString *channelsString = [[NSString alloc] initWithUTF8String:optarg]; if (channelsString == nil) { printUsage(argv); return EXIT_FAILURE; } if (channelsString.length > 0) { channels = [NSSet setWithArray:[channelsString componentsSeparatedByString:@","]]; } } else if (strcmp(DEFER_FLAG, longOptions[optionIndex].name) == 0) { deferInstall = YES; } else if (strcmp(VERBOSE_FLAG, longOptions[optionIndex].name) == 0) { verbose = YES; } else if (strcmp(CHECK_NOW_FLAG, longOptions[optionIndex].name) == 0) { checkForUpdatesNow = YES; } else if (strcmp(GRANT_AUTOMATIC_CHECKING_FLAG, longOptions[optionIndex].name) == 0) { grantAutomaticChecking = YES; } else if (strcmp(SEND_PROFILE_FLAG, longOptions[optionIndex].name) == 0) { sendProfile = YES; } else if (strcmp(PROBE_FLAG, longOptions[optionIndex].name) == 0) { probeForUpdates = YES; } else if (strcmp(INTERACTIVE_FLAG, longOptions[optionIndex].name) == 0) { interactive = YES; } else if (strcmp(ALLOW_MAJOR_UPGRADES_FLAG, longOptions[optionIndex].name) == 0) { allowMajorUpgrades = YES; } case ':': break; case '?': printUsage(argv); return EXIT_FAILURE; default: abort(); } } if (optind >= argc) { printUsage(argv); return EXIT_FAILURE; } NSString *updatePath = [[NSString alloc] initWithUTF8String:argv[optind]]; if (updatePath == nil) { printUsage(argv); return EXIT_FAILURE; } if (probeForUpdates && (applicationPath != nil || deferInstall || checkForUpdatesNow || interactive)) { fprintf(stderr, "Error: --%s does not work together with --%s, --%s, --%s, --%s\n", PROBE_FLAG, APPLICATION_FLAG, DEFER_FLAG, CHECK_NOW_FLAG, INTERACTIVE_FLAG); return EXIT_FAILURE; } if (interactive && geteuid() == 0) { fprintf(stderr, "Error: --%s is not supported when running as root\n", INTERACTIVE_FLAG); return EXIT_FAILURE; } SUUpdatePermissionResponse *updatePermissionResponse = nil; if (grantAutomaticChecking) { updatePermissionResponse = [[SUUpdatePermissionResponse alloc] initWithAutomaticUpdateChecks:YES sendSystemProfile:sendProfile]; } SPUCommandLineDriver *driver = [[SPUCommandLineDriver alloc] initWithUpdateBundlePath:updatePath applicationBundlePath:applicationPath allowedChannels:channels customFeedURL:feedURL userAgentName:userAgentName updatePermissionResponse:updatePermissionResponse deferInstallation:deferInstall interactiveInstallation:interactive allowMajorUpgrades:allowMajorUpgrades verbose:verbose]; if (driver == nil) { fprintf(stderr, "Error: Failed to initialize updater. Are the bundle paths provided valid?\n"); return EXIT_FAILURE; } if (probeForUpdates) { [driver probeForUpdates]; } else { [driver runAndCheckForUpdatesNow:checkForUpdatesNow]; } [[NSRunLoop currentRunLoop] run]; } return EXIT_SUCCESS; }