From c586788a5bc2908b00d3d601e4a8fc6dae11e0f4 Mon Sep 17 00:00:00 2001 From: Nindi Gill Date: Thu, 8 Dec 2022 20:33:37 +1100 Subject: [PATCH 1/2] Improve cache directory error messaging --- Mist.xcodeproj/project.pbxproj | 4 ++ Mist/Helpers/FileAttributesUpdater.swift | 36 ++++++++++++++++ Mist/Helpers/TaskManager.swift | 19 ++++++++ Mist/Model/DownloadAlertType.swift | 1 + Mist/Model/MistError.swift | 3 ++ Mist/Views/List/ListRow.swift | 55 +++++++++++++++++++++++- MistHelperTool/Info.plist | 2 +- MistHelperTool/main.swift | 21 ++++++++- Shared/HelperToolCommandType.swift | 2 + Shared/ShellExecutor.swift | 2 - 10 files changed, 139 insertions(+), 6 deletions(-) create mode 100644 Mist/Helpers/FileAttributesUpdater.swift diff --git a/Mist.xcodeproj/project.pbxproj b/Mist.xcodeproj/project.pbxproj index 342ee08..98c6920 100644 --- a/Mist.xcodeproj/project.pbxproj +++ b/Mist.xcodeproj/project.pbxproj @@ -94,6 +94,7 @@ 398734D028603D9E00B4C357 /* UInt8+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 398734CF28603D9E00B4C357 /* UInt8+Extension.swift */; }; 398734D228603DE700B4C357 /* Array+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 398734D128603DE700B4C357 /* Array+Extension.swift */; }; 398734D4286046B000B4C357 /* UInt32+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 398734D3286046B000B4C357 /* UInt32+Extension.swift */; }; + 39CA25E32941D8BB0030711E /* FileAttributesUpdater.swift in Sources */ = {isa = PBXBuildFile; fileRef = 39CA25E22941D8BB0030711E /* FileAttributesUpdater.swift */; }; 39CB5E3D293F5C2E00CFDBB8 /* Catalog.swift in Sources */ = {isa = PBXBuildFile; fileRef = 39CB5E3C293F5C2E00CFDBB8 /* Catalog.swift */; }; 39CB5E3F2941486D00CFDBB8 /* CatalogSeedType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 39CB5E3E2941486D00CFDBB8 /* CatalogSeedType.swift */; }; 39CB5E5429418A2900CFDBB8 /* MistTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 39CB5E5329418A2900CFDBB8 /* MistTests.swift */; }; @@ -239,6 +240,7 @@ 398734CF28603D9E00B4C357 /* UInt8+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UInt8+Extension.swift"; sourceTree = ""; }; 398734D128603DE700B4C357 /* Array+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Array+Extension.swift"; sourceTree = ""; }; 398734D3286046B000B4C357 /* UInt32+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UInt32+Extension.swift"; sourceTree = ""; }; + 39CA25E22941D8BB0030711E /* FileAttributesUpdater.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileAttributesUpdater.swift; sourceTree = ""; }; 39CB5E3C293F5C2E00CFDBB8 /* Catalog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Catalog.swift; sourceTree = ""; }; 39CB5E3E2941486D00CFDBB8 /* CatalogSeedType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CatalogSeedType.swift; sourceTree = ""; }; 39CB5E5129418A2900CFDBB8 /* MistTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = MistTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -373,6 +375,7 @@ 39CF56202861C992006FB5D2 /* DiskImageMounter.swift */, 39CF56232861CA85006FB5D2 /* DiskImageUnmounter.swift */, 3935F47D2864813B00760AB0 /* DownloadManager.swift */, + 39CA25E22941D8BB0030711E /* FileAttributesUpdater.swift */, 39CF56382862D75D006FB5D2 /* FileCreator.swift */, 39CF56342862D4BF006FB5D2 /* FileCompressor.swift */, 39CF56162861BE66006FB5D2 /* FileCopier.swift */, @@ -803,6 +806,7 @@ 390451D02856F63700E0B563 /* Installer.swift in Sources */, 3935F47628643AF000760AB0 /* UNNotificationAction+Extension.swift in Sources */, 39252AB3285C5D7700956C74 /* SettingsGeneralUpdatesView.swift in Sources */, + 39CA25E32941D8BB0030711E /* FileAttributesUpdater.swift in Sources */, 3935F4AB286B04BC00760AB0 /* HelperToolInfoPropertyList.swift in Sources */, 393F35BC28641181005B7165 /* RefreshState.swift in Sources */, 390451CA2856F1D300E0B563 /* ScaledImage.swift in Sources */, diff --git a/Mist/Helpers/FileAttributesUpdater.swift b/Mist/Helpers/FileAttributesUpdater.swift new file mode 100644 index 0000000..63bd36f --- /dev/null +++ b/Mist/Helpers/FileAttributesUpdater.swift @@ -0,0 +1,36 @@ +// +// FileAttributesUpdater.swift +// Mist +// +// Created by Nindi Gill on 8/12/2022. +// + +import Foundation +import SecureXPC + +/// Helper struct to update file / directory attributes +struct FileAttributesUpdater { + + /// Update file / directory attributes at the provided URL. + /// + /// - Parameters: + /// - url: The URL of the file / directory to update. + /// - ownerAccountName: The username of the user that will be used to set the file / directory ownership. + /// + /// - Throws: An `Error` if the command failed to execute. + static func update(url: URL, ownerAccountName: String) async throws { + + guard FileManager.default.fileExists(atPath: url.path) else { + return + } + + let arguments: [String] = [url.path, ownerAccountName] + let client: XPCClient = XPCClient.forMachService(named: .helperIdentifier) + let request: HelperToolCommandRequest = HelperToolCommandRequest(type: .fileAttributes, arguments: arguments, environment: [:]) + let response: HelperToolCommandResponse = try await client.sendMessage(request, to: XPCRoute.commandRoute) + + guard response.terminationStatus == 0 else { + throw MistError.invalidTerminationStatus(status: response.terminationStatus, string: response.standardError) + } + } +} diff --git a/Mist/Helpers/TaskManager.swift b/Mist/Helpers/TaskManager.swift index 21027ae..b5305d3 100644 --- a/Mist/Helpers/TaskManager.swift +++ b/Mist/Helpers/TaskManager.swift @@ -6,6 +6,7 @@ // import Foundation +import System // swiftlint:disable file_length // swiftlint:disable:next type_body_length @@ -231,6 +232,24 @@ class TaskManager: ObservableObject { try await DirectoryCreator.create(cacheDirectoryURL, withIntermediateDirectories: true) } ] + } else { + let attributes: [FileAttributeKey: Any] = try FileManager.default.attributesOfItem(atPath: cacheDirectoryURL.path) + + guard let posixPermissions: NSNumber = attributes[.posixPermissions] as? NSNumber, + let ownerAccountName: String = attributes[.ownerAccountName] as? String, + let groupOwnerAccountName: String = attributes[.groupOwnerAccountName] as? String else { + throw MistError.missingFileAttributes + } + + let filePermissions: FilePermissions = FilePermissions(rawValue: CModeT(posixPermissions.int16Value)) + + if filePermissions != [.ownerReadWriteExecute, .groupReadExecute, .otherReadExecute] || ownerAccountName != NSUserName() || groupOwnerAccountName != "staff" { + tasks += [ + MistTask(type: .configure, description: "cache directory") { + try await FileAttributesUpdater.update(url: cacheDirectoryURL, ownerAccountName: ownerAccountName) + } + ] + } } for package in installer.allDownloads { diff --git a/Mist/Model/DownloadAlertType.swift b/Mist/Model/DownloadAlertType.swift index bfa0703..c8b35a1 100644 --- a/Mist/Model/DownloadAlertType.swift +++ b/Mist/Model/DownloadAlertType.swift @@ -10,4 +10,5 @@ import Foundation enum DownloadAlertType: String { case compatibility = "Compatiblity" case helperTool = "Helper Tool" + case cacheDirectory = "Cache Directory" } diff --git a/Mist/Model/MistError.swift b/Mist/Model/MistError.swift index 1f688de..9b9c558 100644 --- a/Mist/Model/MistError.swift +++ b/Mist/Model/MistError.swift @@ -19,6 +19,7 @@ enum MistError: Error, Equatable { case invalidTerminationStatus(status: Int32, string: String?) case invalidURL(_ url: String) case maximumRetriesReached + case missingFileAttributes case outputStreamBufferError case outputStreamWriteError case userCancelled @@ -51,6 +52,8 @@ enum MistError: Error, Equatable { return "Invalid URL: '\(url)'" case .maximumRetriesReached: return "Maximum number of retries reached" + case .missingFileAttributes: + return "Missing file attributes" case .outputStreamBufferError: return "Output Stream Buffer Error" case .outputStreamWriteError: diff --git a/Mist/Views/List/ListRow.swift b/Mist/Views/List/ListRow.swift index 1ee4751..4374d2d 100644 --- a/Mist/Views/List/ListRow.swift +++ b/Mist/Views/List/ListRow.swift @@ -7,6 +7,7 @@ import Blessed import SwiftUI +import System struct ListRow: View { var type: DownloadType @@ -21,6 +22,8 @@ struct ListRow: View { @ObservedObject var taskManager: TaskManager @State private var alertType: DownloadAlertType = .compatibility @State private var showAlert: Bool = false + @AppStorage("cacheDownloads") private var cacheDownloads: Bool = false + @AppStorage("cacheDirectory") private var cacheDirectory: String = .cacheDirectory private let length: CGFloat = 48 private let spacing: CGFloat = 5 private var compatibilityTitle: String { @@ -43,6 +46,12 @@ struct ListRow: View { private var privilegedHelperToolMessage: String { "The Mist Privileged Helper Tool is required to perform Administrator tasks when \(type == .firmware ? "downloading macOS Firmwares" : "creating macOS Installers")." } + private var cacheDirectoryTitle: String { + "Cache directory settings incorrect!" + } + private var cacheDirectoryMessage: String { + "The cache directory has incorrect ownership and/or permissions, which will cause issues caching macOS Installers.\n\nRepair the cache directory ownership and/or permissions and try again." + } var body: some View { HStack { @@ -83,7 +92,14 @@ struct ListRow: View { return Alert( title: Text(privilegedHelperToolTitle), message: Text(privilegedHelperToolMessage), - primaryButton: .default(Text("Install...")) { install() }, + primaryButton: .default(Text("Install...")) { installPrivilegedHelperTool() }, + secondaryButton: .default(Text("Cancel")) + ) + case .cacheDirectory: + return Alert( + title: Text(cacheDirectoryTitle), + message: Text(cacheDirectoryMessage), + primaryButton: .default(Text("Repair...")) { Task { try await repairCacheDirectoryOwnershipAndPermissions() } }, secondaryButton: .default(Text("Cancel")) ) } @@ -103,12 +119,47 @@ struct ListRow: View { return } + if cacheDownloads { + + do { + let attributes: [FileAttributeKey: Any] = try FileManager.default.attributesOfItem(atPath: cacheDirectory) + + guard let posixPermissions: NSNumber = attributes[.posixPermissions] as? NSNumber else { + alertType = .cacheDirectory + showAlert = true + return + } + + let filePermissions: FilePermissions = FilePermissions(rawValue: CModeT(posixPermissions.int16Value)) + + guard filePermissions == [.ownerReadWriteExecute, .groupReadExecute, .otherReadExecute], + let ownerAccountName: String = attributes[.ownerAccountName] as? String, + ownerAccountName == NSUserName(), + let groupOwnerAccountName: String = attributes[.groupOwnerAccountName] as? String, + groupOwnerAccountName == "staff" else { + alertType = .cacheDirectory + showAlert = true + return + } + } catch { + alertType = .cacheDirectory + showAlert = true + return + } + } + showPanel = true } - private func install() { + private func installPrivilegedHelperTool() { try? PrivilegedHelperManager.shared.authorizeAndBless() } + + private func repairCacheDirectoryOwnershipAndPermissions() async throws { + let url: URL = URL(fileURLWithPath: cacheDirectory) + let ownerAccountName: String = NSUserName() + try await FileAttributesUpdater.update(url: url, ownerAccountName: ownerAccountName) + } } struct ListRow_Previews: PreviewProvider { diff --git a/MistHelperTool/Info.plist b/MistHelperTool/Info.plist index 0296bb9..60b50b3 100644 --- a/MistHelperTool/Info.plist +++ b/MistHelperTool/Info.plist @@ -3,7 +3,7 @@ BuildHash - 631381272a839c41efa70afa56e8d6e05a0bdbc167d07ecae792ed338ac0a353 + 977665398f7c4fc4f84fda51c877a98d25ab4a8ed94c7c532dc7070c8e1c845d CFBundleIdentifier com.ninxsoft.mist.helper CFBundleInfoDictionaryVersion diff --git a/MistHelperTool/main.swift b/MistHelperTool/main.swift index fd34725..5edbc7a 100644 --- a/MistHelperTool/main.swift +++ b/MistHelperTool/main.swift @@ -25,7 +25,7 @@ struct HelperToolCommandRunner { case .remove: guard let path: String = request.arguments.first else { - return HelperToolCommandResponse(terminationStatus: 1, standardOutput: nil, standardError: "Invalid URL") + return HelperToolCommandResponse(terminationStatus: 1, standardOutput: nil, standardError: "Invalid URL: \(request.arguments)") } guard FileManager.default.fileExists(atPath: path) else { @@ -38,6 +38,25 @@ struct HelperToolCommandRunner { } catch { return HelperToolCommandResponse(terminationStatus: 1, standardOutput: nil, standardError: error.localizedDescription) } + case .fileAttributes: + + guard let path: String = request.arguments.first, + let ownerAccountName: String = request.arguments.last else { + return HelperToolCommandResponse(terminationStatus: 1, standardOutput: nil, standardError: "Invalid attributes: \(request.arguments)") + } + + let attributes: [FileAttributeKey: Any] = [ + .posixPermissions: 0o755, + .ownerAccountName: ownerAccountName, + .groupOwnerAccountName: "staff" + ] + + do { + try FileManager.default.setAttributes(attributes, ofItemAtPath: path) + return HelperToolCommandResponse(terminationStatus: 0, standardOutput: nil, standardError: nil) + } catch { + return HelperToolCommandResponse(terminationStatus: 1, standardOutput: nil, standardError: error.localizedDescription) + } case .kill: ShellExecutor.shared.terminate() return HelperToolCommandResponse(terminationStatus: 0, standardOutput: nil, standardError: nil) diff --git a/Shared/HelperToolCommandType.swift b/Shared/HelperToolCommandType.swift index 566547c..94875d9 100644 --- a/Shared/HelperToolCommandType.swift +++ b/Shared/HelperToolCommandType.swift @@ -11,6 +11,8 @@ enum HelperToolCommandType: String, Codable { // swiftlint:disable:next redundant_string_enum_value case remove = "remove" // swiftlint:disable:next redundant_string_enum_value + case fileAttributes = "fileAttributes" + // swiftlint:disable:next redundant_string_enum_value case installer = "installer" // swiftlint:disable:next redundant_string_enum_value case createinstallmedia = "createinstallmedia" diff --git a/Shared/ShellExecutor.swift b/Shared/ShellExecutor.swift index 3b17bc1..1b8c0c6 100644 --- a/Shared/ShellExecutor.swift +++ b/Shared/ShellExecutor.swift @@ -59,8 +59,6 @@ class ShellExecutor: NSObject { return (terminationStatus: terminationStatus, standardOutput: standardOutput, standardError: (standardError ?? "").isEmpty ? nil : standardError) } - // swiftlint:enable large_tuple - func terminate() { guard process.isRunning else { From fa22852dbc37920302e231fa99a5c7dbd4dfb819 Mon Sep 17 00:00:00 2001 From: Nindi Gill Date: Thu, 8 Dec 2022 21:15:05 +1100 Subject: [PATCH 2/2] Update example Firmware and Installer structs --- Mist/Model/Firmware.swift | 12 ++-- Mist/Model/Installer.swift | 118 ++++++++++++++++++++++++++++++++++--- 2 files changed, 115 insertions(+), 15 deletions(-) diff --git a/Mist/Model/Firmware.swift b/Mist/Model/Firmware.swift index 54eaa2b..6283a4a 100644 --- a/Mist/Model/Firmware.swift +++ b/Mist/Model/Firmware.swift @@ -22,12 +22,12 @@ struct Firmware: Decodable, Hashable, Identifiable { static var example: Firmware { Firmware( - version: "12.4", - build: "21F79", - shasum: "b5553b62da22e5fdbab2b56b6eb1fb74b58555ac", - size: 13_837_340_777, - url: "https://updates.cdn-apple.com/2022SpringFCS/fullrestores/012-06874/9CECE956-D945-45E2-93E9-4FFDC81BB49A/UniversalMac_12.4_21F79_Restore.ipsw", - date: "2022-05-16T18:23:48Z", + version: "13.0", + build: "22A380", + shasum: "348f49da377d8c394672d1b2800d23452a1d6215", + size: 12_197_669_257, + url: "https://updates.cdn-apple.com/2022FallFCS/fullrestores/012-92188/2C38BCD1-2BFF-4A10-B358-94E8E28BE805/UniversalMac_13.0_22A380_Restore.ipsw", + date: "2022-10-24T17:20:22Z", signed: true, compatible: true ) diff --git a/Mist/Model/Installer.swift b/Mist/Model/Installer.swift index f33f0b6..18abf5f 100644 --- a/Mist/Model/Installer.swift +++ b/Mist/Model/Installer.swift @@ -24,15 +24,115 @@ struct Installer: Decodable, Hashable, Identifiable { static var example: Installer { Installer( - id: "012-06873", - version: "12.4", - build: "21F79", - date: "2022-05-25", - distributionURL: "https://swdist.apple.com/content/downloads/25/34/002-83506-A_0FVTHWXTXJ/9ipp8rhxtcyzjg9pdxekzznprkx48ssbo1/002-83506.English.dist", - distributionSize: 7_242, - packages: [], - boardIDs: [], - deviceIDs: [], + id: "012-92138", + version: "13.0", + build: "22A380", + date: "2022-10-25", + distributionURL: "https://swdist.apple.com/content/downloads/25/16/012-92138-A_KGGGN26YQB/d0kr042ixfvkboeft8qt2i3aclr5bx1e6p/012-92138.English.dist", + distributionSize: 7_467, + packages: [ + Package( + url: "https://swcdn.apple.com/content/downloads/25/16/012-92138-A_KGGGN26YQB/d0kr042ixfvkboeft8qt2i3aclr5bx1e6p/MajorOSInfo.pkg", + size: 1_334_737, + integrityDataURL: "https://swcdn.apple.com/content/downloads/25/16/012-92138-A_KGGGN26YQB/d0kr042ixfvkboeft8qt2i3aclr5bx1e6p/MajorOSInfo.pkg.integrityDataV1", + integrityDataSize: 104 + ), + Package( + url: "https://swcdn.apple.com/content/downloads/25/16/012-92138-A_KGGGN26YQB/d0kr042ixfvkboeft8qt2i3aclr5bx1e6p/InstallAssistant.pkg", + size: 12_151_608_300, + integrityDataURL: "https://swcdn.apple.com/content/downloads/25/16/012-92138-A_KGGGN26YQB/d0kr042ixfvkboeft8qt2i3aclr5bx1e6p/InstallAssistant.pkg.integrityDataV1", + integrityDataSize: 41_792 + ), + Package( + url: "https://swcdn.apple.com/content/downloads/25/16/012-92138-A_KGGGN26YQB/d0kr042ixfvkboeft8qt2i3aclr5bx1e6p/BuildManifest.plist", + size: 3_355_762, + integrityDataURL: "https://swcdn.apple.com/content/downloads/25/16/012-92138-A_KGGGN26YQB/d0kr042ixfvkboeft8qt2i3aclr5bx1e6p/BuildManifest.plist.integrityDataV1", + integrityDataSize: 104 + ), + Package( + url: "https://swcdn.apple.com/content/downloads/25/16/012-92138-A_KGGGN26YQB/d0kr042ixfvkboeft8qt2i3aclr5bx1e6p/InstallInfo.plist", + size: 181, + integrityDataURL: "https://swcdn.apple.com/content/downloads/25/16/012-92138-A_KGGGN26YQB/d0kr042ixfvkboeft8qt2i3aclr5bx1e6p/InstallInfo.plist.integrityDataV1", + integrityDataSize: 104 + ), + Package( + url: "https://swcdn.apple.com/content/downloads/25/16/012-92138-A_KGGGN26YQB/d0kr042ixfvkboeft8qt2i3aclr5bx1e6p/UpdateBrain.zip", + size: 3_450_528, + integrityDataURL: "https://swcdn.apple.com/content/downloads/25/16/012-92138-A_KGGGN26YQB/d0kr042ixfvkboeft8qt2i3aclr5bx1e6p/UpdateBrain.zip.integrityDataV1", + integrityDataSize: 104 + ), + Package( + url: "https://swcdn.apple.com/content/downloads/25/16/012-92138-A_KGGGN26YQB/d0kr042ixfvkboeft8qt2i3aclr5bx1e6p/Info.plist", + size: 5_042, + integrityDataURL: "https://swcdn.apple.com/content/downloads/25/16/012-92138-A_KGGGN26YQB/d0kr042ixfvkboeft8qt2i3aclr5bx1e6p/Info.plist.integrityDataV1", + integrityDataSize: 104 + ) + ], + boardIDs: [ + "Mac-0CFF9C7C2B63DF8D", + "Mac-112818653D3AABFC", + "Mac-1E7E29AD0135F9BC", + "Mac-226CB3C6A851A671", + "Mac-27AD2F918AE68F61", + "Mac-4B682C642B45593E", + "Mac-53FDB3D8DB8CA971", + "Mac-551B86E5744E2388", + "Mac-5F9802EFE386AA28", + "Mac-63001698E7A34814", + "Mac-77F17D7DA9285301", + "Mac-7BA5B2D9E42DDD94", + "Mac-7BA5B2DFE22DDD8C", + "Mac-827FAC58A8FDFA22", + "Mac-827FB448E656EC26", + "Mac-937A206F2EE63C01", + "Mac-A61BADE1FDAD7B05", + "Mac-AA95B1DDAB278B95", + "Mac-AF89B6D9451A490B", + "Mac-B4831CEBD52A0C4C", + "Mac-BE088AF8C5EB4FA2", + "Mac-CAD6701F7CEA0921", + "Mac-CFF7D910A743CAAF", + "Mac-E1008331FDC96864", + "Mac-E7203C0F68AA0004", + "Mac-EE2EBD4B90B839A8" + ], + deviceIDs: [ + "J132AP", + "J137AP", + "J140AAP", + "J140KAP", + "J152FAP", + "J160AP", + "J174AP", + "J185AP", + "J185FAP", + "J213AP", + "J214AP", + "J214KAP", + "J215AP", + "J223AP", + "J230AP", + "J230KAP", + "J274AP", + "J293AP", + "J313AP", + "J314CAP", + "J314SAP", + "J316CAP", + "J316SAP", + "J375CAP", + "J375DAP", + "J413AP", + "J456AP", + "J457AP", + "J493AP", + "J680AP", + "J780AP", + "VMA2MACOSAP", + "VMM-X86_64", + "X589AMLUAP", + "X86LEGACYAP" + ], unsupportedModelIdentifiers: [] ) }