ref: data structures for plist files
This commit is contained in:
@@ -17,7 +17,7 @@ struct AppIcon {
|
||||
func extractImageForThumbnail() -> NSImage {
|
||||
switch meta.type {
|
||||
case .IPA, .Archive, .Extension:
|
||||
extractImage(from: meta.readPlistApp())
|
||||
extractImage(from: meta.readPlist_Icon()?.filenames)
|
||||
case .APK:
|
||||
extractImage(from: meta.readApkIconOnly())
|
||||
}
|
||||
@@ -33,7 +33,7 @@ struct AppIcon {
|
||||
|
||||
/// Try multiple methods to extract image.
|
||||
/// This method will always return an image even if none is found, in which case it returns the default image.
|
||||
func extractImage(from appPlist: PlistDict?) -> NSImage {
|
||||
func extractImage(from plistIcons: [String]?) -> NSImage {
|
||||
// no need to unwrap the plist, and most .ipa should include the Artwork anyway
|
||||
if meta.type == .IPA {
|
||||
if let data = meta.zipFile!.unzipFile("iTunesArtwork") {
|
||||
@@ -44,7 +44,7 @@ struct AppIcon {
|
||||
}
|
||||
|
||||
// Extract image name from app plist
|
||||
var plistImgNames = (appPlist == nil) ? [] : iconNamesFromPlist(appPlist!)
|
||||
var plistImgNames = plistIcons ?? []
|
||||
os_log(.debug, log: log, "[icon] icon names in plist: %{public}@", plistImgNames)
|
||||
|
||||
// If no previous filename works (or empty), try default icon names
|
||||
@@ -90,33 +90,6 @@ struct AppIcon {
|
||||
// MARK: - Plist
|
||||
|
||||
extension AppIcon {
|
||||
/// Parse app plist to find the bundle icon filename.
|
||||
/// @param appPlist If `nil`, will load plist on the fly (used for thumbnail)
|
||||
/// @return Filenames which do not necessarily exist on filesystem. This may include `@2x` and/or no file extension.
|
||||
private func iconNamesFromPlist(_ appPlist: PlistDict) -> [String] {
|
||||
// Check for CFBundleIcons (since 5.0)
|
||||
if let icons = unpackNameListFromPlistDict(appPlist["CFBundleIcons"]), !icons.isEmpty {
|
||||
return icons
|
||||
}
|
||||
// iPad-only apps
|
||||
if let icons = unpackNameListFromPlistDict(appPlist["CFBundleIcons~ipad"]), !icons.isEmpty {
|
||||
return icons
|
||||
}
|
||||
// Check for CFBundleIconFiles (since 3.2)
|
||||
if let icons = appPlist["CFBundleIconFiles"] as? [String], !icons.isEmpty {
|
||||
return icons
|
||||
}
|
||||
// key found on iTunesU app
|
||||
if let icons = appPlist["Icon files"] as? [String], !icons.isEmpty {
|
||||
return icons
|
||||
}
|
||||
// Check for CFBundleIconFile (legacy, before 3.2)
|
||||
if let icon = appPlist["CFBundleIconFile"] as? String { // may be nil
|
||||
return [icon]
|
||||
}
|
||||
return []
|
||||
}
|
||||
|
||||
/// Given a filename, search Bundle or Filesystem for files that match. Select the filename with the highest resolution.
|
||||
private func expandImageName(_ iconList: [String]) -> String? {
|
||||
var matches: [String] = []
|
||||
@@ -161,21 +134,6 @@ extension AppIcon {
|
||||
}
|
||||
return matches.isEmpty ? nil : sortedByResolution(matches).first
|
||||
}
|
||||
|
||||
/// Deep select icons from plist key `CFBundleIcons` and `CFBundleIcons~ipad`
|
||||
private func unpackNameListFromPlistDict(_ bundleDict: Any?) -> [String]? {
|
||||
if let bundleDict = bundleDict as? PlistDict {
|
||||
if let primaryDict = bundleDict["CFBundlePrimaryIcon"] as? PlistDict {
|
||||
if let icons = primaryDict["CFBundleIconFiles"] as? [String] {
|
||||
return icons
|
||||
}
|
||||
if let name = primaryDict["CFBundleIconName"] as? String { // key found on a .tipa file
|
||||
return [name]
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
/// @return lower index means higher resolution.
|
||||
private func resolutionIndex(_ iconName: String) -> Int {
|
||||
|
||||
@@ -85,16 +85,6 @@ struct MetaInfo {
|
||||
return try? Data(contentsOf: self.effectiveUrl(osxSubdir, filename))
|
||||
}
|
||||
}
|
||||
|
||||
/// Read app default `Info.plist`. (used for both, Preview and Thumbnail)
|
||||
func readPlistApp() -> PlistDict? {
|
||||
switch self.type {
|
||||
case .IPA, .Archive, .Extension:
|
||||
return self.readPayloadFile("Info.plist", osxSubdir: nil)?.asPlistOrNil()
|
||||
case .APK:
|
||||
return nil // not applicable for Android
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
65
src/Plist+Icon.swift
Normal file
65
src/Plist+Icon.swift
Normal file
@@ -0,0 +1,65 @@
|
||||
|
||||
extension MetaInfo {
|
||||
/// Read `Info.plist`. (used for `ThumbnailProvider`)
|
||||
func readPlist_Icon() -> Plist_Icon? {
|
||||
if let x = self.readPayloadFile("Info.plist", osxSubdir: nil)?.asPlistOrNil() {
|
||||
return Plist_Icon(x)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Plist_Icon
|
||||
|
||||
/// Representation of `Info.plist` (containing only the icon extractor).
|
||||
/// Seperate from main class because everything else is not needed for `ThumbnailProvider`
|
||||
struct Plist_Icon {
|
||||
let filenames: [String]
|
||||
|
||||
init(_ plist: PlistDict) {
|
||||
filenames = parseIconNames(plist)
|
||||
}
|
||||
}
|
||||
|
||||
/// Find icon filenames.
|
||||
/// @return Filenames which do not necessarily exist on filesystem. This may include `@2x` and/or no file extension.
|
||||
private func parseIconNames(_ plist: PlistDict) -> [String] {
|
||||
// Check for CFBundleIcons (since 5.0)
|
||||
if let icons = unpackNameList(plist["CFBundleIcons"]) {
|
||||
return icons
|
||||
}
|
||||
// iPad-only apps
|
||||
if let icons = unpackNameList(plist["CFBundleIcons~ipad"]) {
|
||||
return icons
|
||||
}
|
||||
// Check for CFBundleIconFiles (since 3.2)
|
||||
if let icons = plist["CFBundleIconFiles"] as? [String], !icons.isEmpty {
|
||||
return icons
|
||||
}
|
||||
// key found on iTunesU app
|
||||
if let icons = plist["Icon files"] as? [String], !icons.isEmpty {
|
||||
return icons
|
||||
}
|
||||
// Check for CFBundleIconFile (legacy, before 3.2)
|
||||
if let icon = plist["CFBundleIconFile"] as? String { // may be nil
|
||||
return [icon]
|
||||
}
|
||||
return []
|
||||
}
|
||||
|
||||
/// Deep select icons from plist key `CFBundleIcons` and `CFBundleIcons~ipad`
|
||||
/// @return Guarantees a non-empty array (or `nil`)
|
||||
private func unpackNameList(_ bundleDict: Any?) -> [String]? {
|
||||
if let bundleDict = bundleDict as? PlistDict {
|
||||
if let primaryDict = bundleDict["CFBundlePrimaryIcon"] as? PlistDict {
|
||||
if let icons = primaryDict["CFBundleIconFiles"] as? [String], !icons.isEmpty {
|
||||
return icons
|
||||
}
|
||||
if let name = primaryDict["CFBundleIconName"] as? String { // key found on a .tipa file
|
||||
return [name]
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
70
src/Plist+Info.swift
Normal file
70
src/Plist+Info.swift
Normal file
@@ -0,0 +1,70 @@
|
||||
import Foundation
|
||||
|
||||
extension MetaInfo {
|
||||
/// Read `Info.plist`. (used for `PreviewProvider`)
|
||||
func readPlist_Info() -> Plist_Info? {
|
||||
if let x = self.readPayloadFile("Info.plist", osxSubdir: nil)?.asPlistOrNil() {
|
||||
return Plist_Info(x, isOSX: isOSX)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Plist_Info
|
||||
|
||||
/// Representation of `Info.plist` of an `.ipa` bundle
|
||||
struct Plist_Info {
|
||||
let bundleId: String?
|
||||
let name: String?
|
||||
let version: String?
|
||||
let buildVersion: String?
|
||||
|
||||
let exePath: String?
|
||||
let sdkVersion: String?
|
||||
let minOS: String?
|
||||
let extensionType: String?
|
||||
|
||||
let icons: [String]
|
||||
let deviceFamily: [String]
|
||||
let transportSecurity: PlistDict?
|
||||
|
||||
init(_ plist: PlistDict, isOSX: Bool) {
|
||||
bundleId = plist["CFBundleIdentifier"] as? String
|
||||
name = plist["CFBundleDisplayName"] as? String ?? plist["CFBundleName"] as? String
|
||||
version = plist["CFBundleShortVersionString"] as? String
|
||||
buildVersion = plist["CFBundleVersion"] as? String
|
||||
exePath = plist["CFBundleExecutable"] as? String
|
||||
sdkVersion = plist["DTSDKName"] as? String
|
||||
minOS = plist[isOSX ? "LSMinimumSystemVersion" : "MinimumOSVersion"] as? String
|
||||
extensionType = (plist["NSExtension"] as? PlistDict)?["NSExtensionPointIdentifier"] as? String
|
||||
icons = Plist_Icon(plist).filenames
|
||||
deviceFamily = parseDeviceFamily(plist, isOSX: isOSX)
|
||||
transportSecurity = plist["NSAppTransportSecurity"] as? PlistDict
|
||||
}
|
||||
}
|
||||
|
||||
private func parseDeviceFamily(_ plist: PlistDict, isOSX: Bool) -> [String] {
|
||||
if isOSX {
|
||||
return plist["CFBundleSupportedPlatforms"] as? [String] ?? ["macOS"]
|
||||
}
|
||||
|
||||
if let platforms = (plist["UIDeviceFamily"] as? [Int])?.compactMap({
|
||||
switch $0 {
|
||||
case 1: "iPhone"
|
||||
case 2: "iPad"
|
||||
case 3: "TV"
|
||||
case 4: "Watch"
|
||||
default: nil
|
||||
}
|
||||
}), platforms.count > 0 {
|
||||
return platforms
|
||||
}
|
||||
|
||||
if let minVersion = plist["MinimumOSVersion"] as? String {
|
||||
if minVersion.hasPrefix("1.") || minVersion.hasPrefix("2.") || minVersion.hasPrefix("3.") {
|
||||
return ["iPhone"]
|
||||
}
|
||||
}
|
||||
return []
|
||||
}
|
||||
61
src/Plist+MobileProvision.swift
Normal file
61
src/Plist+MobileProvision.swift
Normal file
@@ -0,0 +1,61 @@
|
||||
import Foundation
|
||||
|
||||
extension MetaInfo {
|
||||
/// Read `embedded.mobileprovision` (if available) and decode with CMS decoder.
|
||||
func readPlist_MobileProvision() -> Plist_MobileProvision? {
|
||||
guard let provisionData = self.readPayloadFile("embedded.mobileprovision", osxSubdir: nil),
|
||||
let plist = provisionData.decodeCMS().asPlistOrNil() else {
|
||||
return nil
|
||||
}
|
||||
return Plist_MobileProvision(plist, isOSX: self.isOSX)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Plist_MobileProvision
|
||||
|
||||
/// Representation of `embedded.mobileprovision`
|
||||
struct Plist_MobileProvision {
|
||||
let creationDate: Date?
|
||||
let expireDate: Date?
|
||||
let profileId: String?
|
||||
let profileName: String?
|
||||
/// Something like "Development" or "Distribution (App Store)".
|
||||
let profileType: String
|
||||
/// Either "Mac" or "iOS"
|
||||
let profilePlatform: String
|
||||
let teamName: String?
|
||||
let teamIds: [String]
|
||||
let devices: [String]
|
||||
let certificates: [ProvisioningCertificate]
|
||||
let entitlements: PlistDict?
|
||||
|
||||
init(_ plist: PlistDict, isOSX: Bool) {
|
||||
creationDate = plist["CreationDate"] as? Date
|
||||
expireDate = plist["ExpirationDate"] as? Date
|
||||
profileId = plist["UUID"] as? String
|
||||
profileName = plist["Name"] as? String
|
||||
profileType = parseProfileType(plist, isOSX: isOSX)
|
||||
profilePlatform = isOSX ? "Mac" : "iOS"
|
||||
teamName = plist["TeamName"] as? String
|
||||
teamIds = plist["TeamIdentifier"] as? [String] ?? []
|
||||
devices = plist["ProvisionedDevices"] as? [String] ?? []
|
||||
certificates = (plist["DeveloperCertificates"] as? [Data] ?? []).compactMap {
|
||||
ProvisioningCertificate($0)
|
||||
}
|
||||
entitlements = plist["Entitlements"] as? PlistDict
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns provision type string like "Development" or "Distribution (App Store)".
|
||||
private func parseProfileType(_ plist: PlistDict, isOSX: Bool) -> String {
|
||||
let hasDevices = plist["ProvisionedDevices"] is [Any]
|
||||
if isOSX {
|
||||
return hasDevices ? "Development" : "Distribution (App Store)"
|
||||
}
|
||||
if hasDevices {
|
||||
let getTaskAllow = (plist["Entitlements"] as? PlistDict)?["get-task-allow"] as? Bool ?? false
|
||||
return getTaskAllow ? "Development" : "Distribution (Ad Hoc)"
|
||||
}
|
||||
let isEnterprise = plist["ProvisionsAllDevices"] as? Bool ?? false
|
||||
return isEnterprise ? "Enterprise" : "Distribution (App Store)"
|
||||
}
|
||||
77
src/Plist+iTunesMetadata.swift
Normal file
77
src/Plist+iTunesMetadata.swift
Normal file
@@ -0,0 +1,77 @@
|
||||
import Foundation
|
||||
|
||||
extension MetaInfo {
|
||||
/// Read `iTunesMetadata.plist` (if available)
|
||||
func readPlist_iTunesMetadata() -> Plist_iTunesMetadata? {
|
||||
assert(type == .IPA)
|
||||
// not `readPayloadFile` because plist is in root dir
|
||||
guard let plist = self.zipFile!.unzipFile("iTunesMetadata.plist")?.asPlistOrNil() else {
|
||||
return nil
|
||||
}
|
||||
return Plist_iTunesMetadata(plist)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Plist_iTunesMetadata
|
||||
|
||||
/// Representation of `iTunesMetadata.plist`
|
||||
struct Plist_iTunesMetadata {
|
||||
let appId: Int?
|
||||
let appName: String?
|
||||
let price: String?
|
||||
let genres: [String]
|
||||
// purchase info
|
||||
let releaseDate: Date?
|
||||
let purchaseDate: Date?
|
||||
// account info
|
||||
let appleId: String?
|
||||
let firstName: String?
|
||||
let lastName: String?
|
||||
|
||||
init(_ plist: PlistDict) {
|
||||
appId = plist["itemId"] as? Int
|
||||
appName = plist["itemName"] as? String
|
||||
price = plist["priceDisplay"] as? String
|
||||
genres = formattedGenres(plist)
|
||||
// download info
|
||||
let downloadInfo = plist["com.apple.iTunesStore.downloadInfo"] as? PlistDict
|
||||
purchaseDate = Date.parseAny(downloadInfo?["purchaseDate"] ?? plist["purchaseDate"])
|
||||
releaseDate = Date.parseAny(downloadInfo?["releaseDate"] ?? plist["releaseDate"])
|
||||
// AppleId & purchaser name
|
||||
let accountInfo = downloadInfo?["accountInfo"] as? PlistDict ?? [:]
|
||||
appleId = accountInfo["AppleID"] as? String ?? plist["appleId"] as? String
|
||||
firstName = accountInfo["FirstName"] as? String
|
||||
lastName = accountInfo["LastName"] as? String
|
||||
}
|
||||
|
||||
/// Returns `"<firstName> <lastName> (<appleId>)"` (with empty values omitted)
|
||||
var purchaserName: String? {
|
||||
let fn = firstName ?? ""
|
||||
let ln = lastName ?? ""
|
||||
let aid = appleId ?? ""
|
||||
switch (fn.isEmpty, ln.isEmpty, aid.isEmpty) {
|
||||
case (true, true, true): return nil
|
||||
case (true, true, false): return "\(aid)"
|
||||
case (_, _, false): return "\(fn) \(ln) (\(aid))"
|
||||
case (_, _, true): return "\(fn) \(ln)"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Concatenate all (sub)genres into flat list.
|
||||
private func formattedGenres(_ plist: PlistDict) -> [String] {
|
||||
var genres: [String] = []
|
||||
let genreId = plist["genreId"] as? Int ?? 0
|
||||
if let mainGenre = AppCategories[genreId] ?? plist["genre"] as? String {
|
||||
genres.append(mainGenre)
|
||||
}
|
||||
|
||||
for subgenre in plist["subgenres"] as? [PlistDict] ?? [] {
|
||||
let subgenreId = subgenre["genreId"] as? Int ?? 0
|
||||
if let subgenreStr = AppCategories[subgenreId] ?? subgenre["genre"] as? String {
|
||||
genres.append(subgenreStr)
|
||||
}
|
||||
}
|
||||
return genres
|
||||
}
|
||||
@@ -1,45 +1,21 @@
|
||||
import Foundation
|
||||
|
||||
extension PreviewGenerator {
|
||||
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"
|
||||
case 3: return "TV"
|
||||
case 4: return "Watch"
|
||||
default: return nil
|
||||
}
|
||||
}).joined(separator: ", ")
|
||||
|
||||
let minVersion = appPlist["MinimumOSVersion"] as? String ?? ""
|
||||
if platforms?.isEmpty ?? true, minVersion.hasPrefix("1.") || minVersion.hasPrefix("2.") || minVersion.hasPrefix("3.") {
|
||||
return "iPhone"
|
||||
}
|
||||
return platforms ?? ""
|
||||
}
|
||||
|
||||
/// Process info stored in `Info.plist`
|
||||
mutating func procAppInfoApple(_ appPlist: PlistDict, isOSX: Bool) {
|
||||
let minVersion = appPlist[isOSX ? "LSMinimumSystemVersion" : "MinimumOSVersion"] as? String ?? ""
|
||||
|
||||
let extensionType = (appPlist["NSExtension"] as? PlistDict)?["NSExtensionPointIdentifier"] as? String
|
||||
mutating func procAppInfoApple(_ appPlist: Plist_Info) {
|
||||
self.apply([
|
||||
"AppInfoHidden": CLASS_VISIBLE,
|
||||
"AppName": appPlist["CFBundleDisplayName"] as? String ?? appPlist["CFBundleName"] as? String ?? "",
|
||||
"AppVersion": appPlist["CFBundleShortVersionString"] as? String ?? "",
|
||||
"AppBuildVer": appPlist["CFBundleVersion"] as? String ?? "",
|
||||
"AppId": appPlist["CFBundleIdentifier"] as? String ?? "",
|
||||
"AppName": appPlist.name ?? "",
|
||||
"AppVersion": appPlist.version ?? "",
|
||||
"AppBuildVer": appPlist.buildVersion ?? "",
|
||||
"AppId": appPlist.bundleId ?? "",
|
||||
|
||||
"AppExtensionTypeHidden": extensionType != nil ? CLASS_VISIBLE : CLASS_HIDDEN,
|
||||
"AppExtensionType": extensionType ?? "",
|
||||
"AppExtensionTypeHidden": appPlist.extensionType != nil ? CLASS_VISIBLE : CLASS_HIDDEN,
|
||||
"AppExtensionType": appPlist.extensionType ?? "",
|
||||
|
||||
"AppDeviceFamily": deviceFamilyList(appPlist, isOSX: isOSX),
|
||||
"AppSDK": appPlist["DTSDKName"] as? String ?? "",
|
||||
"AppMinOS": minVersion,
|
||||
"AppDeviceFamily": appPlist.deviceFamily.joined(separator: ", "),
|
||||
"AppSDK": appPlist.sdkVersion ?? "",
|
||||
"AppMinOS": appPlist.minOS ?? "",
|
||||
])
|
||||
}
|
||||
|
||||
|
||||
@@ -22,9 +22,9 @@ extension PreviewGenerator {
|
||||
}
|
||||
|
||||
/// Process compiled binary and provision plist to extract `Entitlements`
|
||||
mutating func procEntitlements(_ meta: MetaInfo, _ appPlist: PlistDict?, _ provisionPlist: PlistDict?) {
|
||||
var entitlements = readEntitlements(meta, appPlist?["CFBundleExecutable"] as? String)
|
||||
entitlements.applyFallbackIfNeeded(provisionPlist?["Entitlements"] as? PlistDict)
|
||||
mutating func procEntitlements(_ meta: MetaInfo, _ appPlist: Plist_Info?, _ provisionPlist: Plist_MobileProvision?) {
|
||||
var entitlements = readEntitlements(meta, appPlist?.exePath)
|
||||
entitlements.applyFallbackIfNeeded(provisionPlist?.entitlements)
|
||||
|
||||
if entitlements.html == nil && !entitlements.hasError {
|
||||
return
|
||||
|
||||
@@ -1,148 +1,57 @@
|
||||
import Foundation
|
||||
import os // OSLog
|
||||
|
||||
private let log = OSLog(subsystem: Bundle.main.bundleIdentifier!, category: "Html+Certificates")
|
||||
|
||||
|
||||
extension MetaInfo {
|
||||
/// Read `embedded.mobileprovision` file and decode with CMS decoder.
|
||||
func readPlistProvision() -> PlistDict? {
|
||||
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
|
||||
}
|
||||
|
||||
var decoder: CMSDecoder? = nil
|
||||
CMSDecoderCreate(&decoder)
|
||||
let data = provisionData.withUnsafeBytes { ptr in
|
||||
CMSDecoderUpdateMessage(decoder!, ptr.baseAddress!, provisionData.count)
|
||||
CMSDecoderFinalizeMessage(decoder!)
|
||||
var dataRef: CFData?
|
||||
CMSDecoderCopyContent(decoder!, &dataRef)
|
||||
return Data(referencing: dataRef!)
|
||||
}
|
||||
return data.asPlistOrNil()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
extension PreviewGenerator {
|
||||
|
||||
// MARK: - Certificates
|
||||
|
||||
/// Process a single certificate. Extract invalidity / expiration date.
|
||||
/// @param subject just used for printing error logs.
|
||||
private func getCertificateInvalidityDate(_ certificate: SecCertificate, subject: String) -> Date? {
|
||||
var error: Unmanaged<CFError>?
|
||||
guard let outerDict = SecCertificateCopyValues(certificate, [kSecOIDInvalidityDate] as CFArray, &error) as? PlistDict else {
|
||||
os_log(.error, log: log, "Could not get values in '%{public}@' certificate, error = %{public}@", subject, error?.takeUnretainedValue().localizedDescription ?? "unknown error")
|
||||
return nil
|
||||
}
|
||||
guard let innerDict = outerDict[kSecOIDInvalidityDate as String] as? PlistDict else {
|
||||
os_log(.error, log: log, "No invalidity values in '%{public}@' certificate, dictionary = %{public}@", subject, outerDict)
|
||||
return nil
|
||||
}
|
||||
// NOTE: the invalidity date type of kSecPropertyTypeDate is documented as a CFStringRef in the "Certificate, Key, and Trust Services Reference".
|
||||
// In reality, it's a __NSTaggedDate (presumably a tagged pointer representing an NSDate.) But to be sure, we'll check:
|
||||
guard let dateString = innerDict[kSecPropertyKeyValue as String] else {
|
||||
os_log(.error, log: log, "No invalidity date in '%{public}@' certificate, dictionary = %{public}@", subject, innerDict)
|
||||
return nil
|
||||
}
|
||||
return Date.parseAny(dateString)
|
||||
}
|
||||
|
||||
/// Process list of all certificates. Return a two column table with subject and expiration date.
|
||||
private func getCertificateList(_ provisionPlist: PlistDict) -> [TableRow] {
|
||||
guard let certs = provisionPlist["DeveloperCertificates"] as? [Data] else {
|
||||
return []
|
||||
}
|
||||
return certs.compactMap {
|
||||
guard let cert = SecCertificateCreateWithData(nil, $0 as CFData) else {
|
||||
return nil
|
||||
}
|
||||
guard let subject = SecCertificateCopySubjectSummary(cert) as? String else {
|
||||
os_log(.error, log: log, "Could not get subject from certificate")
|
||||
return nil
|
||||
}
|
||||
let expiration: String
|
||||
if let invalidityDate = getCertificateInvalidityDate(cert, subject: subject) {
|
||||
expiration = invalidityDate.relativeExpirationDateString()
|
||||
} else {
|
||||
expiration = "<span class='warning'>No invalidity date in certificate</span>"
|
||||
}
|
||||
return TableRow([subject, expiration])
|
||||
}.sorted { $0[0] < $1[0] }
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Provisioning
|
||||
|
||||
/// Returns provision type string like "Development" or "Distribution (App Store)".
|
||||
private func stringForProfileType(_ provisionPlist: PlistDict, isOSX: Bool) -> String {
|
||||
let hasDevices = provisionPlist["ProvisionedDevices"] is [Any]
|
||||
if isOSX {
|
||||
return hasDevices ? "Development" : "Distribution (App Store)"
|
||||
}
|
||||
if hasDevices {
|
||||
let getTaskAllow = (provisionPlist["Entitlements"] as? PlistDict)?["get-task-allow"] as? Bool ?? false
|
||||
return getTaskAllow ? "Development" : "Distribution (Ad Hoc)"
|
||||
}
|
||||
let isEnterprise = provisionPlist["ProvisionsAllDevices"] as? Bool ?? false
|
||||
return isEnterprise ? "Enterprise" : "Distribution (App Store)"
|
||||
}
|
||||
|
||||
/// Enumerate all entries from provison plist with key `ProvisionedDevices`
|
||||
private func getDeviceList(_ provisionPlist: PlistDict) -> [TableRow] {
|
||||
guard let devArr = provisionPlist["ProvisionedDevices"] as? [String] else {
|
||||
return []
|
||||
}
|
||||
var currentPrefix: String? = nil
|
||||
return devArr.sorted().map { device in
|
||||
// compute the prefix for the first column of the table
|
||||
let displayPrefix: String
|
||||
let devicePrefix = String(device.prefix(1))
|
||||
if currentPrefix != devicePrefix {
|
||||
currentPrefix = devicePrefix
|
||||
displayPrefix = "\(devicePrefix) ➞ "
|
||||
} else {
|
||||
displayPrefix = ""
|
||||
}
|
||||
return [displayPrefix, device]
|
||||
}
|
||||
}
|
||||
|
||||
/// Process info stored in `embedded.mobileprovision`
|
||||
mutating func procProvision(_ provisionPlist: PlistDict?, isOSX: Bool) {
|
||||
mutating func procProvision(_ provisionPlist: Plist_MobileProvision?) {
|
||||
guard let provisionPlist else {
|
||||
return
|
||||
}
|
||||
|
||||
let creationDate = provisionPlist["CreationDate"] as? Date
|
||||
let expireDate = provisionPlist["ExpirationDate"] as? Date
|
||||
let devices = getDeviceList(provisionPlist)
|
||||
let certs = getCertificateList(provisionPlist)
|
||||
|
||||
let deviceCount = provisionPlist.devices.count
|
||||
self.apply([
|
||||
"ProvisionHidden": CLASS_VISIBLE,
|
||||
"ProvisionProfileName": provisionPlist["Name"] as? String ?? "",
|
||||
"ProvisionProfileId": provisionPlist["UUID"] as? String ?? "",
|
||||
"ProvisionTeamName": provisionPlist["TeamName"] as? String ?? "<em>Team name not available</em>",
|
||||
"ProvisionTeamIds": (provisionPlist["TeamIdentifier"] as? [String])?.joined(separator: ", ") ?? "<em>Team ID not available</em>",
|
||||
"ProvisionCreateDate": creationDate?.formattedCreationDate() ?? "",
|
||||
"ProvisionExpireDate": expireDate?.formattedExpirationDate() ?? "",
|
||||
"ProvisionExpireStatus": ExpirationStatus(expireDate).cssClass(),
|
||||
"ProvisionProfileName": provisionPlist.profileName ?? "",
|
||||
"ProvisionProfileId": provisionPlist.profileId ?? "",
|
||||
"ProvisionTeamName": provisionPlist.teamName ?? "<em>Team name not available</em>",
|
||||
"ProvisionTeamIds": provisionPlist.teamIds.isEmpty ? "<em>Team ID not available</em>" : provisionPlist.teamIds.joined(separator: ", "),
|
||||
"ProvisionCreateDate": provisionPlist.creationDate?.formattedCreationDate() ?? "",
|
||||
"ProvisionExpireDate": provisionPlist.expireDate?.formattedExpirationDate() ?? "",
|
||||
"ProvisionExpireStatus": ExpirationStatus(provisionPlist.expireDate).cssClass(),
|
||||
|
||||
"ProvisionProfilePlatform": isOSX ? "Mac" : "iOS",
|
||||
"ProvisionProfileType": stringForProfileType(provisionPlist, isOSX: isOSX),
|
||||
"ProvisionProfilePlatform": provisionPlist.profilePlatform,
|
||||
"ProvisionProfileType": provisionPlist.profileType,
|
||||
|
||||
"ProvisionDeviceCount": devices.isEmpty ? "No Devices" : "\(devices.count) Device\(devices.count == 1 ? "" : "s")",
|
||||
"ProvisionDeviceIds": devices.isEmpty ? "Distribution Profile" : formatAsTable(devices, header: ["", "UDID"]),
|
||||
"ProvisionDeviceCount": deviceCount == 0 ? "No Devices" : "\(deviceCount) Device\(deviceCount == 1 ? "" : "s")",
|
||||
"ProvisionDeviceIds": deviceCount == 0 ? "Distribution Profile" : formatAsTable(groupDevices(provisionPlist.devices), header: ["", "UDID"]),
|
||||
|
||||
"ProvisionDevelopCertificates": certs.isEmpty ? "No Developer Certificates" : formatAsTable(certs),
|
||||
"ProvisionDevelopCertificates": provisionPlist.certificates.isEmpty ? "No Developer Certificates"
|
||||
: formatAsTable(
|
||||
provisionPlist.certificates
|
||||
.sorted { $0.subject < $1.subject }
|
||||
.map {TableRow([$0.subject, $0.expiration?.relativeExpirationDateString() ?? "<span class='warning'>No invalidity date in certificate</span>"])}
|
||||
),
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
/// Group device ids by first letter (`d -> device02`)
|
||||
private func groupDevices(_ devices: [String]) -> [TableRow] {
|
||||
var currentPrefix: String? = nil
|
||||
return devices.sorted().map { device in
|
||||
// compute the prefix for the first column of the table
|
||||
let displayPrefix: String
|
||||
let devicePrefix = String(device.prefix(1))
|
||||
if currentPrefix != devicePrefix {
|
||||
currentPrefix = devicePrefix
|
||||
displayPrefix = "\(devicePrefix) ➞ "
|
||||
} else {
|
||||
displayPrefix = ""
|
||||
}
|
||||
return [displayPrefix, device]
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private typealias TableRow = [String]
|
||||
|
||||
/// Print html table with arbitrary number of columns
|
||||
|
||||
@@ -43,8 +43,8 @@ private func recursiveTransportSecurity(_ dictionary: PlistDict, _ level: Int =
|
||||
|
||||
extension PreviewGenerator {
|
||||
/// Process ATS info in `Info.plist`
|
||||
mutating func procTransportSecurity(_ appPlist: PlistDict?) {
|
||||
guard let value = appPlist?["NSAppTransportSecurity"] as? PlistDict else {
|
||||
mutating func procTransportSecurity(_ appPlist: Plist_Info?) {
|
||||
guard let value = appPlist?.transportSecurity else {
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -1,69 +1,21 @@
|
||||
import Foundation
|
||||
|
||||
extension MetaInfo {
|
||||
/// Read `iTunesMetadata.plist` if available
|
||||
func readPlistItunes() -> PlistDict? {
|
||||
switch self.type {
|
||||
case .IPA:
|
||||
// not `readPayloadFile` because plist is in root dir
|
||||
return self.zipFile!.unzipFile("iTunesMetadata.plist")?.asPlistOrNil()
|
||||
case .Archive, .Extension, .APK:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
extension PreviewGenerator {
|
||||
/// Concatenate all (sub)genres into a comma separated list.
|
||||
private func formattedGenres(_ itunesPlist: PlistDict) -> String {
|
||||
var genres: [String] = []
|
||||
let genreId = itunesPlist["genreId"] as? Int ?? 0
|
||||
if let mainGenre = AppCategories[genreId] ?? itunesPlist["genre"] as? String {
|
||||
genres.append(mainGenre)
|
||||
}
|
||||
|
||||
for subgenre in itunesPlist["subgenres"] as? [PlistDict] ?? [] {
|
||||
let subgenreId = subgenre["genreId"] as? Int ?? 0
|
||||
if let subgenreStr = AppCategories[subgenreId] ?? subgenre["genre"] as? String {
|
||||
genres.append(subgenreStr)
|
||||
}
|
||||
}
|
||||
return genres.joined(separator: ", ")
|
||||
}
|
||||
|
||||
/// Process info stored in `iTunesMetadata.plist`
|
||||
mutating func procItunesMeta(_ itunesPlist: PlistDict?) {
|
||||
mutating func procItunesMeta(_ itunesPlist: Plist_iTunesMetadata?) {
|
||||
guard let itunesPlist else {
|
||||
return
|
||||
}
|
||||
|
||||
let downloadInfo = itunesPlist["com.apple.iTunesStore.downloadInfo"] as? PlistDict
|
||||
let accountInfo = downloadInfo?["accountInfo"] as? PlistDict ?? [:]
|
||||
|
||||
let purchaseDate = Date.parseAny(downloadInfo?["purchaseDate"] ?? itunesPlist["purchaseDate"])
|
||||
let releaseDate = Date.parseAny(downloadInfo?["releaseDate"] ?? itunesPlist["releaseDate"])
|
||||
// AppleId & purchaser name
|
||||
let appleId = accountInfo["AppleID"] as? String ?? itunesPlist["appleId"] as? String ?? ""
|
||||
let firstName = accountInfo["FirstName"] as? String ?? ""
|
||||
let lastName = accountInfo["LastName"] as? String ?? ""
|
||||
|
||||
let name: String
|
||||
if !firstName.isEmpty || !lastName.isEmpty {
|
||||
name = "\(firstName) \(lastName) (\(appleId))"
|
||||
} else {
|
||||
name = appleId
|
||||
}
|
||||
self.apply([
|
||||
"iTunesHidden": CLASS_VISIBLE,
|
||||
"iTunesId": (itunesPlist["itemId"] as? Int)?.description ?? "",
|
||||
"iTunesName": itunesPlist["itemName"] as? String ?? "",
|
||||
"iTunesGenres": formattedGenres(itunesPlist),
|
||||
"iTunesReleaseDate": releaseDate?.mediumFormat() ?? "",
|
||||
"iTunesId": itunesPlist.appId?.description ?? "",
|
||||
"iTunesName": itunesPlist.appName ?? "",
|
||||
"iTunesGenres": itunesPlist.genres.joined(separator: ", "),
|
||||
"iTunesReleaseDate": itunesPlist.releaseDate?.mediumFormat() ?? "",
|
||||
|
||||
"iTunesAppleId": name,
|
||||
"iTunesPurchaseDate": purchaseDate?.mediumFormat() ?? "",
|
||||
"iTunesPrice": itunesPlist["priceDisplay"] as? String ?? "",
|
||||
"iTunesAppleId": itunesPlist.purchaserName ?? "",
|
||||
"iTunesPurchaseDate": itunesPlist.purchaseDate?.mediumFormat() ?? "",
|
||||
"iTunesPrice": itunesPlist.price ?? "",
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,22 +26,22 @@ struct PreviewGenerator {
|
||||
|
||||
switch meta.type {
|
||||
case .IPA, .Archive, .Extension:
|
||||
guard let plistApp = meta.readPlistApp() else {
|
||||
guard let plistApp = meta.readPlist_Info() else {
|
||||
throw RuntimeError("Info.plist not found")
|
||||
}
|
||||
procAppInfoApple(plistApp, isOSX: meta.isOSX)
|
||||
procAppInfoApple(plistApp)
|
||||
if meta.type == .IPA {
|
||||
procItunesMeta(meta.readPlistItunes())
|
||||
procItunesMeta(meta.readPlist_iTunesMetadata())
|
||||
} else if meta.type == .Archive {
|
||||
procArchiveInfo(meta.readPlistXCArchive())
|
||||
}
|
||||
procTransportSecurity(plistApp)
|
||||
|
||||
let plistProvision = meta.readPlistProvision()
|
||||
let plistProvision = meta.readPlist_MobileProvision()
|
||||
procEntitlements(meta, plistApp, plistProvision)
|
||||
procProvision(plistProvision, isOSX: meta.isOSX)
|
||||
procProvision(plistProvision)
|
||||
// App Icon (last, because the image uses a lot of memory)
|
||||
data["AppIcon"] = AppIcon(meta).extractImage(from: plistApp).withRoundCorners().asBase64()
|
||||
data["AppIcon"] = AppIcon(meta).extractImage(from: plistApp.icons).withRoundCorners().asBase64()
|
||||
|
||||
case .APK:
|
||||
guard let manifest = meta.readApkManifest() else {
|
||||
|
||||
58
src/Provisioning.swift
Normal file
58
src/Provisioning.swift
Normal file
@@ -0,0 +1,58 @@
|
||||
import Foundation
|
||||
import os // OSLog
|
||||
|
||||
private let log = OSLog(subsystem: Bundle.main.bundleIdentifier!, category: "Provisioning")
|
||||
|
||||
extension Data {
|
||||
/// In-memory decode of `embedded.mobileprovision`
|
||||
func decodeCMS() -> Data {
|
||||
var decoder: CMSDecoder? = nil
|
||||
CMSDecoderCreate(&decoder)
|
||||
return self.withUnsafeBytes { ptr in
|
||||
CMSDecoderUpdateMessage(decoder!, ptr.baseAddress!, self.count)
|
||||
CMSDecoderFinalizeMessage(decoder!)
|
||||
var dataRef: CFData?
|
||||
CMSDecoderCopyContent(decoder!, &dataRef)
|
||||
return Data(referencing: dataRef!)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct ProvisioningCertificate {
|
||||
let subject: String
|
||||
let expiration: Date?
|
||||
|
||||
/// Parse subject and expiration date from certificate.
|
||||
init?(_ data: Data) {
|
||||
guard let cert = SecCertificateCreateWithData(nil, data as CFData) else {
|
||||
return nil
|
||||
}
|
||||
guard let subj = SecCertificateCopySubjectSummary(cert) as? String else {
|
||||
os_log(.error, log: log, "Could not get subject from certificate")
|
||||
return nil
|
||||
}
|
||||
subject = subj
|
||||
expiration = parseInvalidityDate(cert, subject: subj)
|
||||
}
|
||||
}
|
||||
|
||||
/// Process a single certificate. Extract invalidity / expiration date.
|
||||
/// @param subject just used for printing error logs.
|
||||
private func parseInvalidityDate(_ certificate: SecCertificate, subject: String) -> Date? {
|
||||
var error: Unmanaged<CFError>?
|
||||
guard let outerDict = SecCertificateCopyValues(certificate, [kSecOIDInvalidityDate] as CFArray, &error) as? PlistDict else {
|
||||
os_log(.error, log: log, "Could not get values in '%{public}@' certificate, error = %{public}@", subject, error?.takeUnretainedValue().localizedDescription ?? "unknown error")
|
||||
return nil
|
||||
}
|
||||
guard let innerDict = outerDict[kSecOIDInvalidityDate as String] as? PlistDict else {
|
||||
os_log(.error, log: log, "No invalidity values in '%{public}@' certificate, dictionary = %{public}@", subject, outerDict)
|
||||
return nil
|
||||
}
|
||||
// NOTE: the invalidity date type of kSecPropertyTypeDate is documented as a CFStringRef in the "Certificate, Key, and Trust Services Reference".
|
||||
// In reality, it's a __NSTaggedDate (presumably a tagged pointer representing an NSDate.) But to be sure, we'll check:
|
||||
guard let dateString = innerDict[kSecPropertyKeyValue as String] else {
|
||||
os_log(.error, log: log, "No invalidity date in '%{public}@' certificate, dictionary = %{public}@", subject, innerDict)
|
||||
return nil
|
||||
}
|
||||
return Date.parseAny(dateString)
|
||||
}
|
||||
Reference in New Issue
Block a user