feat: support for macOS xcarchive

This commit is contained in:
relikd
2025-11-05 18:18:08 +01:00
parent 36e30a1fdf
commit af9c398571
6 changed files with 64 additions and 32 deletions

View File

@@ -54,7 +54,7 @@ struct AppIcon {
/// Extract an image from `Assets.car`
func imageFromAssetsCar(_ imageName: String) -> NSImage? {
guard let data = meta.readPayloadFile("Assets.car") else {
guard let data = meta.readPayloadFile("Assets.car", osxSubdir: "Resources") else {
return nil
}
return CarReader(data)?.imageFromAssetsCar(imageName)
@@ -114,10 +114,9 @@ extension AppIcon {
}
case .Archive, .Extension:
let basePath = meta.effectiveUrl ?? meta.url
for iconPath in iconList {
let fileName = iconPath.components(separatedBy: "/").last!
let parentDir = basePath.appendingPathComponent(iconPath, isDirectory: false).deletingLastPathComponent().path
let parentDir = meta.effectiveUrl("Resources", iconPath).deletingLastPathComponent().path
guard let files = try? FileManager.default.contentsOfDirectory(atPath: parentDir) else {
continue
}

View File

@@ -16,17 +16,18 @@ enum FileType {
struct MetaInfo {
let UTI: String
let url: URL
let effectiveUrl: URL? // if set, will point to the app inside of an archive
private let effectiveUrl: URL // if set, will point to the app inside of an archive
let type: FileType
let zipFile: ZipFile? // only set for zipped file types
let isOSX = false // relict of the past when ProvisionQL also processed provision profiles
let isOSX: Bool
/// Use file url and UTI type to generate an info object to pass around.
init(_ url: URL) {
self.url = url
self.UTI = try! url.resourceValues(forKeys: [.typeIdentifierKey]).typeIdentifier ?? "Unknown"
var isOSX = false
var effective: URL? = nil
var zipFile: ZipFile? = nil
@@ -36,26 +37,46 @@ struct MetaInfo {
zipFile = ZipFile(self.url.path)
case "com.apple.xcode.archive":
self.type = FileType.Archive
effective = appPathForArchive(self.url)
let productsDir = url.appendingPathComponent("Products", isDirectory: true)
if productsDir.exists() {
if let bundleDir = recursiveSearchInfoPlist(productsDir) {
isOSX = bundleDir.appendingPathComponent("MacOS").exists() && bundleDir.lastPathComponent == "Contents"
effective = bundleDir
}
}
case "com.apple.application-and-system-extension":
self.type = FileType.Extension
default:
os_log(.error, log: log, "Unsupported file type: %{public}@", self.UTI)
fatalError()
}
self.isOSX = isOSX
self.zipFile = zipFile
self.effectiveUrl = effective
self.effectiveUrl = effective ?? url
}
/// Evaluate path with `osxSubdir` and `filename`
func effectiveUrl(_ osxSubdir: String?, _ filename: String) -> URL {
switch self.type {
case .IPA:
return effectiveUrl
case .Archive, .Extension:
if isOSX, let osxSubdir {
return effectiveUrl
.appendingPathComponent(osxSubdir, isDirectory: true)
.appendingPathComponent(filename, isDirectory: false)
}
return effectiveUrl.appendingPathComponent(filename, isDirectory: false)
}
}
/// Load a file from bundle into memory. Either by file path or via unzip.
func readPayloadFile(_ filename: String) -> Data? {
switch (self.type) {
func readPayloadFile(_ filename: String, osxSubdir: String?) -> Data? {
switch self.type {
case .IPA:
return zipFile!.unzipFile("Payload/*.app/".appending(filename))
case .Archive:
return try? Data(contentsOf: effectiveUrl!.appendingPathComponent(filename))
case .Extension:
return try? Data(contentsOf: url.appendingPathComponent(filename))
case .Archive, .Extension:
return try? Data(contentsOf: self.effectiveUrl(osxSubdir, filename))
}
}
@@ -63,7 +84,7 @@ struct MetaInfo {
func readPlistApp() -> PlistDict? {
switch self.type {
case .IPA, .Archive, .Extension:
return self.readPayloadFile("Info.plist")?.asPlistOrNil()
return self.readPayloadFile("Info.plist", osxSubdir: nil)?.asPlistOrNil()
}
}
}
@@ -88,14 +109,20 @@ extension Data {
}
// MARK: - Meta data for QuickLook
// MARK: - helper methods
/// Search an archive for the .app or .ipa bundle.
private func appPathForArchive(_ url: URL) -> URL? {
let appsDir = url.appendingPathComponent("Products/Applications/")
if FileManager.default.fileExists(atPath: appsDir.path) {
if let x = try? FileManager.default.contentsOfDirectory(at: appsDir, includingPropertiesForKeys: nil), !x.isEmpty {
return x.first
/// breadth-first search for `Info.plist`
private func recursiveSearchInfoPlist(_ url: URL) -> URL? {
var queue: [URL] = [url]
while !queue.isEmpty {
let current = queue.removeLast()
if let subfiles = try? FileManager.default.contentsOfDirectory(at: current, includingPropertiesForKeys: []) {
for fname in subfiles {
if fname.lastPathComponent == "Info.plist" {
return fname.deletingLastPathComponent()
}
}
queue.append(contentsOf: subfiles)
}
}
return nil

View File

@@ -56,9 +56,11 @@ extension PreviewGenerator {
return "No exceptions"
}
/// Process info stored in `Info.plist`
mutating func procAppInfo(_ appPlist: PlistDict) {
var platforms = (appPlist["UIDeviceFamily"] as? [Int])?.compactMap({
private func deviceFamilyList(_ appPlist: PlistDict, isOSX: Bool) -> String {
if isOSX {
return (appPlist["CFBundleSupportedPlatforms"] as? [String])?.joined(separator: ", ") ?? "macOS"
}
let platforms = (appPlist["UIDeviceFamily"] as? [Int])?.compactMap({
switch $0 {
case 1: return "iPhone"
case 2: return "iPad"
@@ -70,8 +72,14 @@ extension PreviewGenerator {
let minVersion = appPlist["MinimumOSVersion"] as? String ?? ""
if platforms?.isEmpty ?? true, minVersion.hasPrefix("1.") || minVersion.hasPrefix("2.") || minVersion.hasPrefix("3.") {
platforms = "iPhone"
return "iPhone"
}
return platforms ?? ""
}
/// Process info stored in `Info.plist`
mutating func procAppInfo(_ appPlist: PlistDict, isOSX: Bool) {
let minVersion = appPlist[isOSX ? "LSMinimumSystemVersion" : "MinimumOSVersion"] as? String ?? ""
let extensionType = (appPlist["NSExtension"] as? PlistDict)?["NSExtensionPointIdentifier"] as? String
self.apply([
@@ -83,7 +91,7 @@ extension PreviewGenerator {
"AppExtensionTypeHidden": extensionType != nil ? "" : CLASS_HIDDEN,
"AppExtensionType": extensionType ?? "",
"AppDeviceFamily": platforms ?? "",
"AppDeviceFamily": deviceFamilyList(appPlist, isOSX: isOSX),
"AppSDK": appPlist["DTSDKName"] as? String ?? "",
"AppMinOS": minVersion,
"AppTransportSecurity": formattedAppTransportSecurity(appPlist),

View File

@@ -16,10 +16,8 @@ extension PreviewGenerator {
}
try! meta.zipFile!.unzipFile("Payload/*.app/\(bundleExecutable)", toDir: tmpPath)
return Entitlements(forBinary: tmpPath + "/" + bundleExecutable)
case .Archive:
return Entitlements(forBinary: meta.effectiveUrl!.path + "/" + bundleExecutable)
case .Extension:
return Entitlements(forBinary: meta.url.path + "/" + bundleExecutable)
case .Archive, .Extension:
return Entitlements(forBinary: meta.effectiveUrl("MacOS", bundleExecutable).path)
}
}

View File

@@ -7,7 +7,7 @@ private let log = OSLog(subsystem: Bundle.main.bundleIdentifier!, category: "Htm
extension MetaInfo {
/// Read `embedded.mobileprovision` file and decode with CMS decoder.
func readPlistProvision() -> PlistDict? {
guard let provisionData = self.readPayloadFile("embedded.mobileprovision") else {
guard let provisionData = self.readPayloadFile("embedded.mobileprovision", osxSubdir: nil) else {
os_log(.info, log: log, "No embedded.mobileprovision file for %{public}@", self.url.path)
return nil
}

View File

@@ -16,7 +16,7 @@ struct PreviewGenerator {
data["QuickLookTitle"] = stringForFileType(meta)
procAppInfo(plistApp)
procAppInfo(plistApp, isOSX: meta.isOSX)
procItunesMeta(plistItunes)
procProvision(plistProvision, isOSX: meta.isOSX)
procEntitlements(meta, plistApp, plistProvision)