feat: support for macOS xcarchive
This commit is contained in:
@@ -54,7 +54,7 @@ struct AppIcon {
|
|||||||
|
|
||||||
/// Extract an image from `Assets.car`
|
/// Extract an image from `Assets.car`
|
||||||
func imageFromAssetsCar(_ imageName: String) -> NSImage? {
|
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 nil
|
||||||
}
|
}
|
||||||
return CarReader(data)?.imageFromAssetsCar(imageName)
|
return CarReader(data)?.imageFromAssetsCar(imageName)
|
||||||
@@ -114,10 +114,9 @@ extension AppIcon {
|
|||||||
}
|
}
|
||||||
|
|
||||||
case .Archive, .Extension:
|
case .Archive, .Extension:
|
||||||
let basePath = meta.effectiveUrl ?? meta.url
|
|
||||||
for iconPath in iconList {
|
for iconPath in iconList {
|
||||||
let fileName = iconPath.components(separatedBy: "/").last!
|
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 {
|
guard let files = try? FileManager.default.contentsOfDirectory(atPath: parentDir) else {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,17 +16,18 @@ enum FileType {
|
|||||||
struct MetaInfo {
|
struct MetaInfo {
|
||||||
let UTI: String
|
let UTI: String
|
||||||
let url: URL
|
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 type: FileType
|
||||||
let zipFile: ZipFile? // only set for zipped file types
|
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.
|
/// Use file url and UTI type to generate an info object to pass around.
|
||||||
init(_ url: URL) {
|
init(_ url: URL) {
|
||||||
self.url = url
|
self.url = url
|
||||||
self.UTI = try! url.resourceValues(forKeys: [.typeIdentifierKey]).typeIdentifier ?? "Unknown"
|
self.UTI = try! url.resourceValues(forKeys: [.typeIdentifierKey]).typeIdentifier ?? "Unknown"
|
||||||
|
|
||||||
|
var isOSX = false
|
||||||
var effective: URL? = nil
|
var effective: URL? = nil
|
||||||
var zipFile: ZipFile? = nil
|
var zipFile: ZipFile? = nil
|
||||||
|
|
||||||
@@ -36,26 +37,46 @@ struct MetaInfo {
|
|||||||
zipFile = ZipFile(self.url.path)
|
zipFile = ZipFile(self.url.path)
|
||||||
case "com.apple.xcode.archive":
|
case "com.apple.xcode.archive":
|
||||||
self.type = FileType.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":
|
case "com.apple.application-and-system-extension":
|
||||||
self.type = FileType.Extension
|
self.type = FileType.Extension
|
||||||
default:
|
default:
|
||||||
os_log(.error, log: log, "Unsupported file type: %{public}@", self.UTI)
|
os_log(.error, log: log, "Unsupported file type: %{public}@", self.UTI)
|
||||||
fatalError()
|
fatalError()
|
||||||
}
|
}
|
||||||
|
self.isOSX = isOSX
|
||||||
self.zipFile = zipFile
|
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.
|
/// Load a file from bundle into memory. Either by file path or via unzip.
|
||||||
func readPayloadFile(_ filename: String) -> Data? {
|
func readPayloadFile(_ filename: String, osxSubdir: String?) -> Data? {
|
||||||
switch (self.type) {
|
switch self.type {
|
||||||
case .IPA:
|
case .IPA:
|
||||||
return zipFile!.unzipFile("Payload/*.app/".appending(filename))
|
return zipFile!.unzipFile("Payload/*.app/".appending(filename))
|
||||||
case .Archive:
|
case .Archive, .Extension:
|
||||||
return try? Data(contentsOf: effectiveUrl!.appendingPathComponent(filename))
|
return try? Data(contentsOf: self.effectiveUrl(osxSubdir, filename))
|
||||||
case .Extension:
|
|
||||||
return try? Data(contentsOf: url.appendingPathComponent(filename))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -63,7 +84,7 @@ struct MetaInfo {
|
|||||||
func readPlistApp() -> PlistDict? {
|
func readPlistApp() -> PlistDict? {
|
||||||
switch self.type {
|
switch self.type {
|
||||||
case .IPA, .Archive, .Extension:
|
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.
|
/// breadth-first search for `Info.plist`
|
||||||
private func appPathForArchive(_ url: URL) -> URL? {
|
private func recursiveSearchInfoPlist(_ url: URL) -> URL? {
|
||||||
let appsDir = url.appendingPathComponent("Products/Applications/")
|
var queue: [URL] = [url]
|
||||||
if FileManager.default.fileExists(atPath: appsDir.path) {
|
while !queue.isEmpty {
|
||||||
if let x = try? FileManager.default.contentsOfDirectory(at: appsDir, includingPropertiesForKeys: nil), !x.isEmpty {
|
let current = queue.removeLast()
|
||||||
return x.first
|
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
|
return nil
|
||||||
|
|||||||
@@ -56,9 +56,11 @@ extension PreviewGenerator {
|
|||||||
return "No exceptions"
|
return "No exceptions"
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Process info stored in `Info.plist`
|
private func deviceFamilyList(_ appPlist: PlistDict, isOSX: Bool) -> String {
|
||||||
mutating func procAppInfo(_ appPlist: PlistDict) {
|
if isOSX {
|
||||||
var platforms = (appPlist["UIDeviceFamily"] as? [Int])?.compactMap({
|
return (appPlist["CFBundleSupportedPlatforms"] as? [String])?.joined(separator: ", ") ?? "macOS"
|
||||||
|
}
|
||||||
|
let platforms = (appPlist["UIDeviceFamily"] as? [Int])?.compactMap({
|
||||||
switch $0 {
|
switch $0 {
|
||||||
case 1: return "iPhone"
|
case 1: return "iPhone"
|
||||||
case 2: return "iPad"
|
case 2: return "iPad"
|
||||||
@@ -70,8 +72,14 @@ extension PreviewGenerator {
|
|||||||
|
|
||||||
let minVersion = appPlist["MinimumOSVersion"] as? String ?? ""
|
let minVersion = appPlist["MinimumOSVersion"] as? String ?? ""
|
||||||
if platforms?.isEmpty ?? true, minVersion.hasPrefix("1.") || minVersion.hasPrefix("2.") || minVersion.hasPrefix("3.") {
|
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
|
let extensionType = (appPlist["NSExtension"] as? PlistDict)?["NSExtensionPointIdentifier"] as? String
|
||||||
self.apply([
|
self.apply([
|
||||||
@@ -83,7 +91,7 @@ extension PreviewGenerator {
|
|||||||
"AppExtensionTypeHidden": extensionType != nil ? "" : CLASS_HIDDEN,
|
"AppExtensionTypeHidden": extensionType != nil ? "" : CLASS_HIDDEN,
|
||||||
"AppExtensionType": extensionType ?? "",
|
"AppExtensionType": extensionType ?? "",
|
||||||
|
|
||||||
"AppDeviceFamily": platforms ?? "",
|
"AppDeviceFamily": deviceFamilyList(appPlist, isOSX: isOSX),
|
||||||
"AppSDK": appPlist["DTSDKName"] as? String ?? "",
|
"AppSDK": appPlist["DTSDKName"] as? String ?? "",
|
||||||
"AppMinOS": minVersion,
|
"AppMinOS": minVersion,
|
||||||
"AppTransportSecurity": formattedAppTransportSecurity(appPlist),
|
"AppTransportSecurity": formattedAppTransportSecurity(appPlist),
|
||||||
|
|||||||
@@ -16,10 +16,8 @@ extension PreviewGenerator {
|
|||||||
}
|
}
|
||||||
try! meta.zipFile!.unzipFile("Payload/*.app/\(bundleExecutable)", toDir: tmpPath)
|
try! meta.zipFile!.unzipFile("Payload/*.app/\(bundleExecutable)", toDir: tmpPath)
|
||||||
return Entitlements(forBinary: tmpPath + "/" + bundleExecutable)
|
return Entitlements(forBinary: tmpPath + "/" + bundleExecutable)
|
||||||
case .Archive:
|
case .Archive, .Extension:
|
||||||
return Entitlements(forBinary: meta.effectiveUrl!.path + "/" + bundleExecutable)
|
return Entitlements(forBinary: meta.effectiveUrl("MacOS", bundleExecutable).path)
|
||||||
case .Extension:
|
|
||||||
return Entitlements(forBinary: meta.url.path + "/" + bundleExecutable)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ private let log = OSLog(subsystem: Bundle.main.bundleIdentifier!, category: "Htm
|
|||||||
extension MetaInfo {
|
extension MetaInfo {
|
||||||
/// Read `embedded.mobileprovision` file and decode with CMS decoder.
|
/// Read `embedded.mobileprovision` file and decode with CMS decoder.
|
||||||
func readPlistProvision() -> PlistDict? {
|
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)
|
os_log(.info, log: log, "No embedded.mobileprovision file for %{public}@", self.url.path)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ struct PreviewGenerator {
|
|||||||
|
|
||||||
data["QuickLookTitle"] = stringForFileType(meta)
|
data["QuickLookTitle"] = stringForFileType(meta)
|
||||||
|
|
||||||
procAppInfo(plistApp)
|
procAppInfo(plistApp, isOSX: meta.isOSX)
|
||||||
procItunesMeta(plistItunes)
|
procItunesMeta(plistItunes)
|
||||||
procProvision(plistProvision, isOSX: meta.isOSX)
|
procProvision(plistProvision, isOSX: meta.isOSX)
|
||||||
procEntitlements(meta, plistApp, plistProvision)
|
procEntitlements(meta, plistApp, plistProvision)
|
||||||
|
|||||||
Reference in New Issue
Block a user