Full Code of sparkle-project/Sparkle for AI

2.x 59ff700f5178 cached
577 files
3.1 MB
840.2k tokens
97 symbols
1 requests
Download .txt
Showing preview only (3,350K chars total). Download the full file or copy to clipboard to get everything.
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: ''

---

<!-- 

If this is a question or an idea, please consider posting in Discussions first:
https://github.com/sparkle-project/Sparkle/discussions

Otherwise the answer to your issue may already be in Console.app on your computer.
Please use Console.app and search for Sparkle.

Please try troubleshooting steps:
https://github.com/sparkle-project/Sparkle#troubleshooting

For security sensitive issues, please file them at https://github.com/sparkle-project/Sparkle/security

-->

## 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: ''

---

<!-- 

The answer to your issue is probably already in Console.app on your computer.
Please use Console.app and search for Sparkle.

Please try troubleshooting steps:
https://github.com/sparkle-project/Sparkle#troubleshooting

-->

### 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<<EOF'
                cat analyze_output.txt
                echo 'EOF'
            } >> $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 <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

@protocol AgentConnectionDelegate <NSObject>

- (void)agentConnectionDidInitiate;
- (void)agentConnectionDidInvalidate;

@end

@protocol SPUInstallerAgentProtocol;

SPU_OBJC_DIRECT_MEMBERS @interface AgentConnection : NSObject

- (instancetype)initWithHostBundleIdentifier:(NSString *)bundleIdentifier delegate:(id<AgentConnectionDelegate>)delegate;

- (void)startListener;
- (void)invalidate;

@property (nonatomic, readonly, nullable) id<SPUInstallerAgentProtocol> 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 () <NSXPCListenerDelegate, SUInstallerAgentInitiationProtocol>

@end

@implementation AgentConnection
{
    NSXPCListener *_xpcListener;
    NSXPCConnection *_activeConnection;
    __weak id<AgentConnectionDelegate> _delegate;
}

@synthesize agent = _agent;
@synthesize connected = _connected;
@synthesize invalidationError = _invalidationError;

- (instancetype)initWithHostBundleIdentifier:(NSString *)bundleIdentifier delegate:(id<AgentConnectionDelegate>)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 <Foundation/Foundation.h>
#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 <os/lock.h>


#include "AppKitPrevention.h"

#define FIRST_UPDATER_MESSAGE_TIMEOUT 18ull
#define RETRIEVE_PROCESS_IDENTIFIER_TIMEOUT 8ull

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

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

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

    SUUpdateValidator *_updateValidator;

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

    id<SUInstallerProtocol> _installer;

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

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

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

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

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

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

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

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

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

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

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

- (void)handleMessageWithIdentifier:(int32_t)identifier data:(NSData *)data
{
    if (identifier == SPUInstallationData && _updateDirectoryPath == nil) {
        dispatch_async(dispatch_get_main_queue(), ^{
            // Mark that we have received the installation data
            // Do not rely on eg: self->_updateDirectoryPath != nil because we may set it to nil again if an early stage fails (i.e, archive extraction)
            self->_receivedInstallationData = YES;
            
            SPUInstallationInputData *installationData = (data != nil) ? (SPUInstallationInputData *)SPUUnarchiveRootObjectSecurely(data, [SPUInstallationInputData class]) : nil;
            if (installationData == nil) {
                [self cleanupAndExitWithStatus:EXIT_FAILURE error:[NSError errorWithDomain:SUSparkleErrorDomain code:SPUInstallerError userInfo:@{ NSLocalizedDescriptionKey: @"Error: Failed to unarchive input installation data" }]];
                return;
            }
            
            NSString *installationType = installationData.installationType;
            if (!SPUValidInstallationType(installationType)) {
                [self cleanupAndExitWithStatus:EXIT_FAILURE error:[NSError errorWithDomain:SUSparkleErrorDomain code:SPUInstallerError userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Error: Received invalid installation type: %@", installationType] }]];
                return;
            }
            
            NSBundle *hostBundle = [NSBundle bundleWithPath:installationData.hostBundlePath];
            
            NSString *bundleIdentifier = hostBundle.bundleIdentifier;
            if (bundleIdentifier == nil || ![bundleIdentifier isEqualToString:self->_hostBundleIdentifier]) {
                [self cleanupAndExitWithStatus:EXIT_FAILURE error:[NSError errorWithDomain:SUSparkleErrorDomain code:SPUInstallerError userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Error: Failed to match host bundle identifiers %@ and %@", self->_hostBundleIdentifier, bundleIdentifier] }]];
                return;
            }
            
            // This will be important later
            if (installationData.relaunchPath == nil) {
                [self cleanupAndExitWithStatus:EXIT_FAILURE error:[NSError errorWithDomain:SUSparkleErrorDomain code:SPUInstallerError userInfo:@{ NSLocalizedDescriptionKey: @"Error: Failed to obtain relaunch path from installation data" }]];
                return;
            }
            
            // This installation path is specific to sparkle and the bundle identifier
            NSString *rootCacheInstallationPath = [[SPULocalCacheDirectory cachePathForBundleIdentifier:bundleIdentifier] stringByAppendingPathComponent:@"Installation"];
            
            [SPULocalCacheDirectory removeOldItemsInDirectory:rootCacheInstallationPath];
            
            NSString *cacheInstallationPath = [SPULocalCacheDirectory createUniqueDirectoryInDirectory:rootCacheInstallationPath];
            if (cacheInstallationPath == nil) {
                [self cleanupAndExitWithStatus:EXIT_FAILURE error:[NSError errorWithDomain:SUSparkleErrorDomain code:SPUInstallerError userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Error: Failed to create installation cache directory in %@", rootCacheInstallationPath] }]];
                return;
            }
            
            // Resolve the bookmark data for the downloaded update
            // See "Share file access between processes with URL bookmarks" in https://developer.apple.com/documentation/security/app_sandbox/accessing_files_from_the_macos_app_sandbox
            BOOL isStale = NO;
            NSError *bookmarkError = nil;
            NSURL *downloadURL = [NSURL URLByResolvingBookmarkData:installationData.updateURLBookmarkData options:NSURLBookmarkResolutionWithoutUI relativeToURL:nil bookmarkDataIsStale:&isStale error:&bookmarkError];
            if (downloadURL == nil) {
                [self cleanupAndExitWithStatus:EXIT_FAILURE error:[NSError errorWithDomain:SUSparkleErrorDomain code:SPUInstallerError userInfo:@{ NSLocalizedDescriptionKey: @"Error: Failed to resolve bookmark data from downloaded update", NSUnderlyingErrorKey: bookmarkError }]];
                
                return;
            }
            
            // Validate the download URL before moving it
            {
                NSArray<NSString *> *downloadURLPathComponents = downloadURL.URLByResolvingSymlinksInPath.pathComponents;
                if (downloadURLPathComponents == nil) {
                    [self cleanupAndExitWithStatus:EXIT_FAILURE error:[NSError errorWithDomain:SUSparkleErrorDomain code:SPUInstallerError userInfo:@{ NSLocalizedDescriptionKey: @"Error: Failed to retrieve path components from download URL" }]];
                    
                    return;
                }
                
                if ([downloadURLPathComponents containsObject:@".."]) {
                    [self cleanupAndExitWithStatus:EXIT_FAILURE error:[NSError errorWithDomain:SUSparkleErrorDomain code:SPUInstallerError userInfo:@{ NSLocalizedDescriptionKey: @"Error: download URL path components contains '..' which is unsafe" }]];
                    
                    return;
                }
                
                if (![downloadURLPathComponents containsObject:@SPARKLE_BUNDLE_IDENTIFIER] || ![downloadURLPathComponents containsObject:@"PersistentDownloads"]) {
                    [self cleanupAndExitWithStatus:EXIT_FAILURE error:[NSError errorWithDomain:SUSparkleErrorDomain code:SPUInstallerError userInfo:@{ NSLocalizedDescriptionKey: @"Error: download URL path components does not contain PersistentDownloads or "@SPARKLE_BUNDLE_IDENTIFIER }]];
                    
                    return;
                }
            }
            
            if (!isStale) {
                SULog(SULogLevelError, @"Error: bookmark data for update download is stale.. but still continuing.");
            }
            
            NSString *originalDownloadName = downloadURL.lastPathComponent;
            if (originalDownloadName == nil) {
                [self cleanupAndExitWithStatus:EXIT_FAILURE error:[NSError errorWithDomain:SUSparkleErrorDomain code:SPUInstallerError userInfo:@{ NSLocalizedDescriptionKey: @"Error: Failed to retrieve download name from download URL" }]];
                
                return;
            }
            
            // Randomize the download name if possible
            // This adds better security if there are any vulnerabilities in extracting/executing archives
            // which allow writing in unexpected locations. For zip/tar/dmg archives we may also extract them before
            // performing signing validation (due to key rotation).
            NSString *downloadName;
            NSString *randomizedUUIDString = [[NSUUID UUID] UUIDString];
            if (randomizedUUIDString != nil) {
                // Find the real path extension of the download name
                // We cannot use -[NSString pathExtension] because it may not give us the full path extension
                // E.g. for "foo.tar.xz" we need "tar.xz", not "xz"
                NSString *downloadPathExtension;
                NSRange pathExtensionDelimiterRange = [originalDownloadName rangeOfString:@"."];
                if (pathExtensionDelimiterRange.location == NSNotFound) {
                    downloadPathExtension = @"";
                } else {
                    downloadPathExtension = [originalDownloadName substringFromIndex:pathExtensionDelimiterRange.location + 1];
                }
                
                NSString *randomizedDownloadName = [randomizedUUIDString stringByAppendingPathExtension:downloadPathExtension];
                if (randomizedDownloadName != nil) {
                    downloadName = randomizedDownloadName;
                } else {
                    downloadName = originalDownloadName;
                }
            } else {
                downloadName = originalDownloadName;
            }
            
            // Move the download archive to somewhere where probably only we will be touching it
            // This prevents eg: if a bug exists in the updater that removes files we are trying to install
            // When this tool is ran as root, we are moving it into a directory that only root will have access to
            
            NSURL *downloadDestinationURL = [[NSURL fileURLWithPath:cacheInstallationPath] URLByAppendingPathComponent:downloadName];
            
            NSError *moveError = nil;
            if (![[[SUFileManager alloc] init] moveItemAtURL:downloadURL toURL:downloadDestinationURL error:&moveError]) {
                [self cleanupAndExitWithStatus:EXIT_FAILURE error:[NSError errorWithDomain:SUSparkleErrorDomain code:SPUInstallerError userInfo:@{ NSLocalizedDescriptionKey: @"Error: Failed to move download archive to new location", NSUnderlyingErrorKey: moveError }]];
                return;
            }
            
            // Make sure the downloaded archive we moved over is a regular file and not a symbolic link placed by an attacker
            NSError *attributesError = nil;
            NSString *downloadDestinationPath = downloadDestinationURL.path;
            if (downloadDestinationPath == nil) {
                [self cleanupAndExitWithStatus:EXIT_FAILURE error:[NSError errorWithDomain:SUSparkleErrorDomain code:SPUInstallerError userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Error: Failed to retrieve download archive path from %@", downloadDestinationURL] }]];
                
                return;
            }
            
            NSDictionary<NSString *, id> *archiveAttributes = [[NSFileManager defaultManager] attributesOfItemAtPath:downloadDestinationPath error:&attributesError];
            
            if (archiveAttributes == nil) {
                [self cleanupAndExitWithStatus:EXIT_FAILURE error:[NSError errorWithDomain:SUSparkleErrorDomain code:SPUInstallerError userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Error: Failed to retrieve download archive attributes from %@", downloadDestinationPath] }]];
                
                return;
            }
            
            if (![(NSString *)archiveAttributes[NSFileType] isEqualToString:NSFileTypeRegular]) {
                [self cleanupAndExitWithStatus:EXIT_FAILURE error:[NSError errorWithDomain:SUSparkleErrorDomain code:SPUInstallerError userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Error: Received bad archive file type: %@", archiveAttributes[NSFileType]] }]];
                return;
            }
            
            NSString *extractionDirectory = [SPULocalCacheDirectory createUniqueDirectoryInDirectory:cacheInstallationPath];
            if (extractionDirectory == nil) {
                [self cleanupAndExitWithStatus:EXIT_FAILURE error:[NSError errorWithDomain:SUSparkleErrorDomain code:SPUInstallerError userInfo:@{ NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Error: Failed to create installation extraction directory in %@", cacheInstallationPath] }]];
                
                return;
            }
            
            // Carry these properties separately rather than using the SUInstallationInputData object
            // Some of our properties may slightly differ than our input and we don't want to make the mistake of using one of those
            self->_installationType = installationType;
            self->_relaunchPath = installationData.relaunchPath;
            self->_downloadName = downloadName;
            self->_signatures = installationData.signatures;
            self->_updateDirectoryPath = cacheInstallationPath;
            self->_extractionDirectory = extractionDirectory;
            self->_decryptionPassword = installationData.decryptionPassword;
            self->_host = [[SUHost alloc] initWithBundle:hostBundle];
            self->_verifierInformation = [[SPUVerifierInformation alloc] initWithExpectedVersion:installationData.expectedVersion expectedContentLength:installationData.expectedContentLength];
            
            [self extractAndInstallUpdate];
        });
    } else if (identifier == SPUSentUpdateAppcastItemData) {
        SUAppcastItem *updateItem = (data != nil) ? (SUAppcastItem *)SPUUnarchiveRootObjectSecurely(data, [SUAppcastItem class]) : nil;
        if (updateItem != nil) {
            SPUInstallationInfo *installationInfo = [[SPUInstallationInfo alloc] initWithAppcastItem:updateItem];
            
            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 <SUInstallerProtocol> installer = [SUInstaller installerForHost:self->_host expectedInstallationType:self->_installationType updateDirectory:self->_extractionDirectory connectionCodeSigningValidationSkipped:connectionCodeSigningValidationSkipped homeDirectory:self->_homeDirectory userName:self->_userName error:&installerError];
        
        if (installer == nil) {
            dispatch_async(dispatch_get_main_queue(), ^{
                [self cleanupAndExitWithStatus:EXIT_FAILURE error:[NSError errorWithDomain:SUSparkleErrorDomain code:SPUInstallerError userInfo:@{ NSLocalizedDescriptionKey: @"Error: Failed to create installer instance", NSUnderlyingErrorKey: installerError }]];
            });
            return;
        }
        
        NSError *firstStageError = nil;
        if (![installer performInitialInstallation:&firstStageError]) {
            self->_installer = nil;
            
            dispatch_async(dispatch_get_main_queue(), ^{
                [self cleanupAndExitWithStatus:EXIT_FAILURE error:[NSError errorWithDomain:SUSparkleErrorDomain code:SPUInstallerError userInfo:@{ NSLocalizedDescriptionKey: @"Error: Failed to start installer", NSUnderlyingErrorKey: firstStageError }]];
            });
            return;
        }
        
        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 <Foundation/Foundation.h>

@protocol SPUDeltaArchiveProtocol;
@class SPUDeltaArchiveHeader;

NS_ASSUME_NONNULL_BEGIN

// Opens patch file for reading and decodes the archive header
id<SPUDeltaArchiveProtocol> 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<SPUDeltaArchiveProtocol> SPUDeltaArchiveReadPatchAndHeader(NSString *patchFile, SPUDeltaArchiveHeader * _Nullable __autoreleasing * _Nullable outHeader)
{
    id<SPUDeltaArchiveProtocol> 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<SPUDeltaArchiveProtocol> 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 <Foundation/Foundation.h>
#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 <NSObject>

@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 <Foundation/Foundation.h>

// 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 <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

@class SUAppcastItem;

SPU_OBJC_DIRECT_MEMBERS @interface SPUInstallationInfo : NSObject <NSSecureCoding>

- (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 <Foundation/Foundation.h>

@class SUSignatures;

NS_ASSUME_NONNULL_BEGIN

SPU_OBJC_DIRECT_MEMBERS @interface SPUInstallationInputData : NSObject <NSSecureCoding>

/*
 * 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 <Foundation/Foundation.h>

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 <Foundation/Foundation.h>

#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 <SPUDeltaArchiveProtocol>

- (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 <sys/stat.h>
#import <CommonCrypto/CommonDigest.h>
#import "SUBinaryDeltaCommon.h"
#import <compression.h>

#if SPARKLE_BUILD_BZIP2_DELTA_SUPPORT
#import <bzlib.h>
#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<SPUDeltaArchiveItem *> *_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<NSString *> *)_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<NSString *> *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<NSString *> *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<SPUDeltaArchiveItem *> *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<SPUDeltaArchiveItem *> *writableItems = _writableItems;
    
    // Build relative path table for tracking file clones
    NSMutableDictionary<NSString *, NSNumber *> *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<NSString *> *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 <Foundation/Foundation.h>

#import "SPUDeltaArchiveProtocol.h"
#import "SPUDeltaCompressionMode.h"

NS_ASSUME_NONNULL_BEGIN

// Legacy container format for binary delta archives
SPU_OBJC_DIRECT_MEMBERS @interface SPUXarDeltaArchive : NSObject <SPUDeltaArchiveProtocol>

- (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 <xar/xar.h>
#include "SUBinaryDeltaCommon.h"
#import <Availability.h>
#import <CommonCrypto/CommonDigest.h>


#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<NSString *, NSValue *> *_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<NSString *, NSValue *> *fileTable, xar_t x, NSString *relativePath, NSString *filePath)
{
    NSArray<NSString *> *rootRelativePathComponents = relativePath.pathComponents;
    // Relative path must at least have starting "/" component and one more path component
    if (rootRelativePathComponents.count < 2) {
        return NULL;
    }
    
    NSArray<NSString *> *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<NSString *> *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 <Foundation/Foundation.h>

@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 <CommonCrypto/CommonDigest.h>
#import <Foundation/Foundation.h>
#include "bspatch.h"
#include <stdio.h>
#include <stdlib.h>
#import <sys/stat.h>


#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<SPUDeltaArchiveProtocol> 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<NSFileAttributeKey, id> *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:&copyError]) {
                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 <Foundation/Foundation.h>
#import "SPUDeltaCompressionMode.h"
#include <fts.h>

#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<NSData *, NSMutableArray<NSString *> *> *hashToFileKeyDictionary, NSMutableDictionary<NSString *, NSData *> *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 <CommonCrypto/CommonDigest.h>
#include <zlib.h> // for crc32()
#include <Foundation/Foundation.h>
#include <fcntl.h>
#include <stdio.h>
#include <string.h>
#include <sys/mman.h>
#include <sys/param.h>
#include <sys/stat.h>
#include <xlocale.h>

#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.mutabl
Download .txt
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
Download .txt
SYMBOL INDEX (97 symbols across 21 files)

FILE: Sparkle/SPUStandardUpdaterController.h
  function SU_EXPORT (line 43) | SU_EXPORT NS_SWIFT_UI_ACTOR @interface SPUStandardUpdaterController : NS...

FILE: Sparkle/SPUUserUpdateState.h
  type SPUUserUpdateStageNotDownloaded (line 47) | typedef NS_ENUM(NSInteger, SPUUserUpdateStage) {

FILE: Sparkle/SUErrors.h
  type SPUNoUpdateFoundReasonUnknown (line 80) | typedef NS_ENUM(OSStatus, SPUNoUpdateFoundReason) {

FILE: Sparkle/SULog.h
  type SULogLevelDefault (line 14) | typedef NS_ENUM(uint8_t, SULogLevel) {

FILE: Vendor/bsdiff/bscommon.c
  function u_char (line 11) | u_char *readfile(const char *filename, off_t *outSize)

FILE: Vendor/bsdiff/bsdiff.c
  function off_t (line 48) | static off_t matchlen(u_char *old, off_t oldsize, u_char *newp, off_t ne...
  function off_t (line 68) | static off_t search(off_t *I, u_char *old, off_t oldsize,
  function offtout (line 99) | static void offtout(off_t x, u_char *buf)
  function bsdiff (line 124) | int bsdiff(int argc, char *argv[])

FILE: Vendor/bsdiff/bspatch.c
  type io_funcs_t (line 47) | typedef struct
  function stream_t (line 54) | static stream_t BSDIFF40_open(FILE *f)
  function BSDIFF40_close (line 64) | static void BSDIFF40_close(stream_t s)
  function off_t (line 70) | static off_t BSDIFF40_read(stream_t s, void *buf, off_t len)
  function stream_t (line 88) | static stream_t BSDIFN40_open(FILE *f)
  function BSDIFN40_close (line 93) | static void BSDIFN40_close(stream_t __unused s)
  function off_t (line 97) | static off_t BSDIFN40_read(stream_t s, void *buf, off_t len)
  type u_char (line 110) | typedef unsigned char u_char;
  function off_t (line 113) | static off_t offtin(u_char *buf)
  function bspatch (line 131) | int bspatch(int argc,const char * const argv[])

FILE: Vendor/bsdiff/sais.c
  function getCounts (line 53) | static
  function getBuckets (line 60) | static
  function LMSsort1 (line 69) | static
  function sais_index_type (line 110) | static
  function LMSsort2 (line 156) | static
  function sais_index_type (line 215) | static
  function induceSA (line 256) | static
  function sais_index_type (line 294) | static
  function sais_index_type (line 340) | static
  function sais_index_type (line 499) | sais_index_type
  function sais_index_type (line 506) | sais_index_type
  function sais_index_type (line 513) | sais_index_type
  function sais_index_type (line 528) | sais_index_type

FILE: Vendor/ed25519-sparkle/src/add_scalar.c
  function ed25519_add_scalar (line 8) | void ed25519_add_scalar(unsigned char *public_key, unsigned char *privat...

FILE: Vendor/ed25519-sparkle/src/fe.c
  function load_3 (line 8) | static uint64_t load_3(const unsigned char *in) {
  function load_4 (line 18) | static uint64_t load_4(const unsigned char *in) {
  function fe_0 (line 35) | void fe_0(fe h) {
  function fe_1 (line 54) | void fe_1(fe h) {
  function fe_add (line 81) | void fe_add(fe h, const fe f, const fe g) {
  function fe_cmov (line 134) | void fe_cmov(fe f, const fe g, unsigned int b) {
  function fe_cswap (line 197) | void fe_cswap(fe f,fe g,unsigned int b) {
  function fe_copy (line 267) | void fe_copy(fe h, const fe f) {
  function fe_frombytes (line 297) | void fe_frombytes(fe h, const unsigned char *s) {
  function fe_invert (line 364) | void fe_invert(fe out, const fe z) {
  function fe_isnegative (line 460) | int fe_isnegative(const fe f) {
  function fe_isnonzero (line 478) | int fe_isnonzero(const fe f) {
  function fe_mul (line 556) | void fe_mul(fe h, const fe f, const fe g) {
  function fe_mul121666 (line 779) | void fe_mul121666(fe h, fe f) {
  function fe_neg (line 846) | void fe_neg(fe h, const fe f) {
  function fe_pow22523 (line 881) | void fe_pow22523(fe out, const fe z) {
  function fe_sq (line 982) | void fe_sq(fe h, const fe f) {
  function fe_sq2 (line 1145) | void fe_sq2(fe h, const fe f) {
  function fe_sub (line 1315) | void fe_sub(fe h, const fe f, const fe g) {
  function fe_tobytes (line 1386) | void fe_tobytes(unsigned char *s, const fe h) {

FILE: Vendor/ed25519-sparkle/src/fixedint.h
  type __int64 (line 66) | typedef __int64 int64_t;

FILE: Vendor/ed25519-sparkle/src/ge.c
  function ge_add (line 9) | void ge_add(ge_p1p1 *r, const ge_p3 *p, const ge_cached *q) {
  function slide (line 25) | static void slide(signed char *r, const unsigned char *a) {
  function ge_double_scalarmult_vartime (line 67) | void ge_double_scalarmult_vartime(ge_p2 *r, const unsigned char *a, cons...
  function ge_frombytes_negate_vartime (line 141) | int ge_frombytes_negate_vartime(ge_p3 *h, const unsigned char *s) {
  function ge_madd (line 188) | void ge_madd(ge_p1p1 *r, const ge_p3 *p, const ge_precomp *q) {
  function ge_msub (line 207) | void ge_msub(ge_p1p1 *r, const ge_p3 *p, const ge_precomp *q) {
  function ge_p1p1_to_p2 (line 227) | void ge_p1p1_to_p2(ge_p2 *r, const ge_p1p1 *p) {
  function ge_p1p1_to_p3 (line 239) | void ge_p1p1_to_p3(ge_p3 *r, const ge_p1p1 *p) {
  function ge_p2_0 (line 247) | void ge_p2_0(ge_p2 *h) {
  function ge_p2_dbl (line 259) | void ge_p2_dbl(ge_p1p1 *r, const ge_p2 *p) {
  function ge_p3_0 (line 274) | void ge_p3_0(ge_p3 *h) {
  function ge_p3_dbl (line 286) | void ge_p3_dbl(ge_p1p1 *r, const ge_p3 *p) {
  function ge_p3_to_cached (line 302) | void ge_p3_to_cached(ge_cached *r, const ge_p3 *p) {
  function ge_p3_to_p2 (line 314) | void ge_p3_to_p2(ge_p2 *r, const ge_p3 *p) {
  function ge_p3_tobytes (line 321) | void ge_p3_tobytes(unsigned char *s, const ge_p3 *h) {
  function equal (line 333) | static unsigned char equal(signed char b, signed char c) {
  function negative (line 343) | static unsigned char negative(signed char b) {
  function cmov (line 349) | static void cmov(ge_precomp *t, const ge_precomp *u, unsigned char b) {
  function select (line 356) | static void select(ge_precomp *t, int pos, signed char b) {
  function ge_scalarmult_base (line 386) | void ge_scalarmult_base(ge_p3 *h, const unsigned char *a) {
  function ge_sub (line 441) | void ge_sub(ge_p1p1 *r, const ge_p3 *p, const ge_cached *q) {
  function ge_tobytes (line 458) | void ge_tobytes(unsigned char *s, const ge_p2 *h) {

FILE: Vendor/ed25519-sparkle/src/ge.h
  type ge_p2 (line 21) | typedef struct {
  type ge_p3 (line 27) | typedef struct {
  type ge_p1p1 (line 34) | typedef struct {
  type ge_precomp (line 41) | typedef struct {
  type ge_cached (line 47) | typedef struct {

FILE: Vendor/ed25519-sparkle/src/key_exchange.c
  function ed25519_key_exchange (line 4) | void ed25519_key_exchange(unsigned char *shared_secret, const unsigned c...

FILE: Vendor/ed25519-sparkle/src/keypair.c
  function ed25519_create_keypair (line 6) | void ed25519_create_keypair(unsigned char *public_key, unsigned char *pr...

FILE: Vendor/ed25519-sparkle/src/sc.c
  function load_3 (line 4) | static uint64_t load_3(const unsigned char *in) {
  function load_4 (line 14) | static uint64_t load_4(const unsigned char *in) {
  function sc_reduce (line 35) | void sc_reduce(unsigned char *s) {
  function sc_muladd (line 362) | void sc_muladd(unsigned char *s, const unsigned char *a, const unsigned ...

FILE: Vendor/ed25519-sparkle/src/seed.c
  function ed25519_create_seed (line 12) | int ed25519_create_seed(unsigned char *seed) {

FILE: Vendor/ed25519-sparkle/src/sha512.c
  function sha512_compress (line 91) | static int sha512_compress(sha512_context *md, unsigned char *buf)
  function sha512_init (line 147) | int sha512_init(sha512_context * md) {
  function sha512_update (line 171) | int sha512_update (sha512_context * md, const unsigned char *in, size_t ...
  function sha512_final (line 218) | int sha512_final(sha512_context * md, unsigned char *out)
  function sha512 (line 267) | int sha512(const unsigned char *message, size_t message_len, unsigned ch...

FILE: Vendor/ed25519-sparkle/src/sha512.h
  type sha512_context (line 9) | typedef struct sha512_context_ {

FILE: Vendor/ed25519-sparkle/src/sign.c
  function ed25519_sign (line 7) | void ed25519_sign(unsigned char *signature, const unsigned char *message...

FILE: Vendor/ed25519-sparkle/src/verify.c
  function consttime_equal (line 6) | static int consttime_equal(const unsigned char *x, const unsigned char *...
  function ed25519_verify (line 47) | int ed25519_verify(const unsigned char *signature, const unsigned char *...
Condensed preview — 577 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (3,421K chars).
[
  {
    "path": ".clang-format",
    "chars": 1380,
    "preview": "AccessModifierOffset: -4\nAlignEscapedNewlinesLeft: true\nAlignTrailingComments: false\nAllowAllParametersOfDeclarationOnNe"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/fixes-and-enhancements.md",
    "chars": 846,
    "preview": "---\nname: Enhancements and Bug Fixes\nabout: Found a bug? Want to implement a new feature? Something else?\ntitle: ''\nassi"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/sparkle-doesn-t-work-in-my-app.md",
    "chars": 884,
    "preview": "---\nname: Sparkle doesn't work in my app\nabout: Problems with integration, unexpected errors using Sparkle\ntitle: ''\nass"
  },
  {
    "path": ".github/pull_request_template.md",
    "chars": 570,
    "preview": "(Insert summary of your pull request here)\n\nFixes # (issue)\n\n## Misc Checklist\n\n- [ ] My change requires a documentation"
  },
  {
    "path": ".github/workflows/ci.yml",
    "chars": 4086,
    "preview": "name: Build & Tests\n\non:\n  push:\n    branches: [ 2.x, master ]\n  pull_request:\n    branches: [ 2.x, master ]\n\njobs:\n  bu"
  },
  {
    "path": ".github/workflows/create-draft-release.yml",
    "chars": 3932,
    "preview": "name: \"Create Draft Release\"\n\nenv:\n  BUILDDIR: \"build\"\n  DEVELOPER_DIR: \"/Applications/Xcode_26.2.app/Contents/Developer"
  },
  {
    "path": ".gitignore",
    "chars": 909,
    "preview": "build/\n*.pbxuser\n!default.pbxuser\n*.mode1v3\n!default.mode1v3\n*.mode2v3\n!default.mode2v3\n*.perspectivev3\n!default.perspec"
  },
  {
    "path": ".gitmodules",
    "chars": 0,
    "preview": ""
  },
  {
    "path": ".swiftlint.yml",
    "chars": 355,
    "preview": "excluded:\n  - Vendor\ndisabled_rules:\n  - opening_brace\n  - empty_parentheses_with_trailing_closure\n  - function_body_len"
  },
  {
    "path": "Autoupdate/AgentConnection.h",
    "chars": 815,
    "preview": "//\n//  AgentConnection.h\n//  Sparkle\n//\n//  Created by Mayur Pawashe on 7/17/16.\n//  Copyright © 2016 Sparkle Project. A"
  },
  {
    "path": "Autoupdate/AgentConnection.m",
    "chars": 4393,
    "preview": "//\n//  AgentConnection.m\n//  Sparkle\n//\n//  Created by Mayur Pawashe on 7/17/16.\n//  Copyright © 2016 Sparkle Project. A"
  },
  {
    "path": "Autoupdate/AppInstaller.h",
    "chars": 593,
    "preview": "//\n//  AppInstaller.h\n//  Sparkle\n//\n//  Created by Mayur Pawashe on 3/7/16.\n//  Copyright © 2016 Sparkle Project. All r"
  },
  {
    "path": "Autoupdate/AppInstaller.m",
    "chars": 41411,
    "preview": "//\n//  AppInstaller.m\n//  Sparkle\n//\n//  Created by Mayur Pawashe on 3/7/16.\n//  Copyright © 2016 Sparkle Project. All r"
  },
  {
    "path": "Autoupdate/SPUDeltaArchive.h",
    "chars": 514,
    "preview": "//\n//  SPUDeltaArchive.h\n//  Sparkle\n//\n//  Created by Mayur Pawashe on 12/29/21.\n//  Copyright © 2021 Sparkle Project. "
  },
  {
    "path": "Autoupdate/SPUDeltaArchive.m",
    "chars": 4054,
    "preview": "//\n//  SPUDeltaArchive.m\n//  Sparkle\n//\n//  Created by Mayur Pawashe on 12/29/21.\n//  Copyright © 2021 Sparkle Project. "
  },
  {
    "path": "Autoupdate/SPUDeltaArchiveProtocol.h",
    "chars": 4904,
    "preview": "//\n//  SPUDeltaArchiveProtocol.h\n//  Autoupdate\n//\n//  Created by Mayur Pawashe on 12/28/21.\n//  Copyright © 2021 Sparkl"
  },
  {
    "path": "Autoupdate/SPUDeltaCompressionMode.h",
    "chars": 576,
    "preview": "//\n//  SPUDeltaCompressionMode.h\n//  Sparkle\n//\n//  Created by Mayur Pawashe on 1/3/22.\n//  Copyright © 2022 Sparkle Pro"
  },
  {
    "path": "Autoupdate/SPUInstallationInfo.h",
    "chars": 513,
    "preview": "//\n//  SPUInstallationInfo.h\n//  Sparkle\n//\n//  Created by Mayur Pawashe on 4/10/16.\n//  Copyright © 2016 Sparkle Projec"
  },
  {
    "path": "Autoupdate/SPUInstallationInfo.m",
    "chars": 1775,
    "preview": "//\n//  SPUInstallationInfo.m\n//  Sparkle\n//\n//  Created by Mayur Pawashe on 4/10/16.\n//  Copyright © 2016 Sparkle Projec"
  },
  {
    "path": "Autoupdate/SPUInstallationInputData.h",
    "chars": 2016,
    "preview": "//\n//  SPUInstallationInputData.h\n//  Sparkle\n//\n//  Created by Mayur Pawashe on 3/24/16.\n//  Copyright © 2016 Sparkle P"
  },
  {
    "path": "Autoupdate/SPUInstallationInputData.m",
    "chars": 4566,
    "preview": "//\n//  SPUInstallationInputData.m\n//  Sparkle\n//\n//  Created by Mayur Pawashe on 3/24/16.\n//  Copyright © 2016 Sparkle P"
  },
  {
    "path": "Autoupdate/SPUMessageTypes.h",
    "chars": 1508,
    "preview": "//\n//  SPUMessageTypes.h\n//  Sparkle\n//\n//  Created by Mayur Pawashe on 3/11/16.\n//  Copyright © 2016 Sparkle Project. A"
  },
  {
    "path": "Autoupdate/SPUMessageTypes.m",
    "chars": 3177,
    "preview": "//\n//  SPUMessageTypes.m\n//  Sparkle\n//\n//  Created by Mayur Pawashe on 3/11/16.\n//  Copyright © 2016 Sparkle Project. A"
  },
  {
    "path": "Autoupdate/SPUSparkleDeltaArchive.h",
    "chars": 4142,
    "preview": "//\n//  SPUSparkleDeltaArchive.h\n//  Sparkle\n//\n//  Created by Mayur Pawashe on 12/30/21.\n//  Copyright © 2021 Sparkle Pr"
  },
  {
    "path": "Autoupdate/SPUSparkleDeltaArchive.m",
    "chars": 58944,
    "preview": "//\n//  SPUSparkleDeltaArchive.m\n//  Sparkle\n//\n//  Created by Mayur Pawashe on 12/30/21.\n//  Copyright © 2021 Sparkle Pr"
  },
  {
    "path": "Autoupdate/SPUXarDeltaArchive.h",
    "chars": 672,
    "preview": "//\n//  SPUXarDeltaArchive.h\n//  Autoupdate\n//\n//  Created by Mayur Pawashe on 12/28/21.\n//  Copyright © 2021 Sparkle Pro"
  },
  {
    "path": "Autoupdate/SPUXarDeltaArchive.m",
    "chars": 15035,
    "preview": "//\n//  SPUXarDeltaArchive.m\n//  Autoupdate\n//\n//  Created by Mayur Pawashe on 12/28/21.\n//  Copyright © 2021 Sparkle Pro"
  },
  {
    "path": "Autoupdate/SUBinaryDeltaApply.h",
    "chars": 424,
    "preview": "//\n//  SUBinaryDeltaApply.h\n//  Sparkle\n//\n//  Created by Mark Rowe on 2009-06-01.\n//  Copyright 2009 Mark Rowe. All rig"
  },
  {
    "path": "Autoupdate/SUBinaryDeltaApply.m",
    "chars": 23084,
    "preview": "//\n//  SUBinaryDeltaApply.m\n//  Sparkle\n//\n//  Created by Mark Rowe on 2009-06-01.\n//  Copyright 2009 Mark Rowe. All rig"
  },
  {
    "path": "Autoupdate/SUBinaryDeltaCommon.h",
    "chars": 4744,
    "preview": "//\n//  SUBinaryDeltaCommon.h\n//  Sparkle\n//\n//  Created by Mark Rowe on 2009-06-01.\n//  Copyright 2009 Mark Rowe. All ri"
  },
  {
    "path": "Autoupdate/SUBinaryDeltaCommon.m",
    "chars": 17272,
    "preview": "//\n//  SUBinaryDeltaCommon.m\n//  Sparkle\n//\n//  Created by Mark Rowe on 2009-06-01.\n//  Copyright 2009 Mark Rowe. All ri"
  },
  {
    "path": "Autoupdate/SUBinaryDeltaCreate.h",
    "chars": 541,
    "preview": "//\n//  SUBinaryDeltaCreate.m\n//  Sparkle\n//\n//  Created by Mayur Pawashe on 4/9/15.\n//  Copyright (c) 2015 Sparkle Proje"
  },
  {
    "path": "Autoupdate/SUBinaryDeltaCreate.m",
    "chars": 45798,
    "preview": "//\n//  SUBinaryDeltaCreate.m\n//  Sparkle\n//\n//  Created by Mayur Pawashe on 4/9/15.\n//  Copyright (c) 2015 Sparkle Proje"
  },
  {
    "path": "Autoupdate/SUBinaryDeltaUnarchiver.h",
    "chars": 685,
    "preview": "//\n//  SUBinaryDeltaUnarchiver.h\n//  Sparkle\n//\n//  Created by Mark Rowe on 2009-06-03.\n//  Copyright 2009 Mark Rowe. Al"
  },
  {
    "path": "Autoupdate/SUBinaryDeltaUnarchiver.m",
    "chars": 4555,
    "preview": "//\n//  SUBinaryDeltaUnarchiver.m\n//  Sparkle\n//\n//  Created by Mark Rowe on 2009-06-03.\n//  Copyright 2009 Mark Rowe. Al"
  },
  {
    "path": "Autoupdate/SUCodeSigningVerifier.h",
    "chars": 1847,
    "preview": "//\n//  SUCodeSigningVerifier.h\n//  Sparkle\n//\n//  Created by Andy Matuschak on 7/5/12.\n//\n//\n\n#ifndef SUCODESIGNINGVERIF"
  },
  {
    "path": "Autoupdate/SUCodeSigningVerifier.m",
    "chars": 23936,
    "preview": "//\n//  SUCodeSigningVerifier.m\n//  Sparkle\n//\n//  Created by Andy Matuschak on 7/5/12.\n//\n//\n\n#include <Security/CodeSig"
  },
  {
    "path": "Autoupdate/SUDiskImageUnarchiver.h",
    "chars": 651,
    "preview": "//\n//  SUDiskImageUnarchiver.h\n//  Sparkle\n//\n//  Created by Andy Matuschak on 6/16/08.\n//  Copyright 2008 Andy Matuscha"
  },
  {
    "path": "Autoupdate/SUDiskImageUnarchiver.m",
    "chars": 12295,
    "preview": "//\n//  SUDiskImageUnarchiver.m\n//  Sparkle\n//\n//  Created by Andy Matuschak on 6/16/08.\n//  Copyright 2008 Andy Matuscha"
  },
  {
    "path": "Autoupdate/SUFlatPackageUnarchiver.h",
    "chars": 708,
    "preview": "//\n//  SUFlatPackageUnarchiver.h\n//  Autoupdate\n//\n//  Created by Mayur Pawashe on 1/30/21.\n//  Copyright © 2021 Sparkle"
  },
  {
    "path": "Autoupdate/SUFlatPackageUnarchiver.m",
    "chars": 3461,
    "preview": "//\n//  SUFlatPackageUnarchiver.m\n//  Autoupdate\n//\n//  Created by Mayur Pawashe on 1/30/21.\n//  Copyright © 2021 Sparkle"
  },
  {
    "path": "Autoupdate/SUGuidedPackageInstaller.h",
    "chars": 1001,
    "preview": "//\n//  SUGuidedPackageInstaller.h\n//  Sparkle\n//\n//  Created by Graham Miln on 14/05/2010.\n//  Copyright 2010 Dragon Sys"
  },
  {
    "path": "Autoupdate/SUGuidedPackageInstaller.m",
    "chars": 2538,
    "preview": "//\n//  SUGuidedPackageInstaller.m\n//  Sparkle\n//\n//  Created by Graham Miln on 14/05/2010.\n//  Copyright 2010 Dragon Sys"
  },
  {
    "path": "Autoupdate/SUInstaller.h",
    "chars": 950,
    "preview": "//\n//  SUInstaller.h\n//  Sparkle\n//\n//  Created by Andy Matuschak on 4/10/08.\n//  Copyright 2008 Andy Matuschak. All rig"
  },
  {
    "path": "Autoupdate/SUInstaller.m",
    "chars": 15454,
    "preview": "//\n//  SUInstaller.m\n//  Sparkle\n//\n//  Created by Andy Matuschak on 4/10/08.\n//  Copyright 2008 Andy Matuschak. All rig"
  },
  {
    "path": "Autoupdate/SUInstallerProtocol.h",
    "chars": 1188,
    "preview": "//\n//  SUInstallerProtocol.h\n//  Sparkle\n//\n//  Created by Mayur Pawashe on 3/12/16.\n//  Copyright © 2016 Sparkle Projec"
  },
  {
    "path": "Autoupdate/SUPipedUnarchiver.h",
    "chars": 576,
    "preview": "//\n//  SUPipedUnarchiver.h\n//  Sparkle\n//\n//  Created by Andy Matuschak on 6/16/08.\n//  Copyright 2008 Andy Matuschak. A"
  },
  {
    "path": "Autoupdate/SUPipedUnarchiver.m",
    "chars": 15784,
    "preview": "//\n//  SUPipedUnarchiver.m\n//  Sparkle\n//\n//  Created by Andy Matuschak on 6/16/08.\n//  Copyright 2008 Andy Matuschak. A"
  },
  {
    "path": "Autoupdate/SUPlainInstaller.h",
    "chars": 670,
    "preview": "//\n//  SUPlainInstaller.h\n//  Sparkle\n//\n//  Created by Andy Matuschak on 4/10/08.\n//  Copyright 2008 Andy Matuschak. Al"
  },
  {
    "path": "Autoupdate/SUPlainInstaller.m",
    "chars": 18982,
    "preview": "//\n//  SUPlainInstaller.m\n//  Sparkle\n//\n//  Created by Andy Matuschak on 4/10/08.\n//  Copyright 2008 Andy Matuschak. Al"
  },
  {
    "path": "Autoupdate/SUSignatureVerifier.h",
    "chars": 1773,
    "preview": "//\n//  SUSignatureVerifier.h\n//  Sparkle\n//\n//  Created by Andy Matuschak on 3/16/06.\n//  Copyright 2006 Andy Matuschak."
  },
  {
    "path": "Autoupdate/SUSignatureVerifier.m",
    "chars": 20663,
    "preview": "//\n//  SUDSAVerifier.m\n//  Sparkle\n//\n//  Created by Andy Matuschak on 3/16/06.\n//  Copyright 2006 Andy Matuschak. All r"
  },
  {
    "path": "Autoupdate/SUStatusInfoProtocol.h",
    "chars": 426,
    "preview": "//\n//  SUStatusInfoProtocol.h\n//  Sparkle\n//\n//  Created by Mayur Pawashe on 7/10/16.\n//  Copyright © 2016 Sparkle Proje"
  },
  {
    "path": "Autoupdate/SUUnarchiver.h",
    "chars": 614,
    "preview": "//\n//  SUUnarchiver.h\n//  Sparkle\n//\n//  Created by Andy Matuschak on 3/16/06.\n//  Copyright 2006 Andy Matuschak. All ri"
  },
  {
    "path": "Autoupdate/SUUnarchiver.m",
    "chars": 1748,
    "preview": "//\n//  SUUnarchiver.m\n//  Sparkle\n//\n//  Created by Andy Matuschak on 3/16/06.\n//  Copyright 2006 Andy Matuschak. All ri"
  },
  {
    "path": "Autoupdate/SUUnarchiverNotifier.h",
    "chars": 578,
    "preview": "//\n//  SUUnarchiverNotifier.h\n//  Sparkle\n//\n//  Created by Mayur Pawashe on 12/21/16.\n//  Copyright © 2016 Sparkle Proj"
  },
  {
    "path": "Autoupdate/SUUnarchiverNotifier.m",
    "chars": 1655,
    "preview": "//\n//  SUUnarchiverNotifier.m\n//  Sparkle\n//\n//  Created by Mayur Pawashe on 12/21/16.\n//  Copyright © 2016 Sparkle Proj"
  },
  {
    "path": "Autoupdate/SUUnarchiverProtocol.h",
    "chars": 595,
    "preview": "//\n//  SUUnarchiverProtocol.h\n//  Sparkle\n//\n//  Created by Mayur Pawashe on 3/26/16.\n//  Copyright © 2016 Sparkle Proje"
  },
  {
    "path": "Autoupdate/StatusInfo.h",
    "chars": 525,
    "preview": "//\n//  StatusInfo.h\n//  Sparkle\n//\n//  Created by Mayur Pawashe on 7/10/16.\n//  Copyright © 2016 Sparkle Project. All ri"
  },
  {
    "path": "Autoupdate/StatusInfo.m",
    "chars": 3079,
    "preview": "//\n//  StatusInfo.m\n//  Sparkle\n//\n//  Created by Mayur Pawashe on 7/10/16.\n//  Copyright © 2016 Sparkle Project. All ri"
  },
  {
    "path": "Autoupdate/main.m",
    "chars": 1753,
    "preview": "#import <Foundation/Foundation.h>\n#import \"AppInstaller.h\"\n\n\n#include \"AppKitPrevention.h\"\n\nint main(int __unused argc, "
  },
  {
    "path": "BinaryDelta/Bridging-Header.h",
    "chars": 231,
    "preview": "//\n//  Use this file to import your target's public headers that you would like to expose to Swift.\n//\n\n#import \"SUBinar"
  },
  {
    "path": "BinaryDelta/main.swift",
    "chars": 9410,
    "preview": "//\n//  main.swift\n//  BinaryDelta\n//\n//  Created by Mayur Pawashe on 1/3/22.\n//  Copyright © 2022 Sparkle Project. All r"
  },
  {
    "path": "CHANGELOG",
    "chars": 71512,
    "preview": "# 2.9.0\n\n* Add basic markdown support for release notes (requires macOS 12+) including [customizing its presentation](ht"
  },
  {
    "path": "CODE_OF_CONDUCT.md",
    "chars": 3235,
    "preview": "# Contributor Covenant Code of Conduct\n\n## Our Pledge\n\nIn the interest of fostering an open and welcoming environment, w"
  },
  {
    "path": "Carthage-dev.json",
    "chars": 5444,
    "preview": "{\"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\": "
  },
  {
    "path": "Configurations/CommandLineTool-Debug.xcconfig",
    "chars": 107,
    "preview": "// Generate Appcast Debug\n\n#include \"CommandLineTool-Shared.xcconfig\"\n#include \"ConfigSwiftDebug.xcconfig\"\n"
  },
  {
    "path": "Configurations/CommandLineTool-Release.xcconfig",
    "chars": 111,
    "preview": "// Generate Appcast Release\n\n#include \"CommandLineTool-Shared.xcconfig\"\n#include \"ConfigSwiftRelease.xcconfig\"\n"
  },
  {
    "path": "Configurations/CommandLineTool-Shared.xcconfig",
    "chars": 243,
    "preview": "// Generate Appcast only\n\n#include \"ConfigSwift.xcconfig\"\n\nSWIFT_OBJC_BRIDGING_HEADER = $(PRODUCT_NAME)/Bridging-Header."
  },
  {
    "path": "Configurations/ConfigCommon.xcconfig",
    "chars": 9986,
    "preview": "// Common\n\n// Set to 1 or 0 to build and bundle the provided XPC Services in Sparkle framework\n// (Note for testing purp"
  },
  {
    "path": "Configurations/ConfigCommonCoverage.xcconfig",
    "chars": 451,
    "preview": "#include \"ConfigCommonDebug.xcconfig\"\n\nGCC_PREPROCESSOR_DEFINITIONS = $(GCC_PREPROCESSOR_DEFINITIONS_COMMON)\nOTHER_SWIFT"
  },
  {
    "path": "Configurations/ConfigCommonDebug.xcconfig",
    "chars": 557,
    "preview": "#include \"ConfigCommon.xcconfig\"\n\n// Debug only\n\nCLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;\nCLANG_WARN_COMMA = YES;\nC"
  },
  {
    "path": "Configurations/ConfigCommonRelease.xcconfig",
    "chars": 648,
    "preview": "#include \"ConfigCommon.xcconfig\"\n\n// Release only\n\nCLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;\nCLANG_WARN_COMMA = YES;"
  },
  {
    "path": "Configurations/ConfigDownloader.xcconfig",
    "chars": 355,
    "preview": "// Downloader\n\nINFOPLIST_FILE = Downloader/Info.plist\nWRAPPER_EXTENSION = xpc\nPRODUCT_BUNDLE_IDENTIFIER = ${DOWNLOADER_B"
  },
  {
    "path": "Configurations/ConfigDownloaderDebug.xcconfig",
    "chars": 57,
    "preview": "// Downloader Debug\n#include \"ConfigDownloader.xcconfig\"\n"
  },
  {
    "path": "Configurations/ConfigFramework.xcconfig",
    "chars": 1315,
    "preview": "// Framework only\n\nDYLIB_INSTALL_NAME_BASE = @rpath\nDYLIB_COMPATIBILITY_VERSION = 1.6\nDYLIB_CURRENT_VERSION = $(SPARKLE_"
  },
  {
    "path": "Configurations/ConfigFrameworkDebug.xcconfig",
    "chars": 117,
    "preview": "#include \"ConfigFramework.xcconfig\"\n\n// Unit tests need access to non-public classes\nGCC_SYMBOLS_PRIVATE_EXTERN = NO\n"
  },
  {
    "path": "Configurations/ConfigFrameworkRelease.xcconfig",
    "chars": 120,
    "preview": "#include \"ConfigFramework.xcconfig\"\n\n// Strip all non-global symbols (including debug symbols)\nSTRIP_STYLE = non-global\n"
  },
  {
    "path": "Configurations/ConfigInstallerConnection.xcconfig",
    "chars": 331,
    "preview": "// InstallerConnection\n\nINFOPLIST_FILE = InstallerConnection/Info.plist\nWRAPPER_EXTENSION = xpc\nPRODUCT_BUNDLE_IDENTIFIE"
  },
  {
    "path": "Configurations/ConfigInstallerConnectionDebug.xcconfig",
    "chars": 76,
    "preview": "// InstallerConnection Debug\n\n#include \"ConfigInstallerConnection.xcconfig\"\n"
  },
  {
    "path": "Configurations/ConfigInstallerLauncher.xcconfig",
    "chars": 324,
    "preview": "// Installer Launcher\n\nINFOPLIST_FILE = InstallerLauncher/Info.plist\nWRAPPER_EXTENSION = xpc\nPRODUCT_BUNDLE_IDENTIFIER ="
  },
  {
    "path": "Configurations/ConfigInstallerLauncherDebug.xcconfig",
    "chars": 72,
    "preview": "// Installer Launcher Debug\n#include \"ConfigInstallerLauncher.xcconfig\"\n"
  },
  {
    "path": "Configurations/ConfigInstallerProgress.xcconfig",
    "chars": 393,
    "preview": "// Installer Progress only\n\nINFOPLIST_FILE = Sparkle/InstallerProgress/InstallerProgress-Info.plist\nPRODUCT_NAME = $(SPA"
  },
  {
    "path": "Configurations/ConfigInstallerStatus.xcconfig",
    "chars": 334,
    "preview": "// InstallerStatus\n\nINFOPLIST_FILE = InstallerStatus/Info.plist\nWRAPPER_EXTENSION = xpc\nPRODUCT_BUNDLE_IDENTIFIER = ${IN"
  },
  {
    "path": "Configurations/ConfigInstallerStatusDebug.xcconfig",
    "chars": 68,
    "preview": "// InstallerStatus Debug\n\n#include \"ConfigInstallerStatus.xcconfig\"\n"
  },
  {
    "path": "Configurations/ConfigRelaunch.xcconfig",
    "chars": 214,
    "preview": "// Relaunch Tool only\n\nPRODUCT_NAME = $(SPARKLE_RELAUNCH_TOOL_NAME)\nSKIP_INSTALL = YES\nCLANG_ENABLE_MODULES = NO\nGCC_PRE"
  },
  {
    "path": "Configurations/ConfigSparkleTool.xcconfig",
    "chars": 245,
    "preview": "// sparkle command line tool only\n\nINFOPLIST_FILE = sparkle-cli/Info.plist\nPRODUCT_BUNDLE_IDENTIFIER = org.sparkle-proje"
  },
  {
    "path": "Configurations/ConfigSwift.xcconfig",
    "chars": 53,
    "preview": "SWIFT_VERSION = 5\nSWIFT_SWIFT3_OBJC_INFERENCE = Off;\n"
  },
  {
    "path": "Configurations/ConfigSwiftDebug.xcconfig",
    "chars": 34,
    "preview": "SWIFT_OPTIMIZATION_LEVEL = -Onone\n"
  },
  {
    "path": "Configurations/ConfigSwiftRelease.xcconfig",
    "chars": 67,
    "preview": "SWIFT_OPTIMIZATION_LEVEL = -O\nSWIFT_COMPILATION_MODE = wholemodule\n"
  },
  {
    "path": "Configurations/ConfigTestApp.xcconfig",
    "chars": 383,
    "preview": "// Test Application\n\nINFOPLIST_FILE = TestApplication/TestApplication-Info.plist\nWRAPPER_EXTENSION = app\nASSETCATALOG_CO"
  },
  {
    "path": "Configurations/ConfigTestAppDebug.xcconfig",
    "chars": 60,
    "preview": "// Test Application Debug\n#include \"ConfigTestApp.xcconfig\"\n"
  },
  {
    "path": "Configurations/ConfigTestAppHelper.xcconfig",
    "chars": 251,
    "preview": "// Test Application Helper\n\nINFOPLIST_FILE = TestAppHelper/Info.plist\nWRAPPER_EXTENSION = xpc\nLD_RUNPATH_SEARCH_PATHS = "
  },
  {
    "path": "Configurations/ConfigTestAppHelperDebug.xcconfig",
    "chars": 73,
    "preview": "// Test Application Helper Debug\n#include \"ConfigTestAppHelper.xcconfig\"\n"
  },
  {
    "path": "Configurations/ConfigUITest.xcconfig",
    "chars": 328,
    "preview": "// UI Test only\n\n#include \"ConfigSwift.xcconfig\"\n\nINFOPLIST_FILE = UITests/UITests-Info.plist\nWRAPPER_EXTENSION = xctest"
  },
  {
    "path": "Configurations/ConfigUITestCoverage.xcconfig",
    "chars": 147,
    "preview": "#include \"ConfigUITest.xcconfig\"\n#include \"ConfigSwiftDebug.xcconfig\"\n\nGCC_GENERATE_TEST_COVERAGE_FILES = NO\nGCC_INSTRUM"
  },
  {
    "path": "Configurations/ConfigUITestDebug.xcconfig",
    "chars": 70,
    "preview": "#include \"ConfigUITest.xcconfig\"\n#include \"ConfigSwiftDebug.xcconfig\"\n"
  },
  {
    "path": "Configurations/ConfigUITestRelease.xcconfig",
    "chars": 72,
    "preview": "#include \"ConfigUITest.xcconfig\"\n#include \"ConfigSwiftRelease.xcconfig\"\n"
  },
  {
    "path": "Configurations/ConfigUnitTest.xcconfig",
    "chars": 719,
    "preview": "// Unit Test only\n\n#include \"ConfigSwift.xcconfig\"\n\nINFOPLIST_FILE = Tests/SparkleTests-Info.plist\nWRAPPER_EXTENSION = x"
  },
  {
    "path": "Configurations/ConfigUnitTestCoverage.xcconfig",
    "chars": 149,
    "preview": "#include \"ConfigUnitTest.xcconfig\"\n#include \"ConfigSwiftDebug.xcconfig\"\n\nGCC_GENERATE_TEST_COVERAGE_FILES = NO\nGCC_INSTR"
  },
  {
    "path": "Configurations/ConfigUnitTestDebug.xcconfig",
    "chars": 72,
    "preview": "#include \"ConfigUnitTest.xcconfig\"\n#include \"ConfigSwiftDebug.xcconfig\"\n"
  },
  {
    "path": "Configurations/ConfigUnitTestRelease.xcconfig",
    "chars": 74,
    "preview": "#include \"ConfigUnitTest.xcconfig\"\n#include \"ConfigSwiftRelease.xcconfig\"\n"
  },
  {
    "path": "Configurations/bsdiff-Debug.xcconfig",
    "chars": 94,
    "preview": "//  Copyright © 2019 Sparkle Project. All rights reserved.\n\n#include \"bsdiff-Shared.xcconfig\"\n"
  },
  {
    "path": "Configurations/bsdiff-Release.xcconfig",
    "chars": 202,
    "preview": "//  Copyright © 2019 Sparkle Project. All rights reserved.\n\n#include \"bsdiff-Shared.xcconfig\"\n\n// Disable asserts for pe"
  },
  {
    "path": "Configurations/bsdiff-Shared.xcconfig",
    "chars": 459,
    "preview": "//  Copyright © 2019 Sparkle Project. All rights reserved.\n\n// Deployment\nSKIP_INSTALL = YES\n\n// Packaging\nEXECUTABLE_PR"
  },
  {
    "path": "Configurations/ed25519-Debug.xcconfig",
    "chars": 95,
    "preview": "//  Copyright © 2019 Sparkle Project. All rights reserved.\n\n#include \"ed25519-Shared.xcconfig\"\n"
  },
  {
    "path": "Configurations/ed25519-Release.xcconfig",
    "chars": 95,
    "preview": "//  Copyright © 2019 Sparkle Project. All rights reserved.\n\n#include \"ed25519-Shared.xcconfig\"\n"
  },
  {
    "path": "Configurations/ed25519-Shared.xcconfig",
    "chars": 582,
    "preview": "//  Copyright © 2019 Sparkle Project. All rights reserved.\n\n// Deployment\nSKIP_INSTALL = YES\n\n// Packaging\nEXECUTABLE_PR"
  },
  {
    "path": "Configurations/generate_latest_changes.py",
    "chars": 754,
    "preview": "#!/usr/bin/env python3\n\nimport os, sys\n\n# Ignore the first version line starting with # x.y.z..\n# and print everything u"
  },
  {
    "path": "Configurations/link-tools.sh",
    "chars": 909,
    "preview": "#!/bin/sh\n\n# If Carthage is trying to build us, it won't preserve code signing information from our bundled tools proper"
  },
  {
    "path": "Configurations/make-release-package.sh",
    "chars": 8242,
    "preview": "#!/bin/bash\nset -e\n\n# Tests the code signing validity of the extracted products within the provided path.\n# This guards "
  },
  {
    "path": "Configurations/make-xcframework.sh",
    "chars": 1400,
    "preview": "#!/bin/bash\n\n# Deleting old products\nrm -rd \"$BUILT_PRODUCTS_DIR/Sparkle.xcarchive\"\nrm -rd \"$BUILT_PRODUCTS_DIR/Sparkle."
  },
  {
    "path": "Configurations/release-move-tag.sh",
    "chars": 1785,
    "preview": "#!/bin/bash\nset -e\n\n# Convenience script to automatically commit Package.swift after updating the checksum and move the "
  },
  {
    "path": "Configurations/set-git-version-info.sh",
    "chars": 1140,
    "preview": "#!/bin/sh\nset -e\n\nif ! which -s git ; then\n    exit 0\nfi\n\nif [ -z \"$PROJECT_DIR\" ] || \\\n   [ -z \"$BUILT_PRODUCTS_DIR\" ] "
  },
  {
    "path": "Configurations/strip-framework.sh",
    "chars": 2231,
    "preview": "#!/bin/sh\n\nFRAMEWORK_PATH=\"${TARGET_BUILD_DIR}\"/\"${FULL_PRODUCT_NAME}\"\n\n# Remove any unused XPC Services\n\nremovedservice"
  },
  {
    "path": "Configurations/update-carthage.py",
    "chars": 541,
    "preview": "#!/usr/bin/env python3\n\nimport os, sys, json\n\nif len(sys.argv) < 3:\n    print(\"Usage: path-to-carthage-file release-tag\""
  },
  {
    "path": "Documentation/.gitignore",
    "chars": 5,
    "preview": "html\n"
  },
  {
    "path": "Documentation/API_README.markdown",
    "chars": 731,
    "preview": "# Sparkle 2 API Reference\n\nThese are the primary classes and protocols in Sparkle 2 you may be interested in:\n\n- `SPUSta"
  },
  {
    "path": "Documentation/Design Practices.md",
    "chars": 5865,
    "preview": "# Design Practices\n\n## XPC Services\n\nXPC services in Sparkle are all optional, so the code involved in the services need"
  },
  {
    "path": "Documentation/Installation.md",
    "chars": 22532,
    "preview": "# Details on Installer IPC & Security\n\n## Important components:\n\n* The bundle to update & replace\n* The application to l"
  },
  {
    "path": "Documentation/Security.md",
    "chars": 5327,
    "preview": "# Security\n\nFirst, some references I've found to be quite useful:\n\n* WWDC 2010 video on \"Creating Secure Applications\" -"
  },
  {
    "path": "Downloader/Downloader.entitlements",
    "chars": 295,
    "preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/P"
  },
  {
    "path": "Downloader/Info.plist",
    "chars": 1134,
    "preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/P"
  },
  {
    "path": "Downloader/SPUDownloader.h",
    "chars": 754,
    "preview": "//\n//  SPUDownloader.h\n//  Downloader\n//\n//  Created by Mayur Pawashe on 4/1/16.\n//  Copyright © 2016 Sparkle Project. A"
  },
  {
    "path": "Downloader/SPUDownloader.m",
    "chars": 14016,
    "preview": "//\n//  SPUDownloader.m\n//  Downloader\n//\n//  Created by Mayur Pawashe on 4/1/16.\n//  Copyright © 2016 Sparkle Project. A"
  },
  {
    "path": "Downloader/SPUDownloaderDelegate.h",
    "chars": 1090,
    "preview": "//\n//  SPUDownloaderDelegate.h\n//  Sparkle\n//\n//  Created by Mayur Pawashe on 4/1/16.\n//  Copyright © 2016 Sparkle Proje"
  },
  {
    "path": "Downloader/SPUDownloaderProtocol.h",
    "chars": 823,
    "preview": "//\n//  SPUDownloaderProtocol.h\n//  PersistentDownloader\n//\n//  Created by Mayur Pawashe on 4/1/16.\n//  Copyright © 2016 "
  },
  {
    "path": "Downloader/main.m",
    "chars": 3450,
    "preview": "//\n//  main.m\n//  PersistentDownloader\n//\n//  Created by Mayur Pawashe on 4/1/16.\n//  Copyright © 2016 Sparkle Project. "
  },
  {
    "path": "INSTALL",
    "chars": 234,
    "preview": "For integration and usage, please visit Sparkle's Documentation:\nhttps://sparkle-project.org/documentation/\n\nFor integra"
  },
  {
    "path": "InstallerConnection/Info.plist",
    "chars": 981,
    "preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/P"
  },
  {
    "path": "InstallerConnection/SUInstallerCommunicationProtocol.h",
    "chars": 370,
    "preview": "//\n//  SUInstallerCommunicationProtocol.h\n//  Sparkle\n//\n//  Created by Mayur Pawashe on 7/9/16.\n//  Copyright © 2016 Sp"
  },
  {
    "path": "InstallerConnection/SUInstallerConnection.h",
    "chars": 840,
    "preview": "//\n//  SUInstallerConnection.h\n//  InstallerConnection\n//\n//  Created by Mayur Pawashe on 7/9/16.\n//  Copyright © 2016 S"
  },
  {
    "path": "InstallerConnection/SUInstallerConnection.m",
    "chars": 4598,
    "preview": "//\n//  SUInstallerConnection.m\n//  InstallerConnection\n//\n//  Created by Mayur Pawashe on 7/9/16.\n//  Copyright © 2016 S"
  },
  {
    "path": "InstallerConnection/SUInstallerConnectionProtocol.h",
    "chars": 823,
    "preview": "//\n//  SUInstallerConnectionProtocol.h\n//  InstallerConnection\n//\n//  Created by Mayur Pawashe on 7/9/16.\n//  Copyright "
  },
  {
    "path": "InstallerConnection/SUXPCInstallerConnection.h",
    "chars": 608,
    "preview": "//\n//  SUXPCInstallerConnection.h\n//  Sparkle\n//\n//  Created by Mayur Pawashe on 7/10/16.\n//  Copyright © 2016 Sparkle P"
  },
  {
    "path": "InstallerConnection/SUXPCInstallerConnection.m",
    "chars": 2668,
    "preview": "//\n//  SUXPCInstallerConnection.m\n//  Sparkle\n//\n//  Created by Mayur Pawashe on 7/10/16.\n//  Copyright © 2016 Sparkle P"
  },
  {
    "path": "InstallerConnection/main.m",
    "chars": 1798,
    "preview": "//\n//  main.m\n//  InstallerConnection\n//\n//  Created by Mayur Pawashe on 7/9/16.\n//  Copyright © 2016 Sparkle Project. A"
  },
  {
    "path": "InstallerLauncher/Info.plist",
    "chars": 1024,
    "preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/P"
  },
  {
    "path": "InstallerLauncher/SUInstallerLauncher+Private.h",
    "chars": 1239,
    "preview": "//\n//  SUInstallerLauncher+Private.h\n//  SUInstallerLauncher+Private\n//\n//  Created by Mayur Pawashe on 8/21/21.\n//  Cop"
  },
  {
    "path": "InstallerLauncher/SUInstallerLauncher.h",
    "chars": 419,
    "preview": "//\n//  SUInstallerLauncher.h\n//  InstallerLauncher\n//\n//  Created by Mayur Pawashe on 4/1/16.\n//  Copyright © 2016 Spark"
  },
  {
    "path": "InstallerLauncher/SUInstallerLauncher.m",
    "chars": 31941,
    "preview": "//\n//  SUInstallerLauncher.m\n//  InstallerLauncher\n//\n//  Created by Mayur Pawashe on 4/1/16.\n//  Copyright © 2016 Spark"
  },
  {
    "path": "InstallerLauncher/SUInstallerLauncherProtocol.h",
    "chars": 575,
    "preview": "//\n//  SUInstallerLauncherProtocol.h\n//  InstallerLauncher\n//\n//  Created by Mayur Pawashe on 4/1/16.\n//  Copyright © 20"
  },
  {
    "path": "InstallerLauncher/SUInstallerLauncherStatus.h",
    "chars": 400,
    "preview": "//\n//  SUInstallerLauncherStatus.h\n//  Sparkle\n//\n//  Created by Mayur Pawashe on 7/18/16.\n//  Copyright © 2016 Sparkle "
  },
  {
    "path": "InstallerLauncher/main.m",
    "chars": 3077,
    "preview": "//\n//  main.m\n//  InstallerLauncher\n//\n//  Created by Mayur Pawashe on 4/1/16.\n//  Copyright © 2016 Sparkle Project. All"
  },
  {
    "path": "InstallerStatus/Info.plist",
    "chars": 981,
    "preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/P"
  },
  {
    "path": "InstallerStatus/SUInstallerStatus.h",
    "chars": 633,
    "preview": "//\n//  SUInstallerStatus.h\n//  InstallerStatus\n//\n//  Created by Mayur Pawashe on 7/10/16.\n//  Copyright © 2016 Sparkle "
  },
  {
    "path": "InstallerStatus/SUInstallerStatus.m",
    "chars": 3289,
    "preview": "//\n//  SUInstallerStatus.m\n//  InstallerStatus\n//\n//  Created by Mayur Pawashe on 7/10/16.\n//  Copyright © 2016 Sparkle "
  },
  {
    "path": "InstallerStatus/SUInstallerStatusProtocol.h",
    "chars": 1164,
    "preview": "//\n//  SUInstallerStatusProtocol.h\n//  InstallerStatus\n//\n//  Created by Mayur Pawashe on 7/10/16.\n//  Copyright © 2016 "
  },
  {
    "path": "InstallerStatus/SUXPCInstallerStatus.h",
    "chars": 350,
    "preview": "//\n//  SUXPCInstallerStatus.h\n//  Sparkle\n//\n//  Created by Mayur Pawashe on 7/10/16.\n//  Copyright © 2016 Sparkle Proje"
  },
  {
    "path": "InstallerStatus/SUXPCInstallerStatus.m",
    "chars": 2297,
    "preview": "//\n//  SUXPCInstallerStatus.m\n//  Sparkle\n//\n//  Created by Mayur Pawashe on 7/10/16.\n//  Copyright © 2016 Sparkle Proje"
  },
  {
    "path": "InstallerStatus/main.m",
    "chars": 1961,
    "preview": "//\n//  main.m\n//  InstallerStatus\n//\n//  Created by Mayur Pawashe on 7/10/16.\n//  Copyright © 2016 Sparkle Project. All "
  },
  {
    "path": "LICENSE",
    "chars": 6153,
    "preview": "Copyright (c) 2006-2013 Andy Matuschak.\nCopyright (c) 2009-2013 Elgato Systems GmbH.\nCopyright (c) 2011-2014 Kornel Lesi"
  },
  {
    "path": "Makefile",
    "chars": 1268,
    "preview": ".PHONY: all localizable-strings release build test ci\n\nall: build\n\nifndef BUILDDIR\n    BUILDDIR := $(shell mkdir -p \"bui"
  },
  {
    "path": "Package.swift",
    "chars": 857,
    "preview": "// swift-tools-version:5.3\nimport PackageDescription\n\n// Version is technically not required here, SPM doesn't check\nlet"
  },
  {
    "path": "README.markdown",
    "chars": 4787,
    "preview": "# Sparkle 2 [![Build Status](https://github.com/sparkle-project/Sparkle/actions/workflows/ci.yml/badge.svg?branch=2.x)]("
  },
  {
    "path": "Resources/AppIcon.icon/icon.json",
    "chars": 1391,
    "preview": "{\n  \"fill\" : {\n    \"automatic-gradient\" : \"srgb:1.00000,1.00000,1.00000,1.00000\"\n  },\n  \"groups\" : [\n    {\n      \"layers"
  },
  {
    "path": "Resources/Images.xcassets/AppIcon.appiconset/Contents.json",
    "chars": 1300,
    "preview": "{\n  \"images\" : [\n    {\n      \"size\" : \"16x16\",\n      \"idiom\" : \"mac\",\n      \"filename\" : \"icon_16x16.png\",\n      \"scale\""
  },
  {
    "path": "Resources/ReleaseNotesColorStyle.css",
    "chars": 231,
    "preview": "@media (prefers-color-scheme: dark) {\n    html {\n        color-scheme: dark;\n        color: white;\n        background: t"
  },
  {
    "path": "Resources/SampleAppcast.xml",
    "chars": 3156,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<rss version=\"2.0\" xmlns:sparkle=\"http://www.andymatuschak.org/xml-namespaces/spa"
  },
  {
    "path": "Sparkle/AppKitPrevention.h",
    "chars": 520,
    "preview": "//\n//  AppKitPrevention.h\n//  Sparkle\n//\n//  Created by Mayur Pawashe on 1/17/17.\n//  Copyright © 2017 Sparkle Project. "
  },
  {
    "path": "Sparkle/Autoupdate/TerminationListener.h",
    "chars": 607,
    "preview": "//\n//  TerminationListener.h\n//  Sparkle\n//\n//  Created by Mayur Pawashe on 3/7/16.\n//  Copyright © 2016 Sparkle Project"
  },
  {
    "path": "Sparkle/Autoupdate/TerminationListener.m",
    "chars": 4729,
    "preview": "//\n//  TerminationListener.m\n//  Sparkle\n//\n//  Created by Mayur Pawashe on 3/7/16.\n//  Copyright © 2016 Sparkle Project"
  },
  {
    "path": "Sparkle/Base.lproj/Sparkle.strings",
    "chars": 12658,
    "preview": "/* Description text for SUUpdateAlert when the critical update has already been downloaded and ready to install. */\n\"%1$"
  },
  {
    "path": "Sparkle/CheckLocalizations.swift",
    "chars": 4025,
    "preview": "#!/usr/bin/xcrun swift\n\nimport Foundation\n\nfunc die(_ msg: String) {\n    print(\"ERROR: \\(msg)\")\n    exit(1)\n}\n\nextension"
  },
  {
    "path": "Sparkle/InstallerProgress/InstallerProgress-Info.plist",
    "chars": 1955,
    "preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/P"
  },
  {
    "path": "Sparkle/InstallerProgress/InstallerProgressAppController.h",
    "chars": 656,
    "preview": "//\n//  InstallerProgressAppController.h\n//  Sparkle\n//\n//  Created by Mayur Pawashe on 4/10/16.\n//  Copyright © 2016 Spa"
  },
  {
    "path": "Sparkle/InstallerProgress/InstallerProgressAppController.m",
    "chars": 22865,
    "preview": "//\n//  InstallerProgressAppController.m\n//  Sparkle\n//\n//  Created by Mayur Pawashe on 4/10/16.\n//  Copyright © 2016 Spa"
  },
  {
    "path": "Sparkle/InstallerProgress/InstallerProgressDelegate.h",
    "chars": 463,
    "preview": "//\n//  InstallerProgressDelegate.h\n//  Sparkle\n//\n//  Created by Mayur Pawashe on 4/10/16.\n//  Copyright © 2016 Sparkle "
  },
  {
    "path": "Sparkle/InstallerProgress/SPUInstallerAgentProtocol.h",
    "chars": 638,
    "preview": "//\n//  SPUInstallerAgentProtocol.h\n//  Sparkle\n//\n//  Created by Mayur Pawashe on 7/17/16.\n//  Copyright © 2016 Sparkle "
  },
  {
    "path": "Sparkle/InstallerProgress/SUInstallerAgentInitiationProtocol.h",
    "chars": 383,
    "preview": "//\n//  SUInstallerAgentInitiationProtocol.h\n//  Sparkle\n//\n//  Created by Mayur Pawashe on 7/17/16.\n//  Copyright © 2016"
  },
  {
    "path": "Sparkle/InstallerProgress/ShowInstallerProgress.h",
    "chars": 349,
    "preview": "//\n//  ShowInstallerProgress.h\n//  Sparkle\n//\n//  Created by Mayur Pawashe on 4/10/16.\n//  Copyright © 2016 Sparkle Proj"
  },
  {
    "path": "Sparkle/InstallerProgress/ShowInstallerProgress.m",
    "chars": 3854,
    "preview": "//\n//  ShowInstallerProgress.m\n//  Installer Progress\n//\n//  Created by Mayur Pawashe on 4/7/16.\n//  Copyright © 2016 Sp"
  },
  {
    "path": "Sparkle/InstallerProgress/main.m",
    "chars": 1213,
    "preview": "//\n//  main.m\n//  Sparkle\n//\n//  Created by Mayur Pawashe on 4/10/16.\n//  Copyright © 2016 Sparkle Project. All rights r"
  },
  {
    "path": "Sparkle/SPUAppcastItemState.h",
    "chars": 1256,
    "preview": "//\n//  SPUAppcastItemState.h\n//  Sparkle\n//\n//  Created by Mayur Pawashe on 5/31/21.\n//  Copyright © 2021 Sparkle Projec"
  },
  {
    "path": "Sparkle/SPUAppcastItemState.m",
    "chars": 4399,
    "preview": "//\n//  SPUAppcastItemState.m\n//  Sparkle\n//\n//  Created by Mayur Pawashe on 5/31/21.\n//  Copyright © 2021 Sparkle Projec"
  },
  {
    "path": "Sparkle/SPUAppcastItemStateResolver+Private.h",
    "chars": 1157,
    "preview": "//\n//  SPUAppcastItemStateResolver+Private.h\n//  Sparkle\n//\n//  Created by Mayur Pawashe on 6/20/21.\n//  Copyright © 202"
  },
  {
    "path": "Sparkle/SPUAppcastItemStateResolver.h",
    "chars": 1104,
    "preview": "//\n//  SPUAppcastItemStateResolver.h\n//  Sparkle\n//\n//  Created by Mayur Pawashe on 5/31/21.\n//  Copyright © 2021 Sparkl"
  },
  {
    "path": "Sparkle/SPUAppcastItemStateResolver.m",
    "chars": 8178,
    "preview": "//\n//  SPUAppcastItemStateResolver.m\n//  Sparkle\n//\n//  Created by Mayur Pawashe on 5/31/21.\n//  Copyright © 2021 Sparkl"
  },
  {
    "path": "Sparkle/SPUAppcastSigningValidationStatus.h",
    "chars": 1006,
    "preview": "//\n//  SPUAppcastSigningValidationStatus.h\n//  Sparkle\n//\n//  Created on 12/30/25.\n//  Copyright © 2025 Sparkle Project."
  },
  {
    "path": "Sparkle/SPUAutomaticUpdateDriver.h",
    "chars": 642,
    "preview": "//\n//  SPUAutomaticUpdateDriver.h\n//  Sparkle\n//\n//  Created by Mayur Pawashe on 3/18/16.\n//  Copyright © 2016 Sparkle P"
  },
  {
    "path": "Sparkle/SPUAutomaticUpdateDriver.m",
    "chars": 5562,
    "preview": "//\n//  SPUAutomaticUpdateDriver.m\n//  Sparkle\n//\n//  Created by Mayur Pawashe on 3/18/16.\n//  Copyright © 2016 Sparkle P"
  },
  {
    "path": "Sparkle/SPUBasicUpdateDriver.h",
    "chars": 1559,
    "preview": "//\n//  SPUBasicUpdateDriver.h\n//  Sparkle\n//\n//  Created by Mayur Pawashe on 3/18/16.\n//  Copyright © 2016 Sparkle Proje"
  },
  {
    "path": "Sparkle/SPUBasicUpdateDriver.m",
    "chars": 14688,
    "preview": "//\n//  SPUBasicUpdateDriver.m\n//  Sparkle\n//\n//  Created by Mayur Pawashe on 3/18/16.\n//  Copyright © 2016 Sparkle Proje"
  },
  {
    "path": "Sparkle/SPUCoreBasedUpdateDriver.h",
    "chars": 2841,
    "preview": "//\n//  SPUCoreBasedUpdateDriver.h\n//  Sparkle\n//\n//  Created by Mayur Pawashe on 3/18/16.\n//  Copyright © 2016 Sparkle P"
  },
  {
    "path": "Sparkle/SPUCoreBasedUpdateDriver.m",
    "chars": 15128,
    "preview": "//\n//  SPUCoreBasedUpdateDriver.m\n//  Sparkle\n//\n//  Created by Mayur Pawashe on 3/18/16.\n//  Copyright © 2016 Sparkle P"
  },
  {
    "path": "Sparkle/SPUDownloadData.h",
    "chars": 1221,
    "preview": "//\n//  SPUDownloadData.h\n//  Sparkle\n//\n//  Created by Mayur Pawashe on 8/10/16.\n//  Copyright © 2016 Sparkle Project. A"
  },
  {
    "path": "Sparkle/SPUDownloadData.m",
    "chars": 2083,
    "preview": "//\n//  SPUDownloadData.m\n//  Sparkle\n//\n//  Created by Mayur Pawashe on 8/10/16.\n//  Copyright © 2016 Sparkle Project. A"
  },
  {
    "path": "Sparkle/SPUDownloadDataPrivate.h",
    "chars": 451,
    "preview": "//\n//  SPUDownloadDataPrivate.h\n//  SPUDownloadDataPrivate\n//\n//  Created by Mayur Pawashe on 8/13/21.\n//  Copyright © 2"
  },
  {
    "path": "Sparkle/SPUDownloadDriver.h",
    "chars": 1898,
    "preview": "//\n//  SPUDownloadDriver.h\n//  Sparkle\n//\n//  Created by Mayur Pawashe on 3/15/16.\n//  Copyright © 2016 Sparkle Project."
  },
  {
    "path": "Sparkle/SPUDownloadDriver.m",
    "chars": 12587,
    "preview": "//\n//  SPUDownloadDriver.m\n//  Sparkle\n//\n//  Created by Mayur Pawashe on 3/15/16.\n//  Copyright © 2016 Sparkle Project."
  },
  {
    "path": "Sparkle/SPUDownloadedUpdate.h",
    "chars": 694,
    "preview": "//\n//  SPUDownloadedUpdate.h\n//  Sparkle\n//\n//  Created by Mayur Pawashe on 1/8/17.\n//  Copyright © 2017 Sparkle Project"
  },
  {
    "path": "Sparkle/SPUDownloadedUpdate.m",
    "chars": 1074,
    "preview": "//\n//  SPUDownloadedUpdate.m\n//  Sparkle\n//\n//  Created by Mayur Pawashe on 1/8/17.\n//  Copyright © 2017 Sparkle Project"
  },
  {
    "path": "Sparkle/SPUExtractSignedFeed.h",
    "chars": 662,
    "preview": "//\n//  SPUExtractSignedFeed.h\n//  Sparkle\n//\n//  Created on 12/25/25.\n//  Copyright © 2025 Sparkle Project. All rights r"
  },
  {
    "path": "Sparkle/SPUExtractSignedFeed.m",
    "chars": 4151,
    "preview": "//\n//  SPUExtractSignedFeed.m\n//  Sparkle\n//\n//  Created on 12/25/25.\n//  Copyright © 2025 Sparkle Project. All rights r"
  },
  {
    "path": "Sparkle/SPUGentleUserDriverReminders.h",
    "chars": 474,
    "preview": "//\n//  SPUGentleUserDriverReminders.h\n//  Sparkle\n//\n//  Copyright © 2022 Sparkle Project. All rights reserved.\n//\n\n#ifn"
  },
  {
    "path": "Sparkle/SPUInformationalUpdate.h",
    "chars": 494,
    "preview": "//\n//  SPUInformationalUpdate.h\n//  Sparkle\n//\n//  Created by Mayur Pawashe on 1/8/17.\n//  Copyright © 2017 Sparkle Proj"
  },
  {
    "path": "Sparkle/SPUInformationalUpdate.m",
    "chars": 787,
    "preview": "//\n//  SPUInformationalUpdate.m\n//  Sparkle\n//\n//  Created by Mayur Pawashe on 1/8/17.\n//  Copyright © 2017 Sparkle Proj"
  },
  {
    "path": "Sparkle/SPUInstallationType.h",
    "chars": 827,
    "preview": "//\n//  SPUInstallationType.h\n//  Sparkle\n//\n//  Created by Mayur Pawashe on 7/24/16.\n//  Copyright © 2016 Sparkle Projec"
  },
  {
    "path": "Sparkle/SPUInstallerDriver.h",
    "chars": 1745,
    "preview": "//\n//  SPUInstallerDriver.h\n//  Sparkle\n//\n//  Created by Mayur Pawashe on 3/17/16.\n//  Copyright © 2016 Sparkle Project"
  },
  {
    "path": "Sparkle/SPUInstallerDriver.m",
    "chars": 30827,
    "preview": "//\n//  SPUInstallerDriver.m\n//  Sparkle\n//\n//  Created by Mayur Pawashe on 3/17/16.\n//  Copyright © 2016 Sparkle Project"
  }
]

// ... and 377 more files (download for full content)

About this extraction

This page contains the full source code of the sparkle-project/Sparkle GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 577 files (3.1 MB), approximately 840.2k tokens, and a symbol index with 97 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!