feat: support for macOS xcarchive
This commit is contained in:
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user