From 2f08e1bee01b9782feef648ceb42cf9f2934db51 Mon Sep 17 00:00:00 2001 From: Austin Wang Date: Wed, 18 Mar 2026 01:56:36 -0700 Subject: [PATCH] Revert "fix: repair NIGHTLY Sparkle quarantine metadata (#1703)" (#1725) This reverts commit 629b63dfb865fa53f091842b9e8423da1132db81. --- GhosttyTabs.xcodeproj/project.pbxproj | 10 +- Sources/Update/UpdateDelegate.swift | 21 -- Sources/Update/UpdateDriver.swift | 27 -- Sources/Update/UpdateQuarantineRepair.swift | 292 -------------------- cmuxTests/UpdateQuarantineRepairTests.swift | 173 ------------ 5 files changed, 1 insertion(+), 522 deletions(-) delete mode 100644 Sources/Update/UpdateQuarantineRepair.swift delete mode 100644 cmuxTests/UpdateQuarantineRepairTests.swift diff --git a/GhosttyTabs.xcodeproj/project.pbxproj b/GhosttyTabs.xcodeproj/project.pbxproj index dac5549c..be770cf8 100644 --- a/GhosttyTabs.xcodeproj/project.pbxproj +++ b/GhosttyTabs.xcodeproj/project.pbxproj @@ -99,9 +99,7 @@ F9000000A1B2C3D4E5F60718 /* GhosttyEnsureFocusWindowActivationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F9000001A1B2C3D4E5F60718 /* GhosttyEnsureFocusWindowActivationTests.swift */; }; FA000000A1B2C3D4E5F60718 /* WorkspaceStressProfileTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA000001A1B2C3D4E5F60718 /* WorkspaceStressProfileTests.swift */; }; A5008381 /* BrowserFindJavaScriptTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5008380 /* BrowserFindJavaScriptTests.swift */; }; - A5008383 /* CommandPaletteSearchEngineTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5008382 /* CommandPaletteSearchEngineTests.swift */; }; - AB169902A1B2C3D4E5F60718 /* UpdateQuarantineRepairTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB169903A1B2C3D4E5F60718 /* UpdateQuarantineRepairTests.swift */; }; - AB169900A1B2C3D4E5F60718 /* UpdateQuarantineRepair.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB169901A1B2C3D4E5F60718 /* UpdateQuarantineRepair.swift */; }; + A5008383 /* CommandPaletteSearchEngineTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5008382 /* CommandPaletteSearchEngineTests.swift */; }; DA7A10CA710E000000000003 /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = DA7A10CA710E000000000001 /* Localizable.xcstrings */; }; DA7A10CA710E000000000004 /* InfoPlist.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = DA7A10CA710E000000000002 /* InfoPlist.xcstrings */; }; A5001623 /* cmux.sdef in Resources */ = {isa = PBXBuildFile; fileRef = A5001622 /* cmux.sdef */; }; @@ -267,8 +265,6 @@ FA000001A1B2C3D4E5F60718 /* WorkspaceStressProfileTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WorkspaceStressProfileTests.swift; sourceTree = ""; }; A5008380 /* BrowserFindJavaScriptTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BrowserFindJavaScriptTests.swift; sourceTree = ""; }; A5008382 /* CommandPaletteSearchEngineTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommandPaletteSearchEngineTests.swift; sourceTree = ""; }; - AB169903A1B2C3D4E5F60718 /* UpdateQuarantineRepairTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdateQuarantineRepairTests.swift; sourceTree = ""; }; - AB169901A1B2C3D4E5F60718 /* UpdateQuarantineRepair.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Update/UpdateQuarantineRepair.swift; sourceTree = ""; }; DA7A10CA710E000000000001 /* Localizable.xcstrings */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; path = Localizable.xcstrings; sourceTree = ""; }; DA7A10CA710E000000000002 /* InfoPlist.xcstrings */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; path = InfoPlist.xcstrings; sourceTree = ""; }; A5001622 /* cmux.sdef */ = {isa = PBXFileReference; lastKnownFileType = text.sdef; path = cmux.sdef; sourceTree = ""; }; @@ -444,7 +440,6 @@ A5001221 /* UpdateTestSupport.swift */, A5001224 /* UpdateTestURLProtocol.swift */, A5001223 /* UpdateLogStore.swift */, - AB169901A1B2C3D4E5F60718 /* UpdateQuarantineRepair.swift */, A5001217 /* UpdatePopoverView.swift */, A5001218 /* UpdateTitlebarAccessory.swift */, A5001219 /* WindowToolbarController.swift */, @@ -524,7 +519,6 @@ FA000001A1B2C3D4E5F60718 /* WorkspaceStressProfileTests.swift */, A5008380 /* BrowserFindJavaScriptTests.swift */, A5008382 /* CommandPaletteSearchEngineTests.swift */, - AB169903A1B2C3D4E5F60718 /* UpdateQuarantineRepairTests.swift */, 970226F3C99D0D937CD00539 /* BrowserConfigTests.swift */, 58C7B1B978620BE162CC057E /* BrowserPanelTests.swift */, 02FC74F2C27127CC565B3E8C /* TerminalAndGhosttyTests.swift */, @@ -737,7 +731,6 @@ A500120B /* UpdateTestSupport.swift in Sources */, A500120E /* UpdateTestURLProtocol.swift in Sources */, A500120D /* UpdateLogStore.swift in Sources */, - AB169900A1B2C3D4E5F60718 /* UpdateQuarantineRepair.swift in Sources */, A5001207 /* UpdatePopoverView.swift in Sources */, A5001208 /* UpdateTitlebarAccessory.swift in Sources */, A5001209 /* WindowToolbarController.swift in Sources */, @@ -785,7 +778,6 @@ FA000000A1B2C3D4E5F60718 /* WorkspaceStressProfileTests.swift in Sources */, A5008381 /* BrowserFindJavaScriptTests.swift in Sources */, A5008383 /* CommandPaletteSearchEngineTests.swift in Sources */, - AB169902A1B2C3D4E5F60718 /* UpdateQuarantineRepairTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/Sources/Update/UpdateDelegate.swift b/Sources/Update/UpdateDelegate.swift index 2fb1cd5e..7de114d3 100644 --- a/Sources/Update/UpdateDelegate.swift +++ b/Sources/Update/UpdateDelegate.swift @@ -79,20 +79,6 @@ extension UpdateDriver: SPUUpdaterDelegate { } } - func updater(_ updater: SPUUpdater, willExtractUpdate item: SUAppcastItem) { - prepareQuarantineRepair(for: item.fileURL) - do { - let result = try UpdateQuarantineRepair.repairDownloadedArchiveIfNeeded( - hostName: UpdateQuarantineRepair.sparkleHostName(), - versionString: item.versionString, - dataURL: item.fileURL - ) - logUpdateQuarantineRepair(stage: "download", result: result) - } catch { - UpdateLogStore.shared.append("quarantine repair download failed: \(error.localizedDescription)") - } - } - func updaterDidNotFindUpdate(_ updater: SPUUpdater, error: Error) { viewModel.clearDetectedUpdate() let nsError = error as NSError @@ -125,13 +111,6 @@ extension UpdateDriver: SPUUpdaterDelegate { } } -private func logUpdateQuarantineRepair(stage: String, result: UpdateQuarantineRepairResult) { - let path = result.url?.path ?? "" - let before = result.beforeRawValue ?? "" - let after = result.afterRawValue ?? "" - UpdateLogStore.shared.append("quarantine repair \(stage): \(result.outcome) path=\(path) before=\(before) after=\(after)") -} - private func describeNoUpdateFoundReason(_ reason: SPUNoUpdateFoundReason) -> String { switch reason { case .unknown: diff --git a/Sources/Update/UpdateDriver.swift b/Sources/Update/UpdateDriver.swift index fc81b6ba..289df890 100644 --- a/Sources/Update/UpdateDriver.swift +++ b/Sources/Update/UpdateDriver.swift @@ -9,8 +9,6 @@ class UpdateDriver: NSObject, SPUUserDriver { private var pendingCheckTransition: DispatchWorkItem? private var checkTimeoutWorkItem: DispatchWorkItem? private var lastFeedURLString: String? - private var updateFileURLForQuarantineRepair: URL? - private var finishedExtractedUpdateQuarantineRepair: Bool = false init(viewModel: UpdateViewModel, hostBundle _: Bundle) { self.viewModel = viewModel @@ -120,13 +118,11 @@ class UpdateDriver: NSObject, SPUUserDriver { func showDownloadDidStartExtractingUpdate() { UpdateLogStore.shared.append("show extraction started") setState(.extracting(.init(progress: 0))) - maybeRepairExtractedUpdateQuarantine() } func showExtractionReceivedProgress(_ progress: Double) { UpdateLogStore.shared.append(String(format: "show extraction progress: %.2f", progress)) setState(.extracting(.init(progress: progress))) - maybeRepairExtractedUpdateQuarantine() } func showReady(toInstallAndRelaunch reply: @escaping @Sendable (SPUUserUpdateChoice) -> Void) { @@ -258,11 +254,6 @@ class UpdateDriver: NSObject, SPUUserDriver { UpdateLogStore.shared.append("feed url resolved\(suffix): \(feedURLString)") } - func prepareQuarantineRepair(for updateFileURL: URL?) { - updateFileURLForQuarantineRepair = updateFileURL - finishedExtractedUpdateQuarantineRepair = false - } - func formatErrorForLog(_ error: Error) -> String { let nsError = error as NSError var parts: [String] = ["\(nsError.domain)(\(nsError.code))"] @@ -311,24 +302,6 @@ class UpdateDriver: NSObject, SPUUserDriver { } } - private func maybeRepairExtractedUpdateQuarantine() { - guard !finishedExtractedUpdateQuarantineRepair else { return } - - do { - let result = try UpdateQuarantineRepair.repairExtractedApplicationIfNeeded(dataURL: updateFileURLForQuarantineRepair) - guard result.outcome != .notFound else { return } - - finishedExtractedUpdateQuarantineRepair = true - let path = result.url?.path ?? "" - let before = result.beforeRawValue ?? "" - let after = result.afterRawValue ?? "" - UpdateLogStore.shared.append("quarantine repair extracted-app: \(result.outcome) path=\(path) before=\(before) after=\(after)") - } catch { - finishedExtractedUpdateQuarantineRepair = true - UpdateLogStore.shared.append("quarantine repair extracted-app failed: \(error.localizedDescription)") - } - } - private func runOnMain(_ action: @escaping () -> Void) { if Thread.isMainThread { action() diff --git a/Sources/Update/UpdateQuarantineRepair.swift b/Sources/Update/UpdateQuarantineRepair.swift deleted file mode 100644 index 423c2981..00000000 --- a/Sources/Update/UpdateQuarantineRepair.swift +++ /dev/null @@ -1,292 +0,0 @@ -import CoreServices -import Darwin -import Foundation - -enum UpdateQuarantineRepairOutcome: Equatable { - case skipped - case notFound - case notQuarantined - case alreadyValid - case repaired -} - -struct UpdateQuarantineRepairResult { - let outcome: UpdateQuarantineRepairOutcome - let url: URL? - let beforeRawValue: String? - let afterRawValue: String? -} - -enum UpdateQuarantineRepair { - static let sparkleCacheDirectoryName = "org.sparkle-project.Sparkle" - static let persistentDownloadsDirectoryName = "PersistentDownloads" - static let installationDirectoryName = "Installation" - - private static let quarantineAttributeName = "com.apple.quarantine" - - static func sparkleHostName(for bundle: Bundle = .main, fileManager: FileManager = .default) -> String { - for key in ["SUBundleName", "CFBundleDisplayName", kCFBundleNameKey as String] { - if let value = bundle.object(forInfoDictionaryKey: key) as? String, - !value.isEmpty { - return value - } - } - return (fileManager.displayName(atPath: bundle.bundlePath) as NSString).deletingPathExtension - } - - static func persistentDownloadsRootURL(bundleIdentifier: String, cachesDirectory: URL? = nil) -> URL { - let base = cachesDirectory ?? FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first ?? FileManager.default.temporaryDirectory - return base - .appendingPathComponent(bundleIdentifier, isDirectory: true) - .appendingPathComponent(sparkleCacheDirectoryName, isDirectory: true) - .appendingPathComponent(persistentDownloadsDirectoryName, isDirectory: true) - } - - static func installationRootURL(bundleIdentifier: String, cachesDirectory: URL? = nil) -> URL { - let base = cachesDirectory ?? FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first ?? FileManager.default.temporaryDirectory - return base - .appendingPathComponent(bundleIdentifier, isDirectory: true) - .appendingPathComponent(sparkleCacheDirectoryName, isDirectory: true) - .appendingPathComponent(installationDirectoryName, isDirectory: true) - } - - static func locateDownloadedArchive( - bundleIdentifier: String, - hostName: String, - versionString: String, - cachesDirectory: URL? = nil, - fileManager: FileManager = .default - ) -> URL? { - let rootURL = persistentDownloadsRootURL(bundleIdentifier: bundleIdentifier, cachesDirectory: cachesDirectory) - let expectedDirectoryName = (hostName.isEmpty || versionString.isEmpty) ? nil : "\(hostName) \(versionString)" - - if let exactMatch = newestItem( - in: rootURL, - fileManager: fileManager, - skipPackageDescendants: true, - matching: { url, values, _ in - guard values.isRegularFile == true else { return false } - guard let expectedDirectoryName else { return true } - return url.deletingLastPathComponent().lastPathComponent == expectedDirectoryName - } - ) { - return exactMatch - } - - return newestItem(in: rootURL, fileManager: fileManager, skipPackageDescendants: true) { _, values, _ in - values.isRegularFile == true - } - } - - static func locateExtractedApplication( - bundleIdentifier: String, - bundleName: String, - cachesDirectory: URL? = nil, - fileManager: FileManager = .default - ) -> URL? { - let rootURL = installationRootURL(bundleIdentifier: bundleIdentifier, cachesDirectory: cachesDirectory) - let expectedBundleName = bundleName.isEmpty ? nil : bundleName - - if let exactMatch = newestItem( - in: rootURL, - fileManager: fileManager, - skipPackageDescendants: true, - matching: { url, values, _ in - guard values.isDirectory == true, url.pathExtension == "app" else { return false } - guard let expectedBundleName else { return true } - return url.lastPathComponent == expectedBundleName - } - ) { - return exactMatch - } - - return newestItem(in: rootURL, fileManager: fileManager, skipPackageDescendants: true) { url, values, _ in - values.isDirectory == true && url.pathExtension == "app" - } - } - - static func repairDownloadedArchiveIfNeeded( - hostName: String, - versionString: String, - bundle: Bundle = .main, - fileManager: FileManager = .default, - cachesDirectory: URL? = nil, - dataURL: URL? = nil - ) throws -> UpdateQuarantineRepairResult { - guard let bundleIdentifier = bundle.bundleIdentifier else { - return .init(outcome: .skipped, url: nil, beforeRawValue: nil, afterRawValue: nil) - } - - guard let archiveURL = locateDownloadedArchive( - bundleIdentifier: bundleIdentifier, - hostName: hostName, - versionString: versionString, - cachesDirectory: cachesDirectory, - fileManager: fileManager - ) else { - return .init(outcome: .notFound, url: nil, beforeRawValue: nil, afterRawValue: nil) - } - - return try repairQuarantineIfNeeded( - at: archiveURL, - agentBundleIdentifier: bundleIdentifier, - agentName: sparkleHostName(for: bundle, fileManager: fileManager), - dataURL: dataURL - ) - } - - static func repairExtractedApplicationIfNeeded( - bundle: Bundle = .main, - fileManager: FileManager = .default, - cachesDirectory: URL? = nil, - dataURL: URL? = nil - ) throws -> UpdateQuarantineRepairResult { - guard let bundleIdentifier = bundle.bundleIdentifier else { - return .init(outcome: .skipped, url: nil, beforeRawValue: nil, afterRawValue: nil) - } - - guard let appURL = locateExtractedApplication( - bundleIdentifier: bundleIdentifier, - bundleName: bundle.bundleURL.lastPathComponent, - cachesDirectory: cachesDirectory, - fileManager: fileManager - ) else { - return .init(outcome: .notFound, url: nil, beforeRawValue: nil, afterRawValue: nil) - } - - return try repairQuarantineIfNeeded( - at: appURL, - agentBundleIdentifier: bundleIdentifier, - agentName: sparkleHostName(for: bundle, fileManager: fileManager), - dataURL: dataURL - ) - } - - static func repairQuarantineIfNeeded( - at url: URL, - agentBundleIdentifier: String, - agentName: String, - dataURL: URL? = nil - ) throws -> UpdateQuarantineRepairResult { - let beforeRawValue = rawQuarantineAttribute(at: url) - var resourceValues = try url.resourceValues(forKeys: [.quarantinePropertiesKey]) - var quarantineProperties = resourceValues.quarantineProperties ?? [:] - - let hasQuarantine = beforeRawValue != nil || !quarantineProperties.isEmpty - guard hasQuarantine else { - return .init(outcome: .notQuarantined, url: url, beforeRawValue: beforeRawValue, afterRawValue: beforeRawValue) - } - - var didChange = false - - let existingBundleIdentifier = (quarantineProperties[kLSQuarantineAgentBundleIdentifierKey as String] as? String)? - .trimmingCharacters(in: .whitespacesAndNewlines) - if existingBundleIdentifier != agentBundleIdentifier { - quarantineProperties[kLSQuarantineAgentBundleIdentifierKey as String] = agentBundleIdentifier - didChange = true - } - - let existingAgentName = (quarantineProperties[kLSQuarantineAgentNameKey as String] as? String)? - .trimmingCharacters(in: .whitespacesAndNewlines) - if existingAgentName != agentName { - quarantineProperties[kLSQuarantineAgentNameKey as String] = agentName - didChange = true - } - - if quarantineProperties[kLSQuarantineTypeKey as String] == nil { - quarantineProperties[kLSQuarantineTypeKey as String] = inferredQuarantineType(for: dataURL) - didChange = true - } - - if let dataURL, quarantineProperties[kLSQuarantineDataURLKey as String] == nil { - quarantineProperties[kLSQuarantineDataURLKey as String] = dataURL - didChange = true - } - - if !didChange, let beforeRawValue, rawQuarantineNeedsLaunchServicesRepair(beforeRawValue) { - didChange = true - } - - guard didChange else { - return .init(outcome: .alreadyValid, url: url, beforeRawValue: beforeRawValue, afterRawValue: beforeRawValue) - } - - resourceValues.quarantineProperties = quarantineProperties - var mutableURL = url - try mutableURL.setResourceValues(resourceValues) - - let afterRawValue = rawQuarantineAttribute(at: url) - return .init(outcome: .repaired, url: url, beforeRawValue: beforeRawValue, afterRawValue: afterRawValue) - } - - static func rawQuarantineAttribute(at url: URL) -> String? { - url.path.withCString { pathPointer in - quarantineAttributeName.withCString { attributePointer in - let size = getxattr(pathPointer, attributePointer, nil, 0, 0, XATTR_NOFOLLOW) - guard size >= 0 else { return nil } - - var buffer = [UInt8](repeating: 0, count: Int(size)) - let bytesRead = getxattr(pathPointer, attributePointer, &buffer, buffer.count, 0, XATTR_NOFOLLOW) - guard bytesRead >= 0 else { return nil } - - return String(decoding: buffer.prefix(Int(bytesRead)), as: UTF8.self) - } - } - } - - static func rawQuarantineNeedsLaunchServicesRepair(_ rawValue: String) -> Bool { - let components = rawValue.split(separator: ";", omittingEmptySubsequences: false) - guard components.count >= 4 else { return true } - return components[3].isEmpty - } - - private static func inferredQuarantineType(for dataURL: URL?) -> String { - guard let scheme = dataURL?.scheme?.lowercased() else { - return kLSQuarantineTypeOtherDownload as String - } - switch scheme { - case "http", "https": - return kLSQuarantineTypeWebDownload as String - default: - return kLSQuarantineTypeOtherDownload as String - } - } - - private static func newestItem( - in rootURL: URL, - fileManager: FileManager, - skipPackageDescendants: Bool, - matching predicate: (URL, URLResourceValues, FileManager.DirectoryEnumerator) -> Bool - ) -> URL? { - guard fileManager.fileExists(atPath: rootURL.path) else { return nil } - - let keys: [URLResourceKey] = [.contentModificationDateKey, .isRegularFileKey, .isDirectoryKey] - guard let enumerator = fileManager.enumerator( - at: rootURL, - includingPropertiesForKeys: keys, - options: [.skipsHiddenFiles], - errorHandler: nil - ) else { - return nil - } - - var newestURL: URL? - var newestDate = Date.distantPast - - for case let candidateURL as URL in enumerator { - let resourceValues = (try? candidateURL.resourceValues(forKeys: Set(keys))) ?? URLResourceValues() - if skipPackageDescendants && (candidateURL.pathExtension == "app" || candidateURL.pathExtension == "pkg") { - enumerator.skipDescendants() - } - guard predicate(candidateURL, resourceValues, enumerator) else { continue } - - let contentModificationDate = resourceValues.contentModificationDate ?? Date.distantPast - if newestURL == nil || contentModificationDate > newestDate { - newestURL = candidateURL - newestDate = contentModificationDate - } - } - - return newestURL - } -} diff --git a/cmuxTests/UpdateQuarantineRepairTests.swift b/cmuxTests/UpdateQuarantineRepairTests.swift deleted file mode 100644 index 04c4e97a..00000000 --- a/cmuxTests/UpdateQuarantineRepairTests.swift +++ /dev/null @@ -1,173 +0,0 @@ -import CoreServices -import Darwin -import Foundation -import XCTest - -#if canImport(cmux_DEV) -@testable import cmux_DEV -#elseif canImport(cmux) -@testable import cmux -#endif - -final class UpdateQuarantineRepairTests: XCTestCase { - func testRepairAddsLaunchServicesMetadataForMissingAgentBundleIdentifier() throws { - let fileURL = try makeTemporaryFile(named: "cmux-nightly.dmg") - try writeRawQuarantine("0383;69ba4249;;", to: fileURL) - - let beforeRawValue = try XCTUnwrap(UpdateQuarantineRepair.rawQuarantineAttribute(at: fileURL)) - XCTAssertEqual(beforeRawValue, "0383;69ba4249;;") - - let result = try UpdateQuarantineRepair.repairQuarantineIfNeeded( - at: fileURL, - agentBundleIdentifier: "com.cmuxterm.app.nightly", - agentName: "cmux NIGHTLY", - dataURL: URL(string: "https://example.com/cmux-nightly-macos.dmg") - ) - - XCTAssertEqual(result.outcome, .repaired) - let afterRawValue = try XCTUnwrap(UpdateQuarantineRepair.rawQuarantineAttribute(at: fileURL)) - XCTAssertNotEqual(afterRawValue, beforeRawValue) - XCTAssertFalse(UpdateQuarantineRepair.rawQuarantineNeedsLaunchServicesRepair(afterRawValue)) - - let properties = try fileURL.resourceValues(forKeys: [.quarantinePropertiesKey]).quarantineProperties - XCTAssertEqual(properties?[kLSQuarantineAgentBundleIdentifierKey as String] as? String, "com.cmuxterm.app.nightly") - XCTAssertEqual(properties?[kLSQuarantineAgentNameKey as String] as? String, "cmux NIGHTLY") - XCTAssertEqual(properties?[kLSQuarantineTypeKey as String] as? String, kLSQuarantineTypeWebDownload as String) - } - - func testRepairIsNoOpWhenLaunchServicesQuarantineRecordIsAlreadyValid() throws { - let fileURL = try makeTemporaryFile(named: "cmux-nightly.dmg") - try writeRawQuarantine("0383;69ba4249;;", to: fileURL) - - _ = try UpdateQuarantineRepair.repairQuarantineIfNeeded( - at: fileURL, - agentBundleIdentifier: "com.cmuxterm.app.nightly", - agentName: "cmux NIGHTLY", - dataURL: URL(string: "https://example.com/cmux-nightly-macos.dmg") - ) - - let repairedRawValue = try XCTUnwrap(UpdateQuarantineRepair.rawQuarantineAttribute(at: fileURL)) - let secondResult = try UpdateQuarantineRepair.repairQuarantineIfNeeded( - at: fileURL, - agentBundleIdentifier: "com.cmuxterm.app.nightly", - agentName: "cmux NIGHTLY", - dataURL: URL(string: "https://example.com/cmux-nightly-macos.dmg") - ) - - XCTAssertEqual(secondResult.outcome, .alreadyValid) - XCTAssertEqual(secondResult.beforeRawValue, repairedRawValue) - XCTAssertEqual(secondResult.afterRawValue, repairedRawValue) - } - - func testLocateDownloadedArchivePrefersNewestMatchingVersionDirectory() throws { - let cachesDirectory = try makeTemporaryDirectory(named: "SparkleCaches") - let rootURL = UpdateQuarantineRepair.persistentDownloadsRootURL( - bundleIdentifier: "com.cmuxterm.app.nightly", - cachesDirectory: cachesDirectory - ) - - let oldArchiveURL = rootURL - .appendingPathComponent("token-old", isDirectory: true) - .appendingPathComponent("cmux NIGHTLY 1234", isDirectory: true) - .appendingPathComponent("old.dmg") - let newArchiveURL = rootURL - .appendingPathComponent("token-new", isDirectory: true) - .appendingPathComponent("cmux NIGHTLY 1234", isDirectory: true) - .appendingPathComponent("new.dmg") - let otherArchiveURL = rootURL - .appendingPathComponent("token-other", isDirectory: true) - .appendingPathComponent("cmux NIGHTLY 9999", isDirectory: true) - .appendingPathComponent("other.dmg") - - try createFile(at: oldArchiveURL) - try createFile(at: newArchiveURL) - try createFile(at: otherArchiveURL) - - try setModificationDate(Date(timeIntervalSince1970: 100), for: oldArchiveURL) - try setModificationDate(Date(timeIntervalSince1970: 200), for: newArchiveURL) - try setModificationDate(Date(timeIntervalSince1970: 300), for: otherArchiveURL) - - let locatedArchiveURL = UpdateQuarantineRepair.locateDownloadedArchive( - bundleIdentifier: "com.cmuxterm.app.nightly", - hostName: "cmux NIGHTLY", - versionString: "1234", - cachesDirectory: cachesDirectory - ) - - XCTAssertEqual(locatedArchiveURL, newArchiveURL) - } - - func testLocateExtractedApplicationUsesNewestMatchingBundleName() throws { - let cachesDirectory = try makeTemporaryDirectory(named: "SparkleInstallation") - let rootURL = UpdateQuarantineRepair.installationRootURL( - bundleIdentifier: "com.cmuxterm.app.nightly", - cachesDirectory: cachesDirectory - ) - - let oldAppURL = rootURL - .appendingPathComponent("install-old", isDirectory: true) - .appendingPathComponent("extract-old", isDirectory: true) - .appendingPathComponent("cmux NIGHTLY.app", isDirectory: true) - let newAppURL = rootURL - .appendingPathComponent("install-new", isDirectory: true) - .appendingPathComponent("extract-new", isDirectory: true) - .appendingPathComponent("cmux NIGHTLY.app", isDirectory: true) - let otherAppURL = rootURL - .appendingPathComponent("install-other", isDirectory: true) - .appendingPathComponent("extract-other", isDirectory: true) - .appendingPathComponent("Different.app", isDirectory: true) - - try FileManager.default.createDirectory(at: oldAppURL, withIntermediateDirectories: true) - try FileManager.default.createDirectory(at: newAppURL, withIntermediateDirectories: true) - try FileManager.default.createDirectory(at: otherAppURL, withIntermediateDirectories: true) - - try setModificationDate(Date(timeIntervalSince1970: 100), for: oldAppURL) - try setModificationDate(Date(timeIntervalSince1970: 200), for: newAppURL) - try setModificationDate(Date(timeIntervalSince1970: 300), for: otherAppURL) - - let locatedAppURL = UpdateQuarantineRepair.locateExtractedApplication( - bundleIdentifier: "com.cmuxterm.app.nightly", - bundleName: "cmux NIGHTLY.app", - cachesDirectory: cachesDirectory - ) - - XCTAssertEqual(locatedAppURL, newAppURL) - } - - private func makeTemporaryDirectory(named name: String) throws -> URL { - let directoryURL = FileManager.default.temporaryDirectory - .appendingPathComponent("UpdateQuarantineRepairTests", isDirectory: true) - .appendingPathComponent(UUID().uuidString, isDirectory: true) - .appendingPathComponent(name, isDirectory: true) - try FileManager.default.createDirectory(at: directoryURL, withIntermediateDirectories: true) - return directoryURL - } - - private func makeTemporaryFile(named name: String) throws -> URL { - let directoryURL = try makeTemporaryDirectory(named: "Files") - let fileURL = directoryURL.appendingPathComponent(name) - try createFile(at: fileURL) - return fileURL - } - - private func createFile(at url: URL) throws { - try FileManager.default.createDirectory(at: url.deletingLastPathComponent(), withIntermediateDirectories: true) - XCTAssertTrue(FileManager.default.createFile(atPath: url.path, contents: Data())) - } - - private func setModificationDate(_ modificationDate: Date, for url: URL) throws { - try FileManager.default.setAttributes([.modificationDate: modificationDate], ofItemAtPath: url.path) - } - - private func writeRawQuarantine(_ value: String, to url: URL) throws { - let bytes = Array(value.utf8) - let status = url.path.withCString { pathPointer in - "com.apple.quarantine".withCString { attributePointer in - bytes.withUnsafeBytes { bufferPointer in - setxattr(pathPointer, attributePointer, bufferPointer.baseAddress, bytes.count, 0, 0) - } - } - } - XCTAssertEqual(status, 0) - } -}